artefacts-cli 0.6.13__tar.gz → 0.6.17__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/PKG-INFO +3 -1
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/__init__.py +1 -1
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/app.py +68 -52
- artefacts_cli-0.6.17/artefacts/cli/app_containers.py +98 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/constants.py +2 -0
- artefacts_cli-0.6.17/artefacts/cli/containers/__init__.py +68 -0
- artefacts_cli-0.6.17/artefacts/cli/containers/docker.py +104 -0
- artefacts_cli-0.6.17/artefacts/cli/containers/utils.py +57 -0
- artefacts_cli-0.6.17/artefacts/cli/utils.py +99 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/version.py +2 -2
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts.yaml +3 -3
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts_cli.egg-info/PKG-INFO +3 -1
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts_cli.egg-info/SOURCES.txt +7 -1
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts_cli.egg-info/requires.txt +2 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/pyproject.toml +2 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/pytest.ini +1 -1
- artefacts_cli-0.6.17/tests/cli/test_app_containers.py +54 -0
- artefacts_cli-0.6.17/tests/conftest.py +42 -0
- artefacts_cli-0.6.17/tests/utils/docker_mock.py +89 -0
- artefacts_cli-0.6.13/artefacts/cli/utils.py +0 -35
- artefacts_cli-0.6.13/tests/conftest.py +0 -8
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/README.md +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/README_INTERNAL.md +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/bagparser.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/other.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/parameters.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/ros1.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/ros2.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/cli/utils_ros.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts/wrappers/artefacts_ros1_meta.launch +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts_cli.egg-info/dependency_links.txt +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts_cli.egg-info/entry_points.txt +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/artefacts_cli.egg-info/top_level.txt +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/bin/release +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/CMakeLists.txt +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_meta.launch +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_turtle.launch +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/turtle_odometry.launch +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/package.xml +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/setup.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/TestTurtle.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/__init__.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_odom.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_post_process.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_trajectory.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/test/viz_turtle_odom.xml +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim2/launch_turtle.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/infra-tests/turtlesim2/sample_node.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/setup.cfg +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/__init__.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/__init__.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_cli.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_config_validation.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_other.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_parameters.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_ros1.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_ros2.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/cli/test_warp.py +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/fixtures/artefacts_deprecated.yaml +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/fixtures/artefacts_ros1.yaml +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/fixtures/warp-env-param.yaml +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/fixtures/warp.yaml +0 -0
- {artefacts_cli-0.6.13 → artefacts_cli-0.6.17}/tests/test_config_validation.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.17
|
4
4
|
Author-email: FD <fabian@artefacts.com>, AGC <alejandro@artefacts.com>, TN <tomo@artefacts.com>, EP <eric@artefacts.com>
|
5
5
|
Project-URL: Homepage, https://github.com/art-e-fact/artefacts-client
|
6
6
|
Project-URL: Bug Tracker, https://github.com/art-e-fact/artefacts-client/issues
|
@@ -18,9 +18,11 @@ Requires-Dist: mcap-ros2-support
|
|
18
18
|
Requires-Dist: PyYAML>=6.0
|
19
19
|
Requires-Dist: requests>=2.27.1
|
20
20
|
Requires-Dist: setuptools-scm
|
21
|
+
Requires-Dist: setuptools>=74
|
21
22
|
Provides-Extra: dev
|
22
23
|
Requires-Dist: awscli; extra == "dev"
|
23
24
|
Requires-Dist: build; extra == "dev"
|
25
|
+
Requires-Dist: docker; extra == "dev"
|
24
26
|
Requires-Dist: lark; extra == "dev"
|
25
27
|
Requires-Dist: pyre-check; extra == "dev"
|
26
28
|
Requires-Dist: pytest; extra == "dev"
|
@@ -9,7 +9,6 @@ import sys
|
|
9
9
|
import tarfile
|
10
10
|
import tempfile
|
11
11
|
import time
|
12
|
-
from typing import Any, Union
|
13
12
|
from urllib.parse import urlparse
|
14
13
|
import webbrowser
|
15
14
|
|
@@ -20,7 +19,9 @@ from pathlib import Path
|
|
20
19
|
from gitignore_parser import parse_gitignore
|
21
20
|
|
22
21
|
from artefacts.cli import init_job, generate_scenarios, AuthenticationError, __version__
|
22
|
+
from artefacts.cli import app_containers as containers
|
23
23
|
from artefacts.cli.constants import DEPRECATED_FRAMEWORKS, SUPPORTED_FRAMEWORKS
|
24
|
+
from artefacts.cli.utils import read_config, config_validation
|
24
25
|
import artefacts_copava as copava
|
25
26
|
|
26
27
|
HOME = os.path.expanduser("~")
|
@@ -100,55 +101,10 @@ class APIConf:
|
|
100
101
|
click.echo(f"Connecting to {self.api_url} using {auth_type}")
|
101
102
|
|
102
103
|
|
103
|
-
def read_config(filename: str) -> dict:
|
104
|
-
try:
|
105
|
-
with open(filename) as f:
|
106
|
-
return copava.parse(f.read())
|
107
|
-
except FileNotFoundError:
|
108
|
-
raise click.ClickException(f"Project config file {filename} not found.")
|
109
|
-
|
110
|
-
|
111
104
|
def validate_artefacts_config(config_file: str) -> dict:
|
112
105
|
pass
|
113
106
|
|
114
107
|
|
115
|
-
def pretty_print_config_error(
|
116
|
-
errors: Union[str, list, dict], indent: int = 0, prefix: str = "", suffix: str = ""
|
117
|
-
) -> str:
|
118
|
-
if type(errors) is str:
|
119
|
-
header = " " * indent
|
120
|
-
output = header + prefix + errors + suffix
|
121
|
-
elif type(errors) is list:
|
122
|
-
_depth = indent + 1
|
123
|
-
output = []
|
124
|
-
for value in errors:
|
125
|
-
output.append(pretty_print_config_error(value, indent=_depth, prefix="- "))
|
126
|
-
output = os.linesep.join(output)
|
127
|
-
elif type(errors) is dict:
|
128
|
-
_depth = indent + 1
|
129
|
-
output = []
|
130
|
-
for key, value in errors.items():
|
131
|
-
output.append(pretty_print_config_error(key, indent=indent, suffix=":"))
|
132
|
-
output.append(pretty_print_config_error(value, indent=_depth))
|
133
|
-
output = os.linesep.join(output)
|
134
|
-
else:
|
135
|
-
# Must not happen, so broad definition, but we want to know fast.
|
136
|
-
raise Exception(f"Unacceptable data type for config error formatting: {errors}")
|
137
|
-
return output
|
138
|
-
|
139
|
-
|
140
|
-
# Click callback syntax
|
141
|
-
def config_validation(context: dict, param: str, value: str) -> str:
|
142
|
-
if context.params["skip_validation"]:
|
143
|
-
return value
|
144
|
-
config = read_config(value)
|
145
|
-
errors = copava.check(config)
|
146
|
-
if len(errors) == 0:
|
147
|
-
return value
|
148
|
-
else:
|
149
|
-
raise click.BadParameter(pretty_print_config_error(errors))
|
150
|
-
|
151
|
-
|
152
108
|
@click.group()
|
153
109
|
def config():
|
154
110
|
return
|
@@ -275,11 +231,64 @@ def hello(project_name):
|
|
275
231
|
is_eager=True, # Necessary for callbacks to see it.
|
276
232
|
help="Skip configuration validation, so that unsupported settings can be tried out, e.g. non-ROS settings or simulators like SAPIEN.",
|
277
233
|
)
|
234
|
+
@click.option(
|
235
|
+
"--in-container",
|
236
|
+
is_flag=True,
|
237
|
+
default=False,
|
238
|
+
help='[Experimental] Run the job inside a package container. The container image is build if it does not exist yet, with default name as "artefacts" (please use --with-image to override the image name). This option overrides (for now) --dryrun, --nosim, --noisolation and --description.',
|
239
|
+
)
|
240
|
+
@click.option(
|
241
|
+
"--with-image",
|
242
|
+
default="artefacts",
|
243
|
+
help="[Experimental] Run the job using the image name passed here. Only used when running with --in-container set.",
|
244
|
+
)
|
245
|
+
@click.option(
|
246
|
+
"--rebuild-container",
|
247
|
+
is_flag=True,
|
248
|
+
default=False,
|
249
|
+
help="[Experimental] Rebuild the container image before running. This flag guarantees that the run uses the latest code available when building the image, and usually takes more time before the run can start (time to compile and generate the image).",
|
250
|
+
)
|
251
|
+
@click.option(
|
252
|
+
"--with-gui",
|
253
|
+
is_flag=True,
|
254
|
+
default=False,
|
255
|
+
help="Show any GUI if any is created by the test runs. By default, UI elements are run but hidden---only test logs are returned. Please note GUI often assume X11 (e.g. ROS), typically with Qt, so this may not work without a appropriate environment.",
|
256
|
+
)
|
278
257
|
@click.argument("jobname")
|
258
|
+
@click.pass_context
|
279
259
|
def run(
|
280
|
-
|
260
|
+
ctx: click.Context,
|
261
|
+
config,
|
262
|
+
jobname,
|
263
|
+
dryrun,
|
264
|
+
nosim,
|
265
|
+
noisolation,
|
266
|
+
description="",
|
267
|
+
skip_validation=False,
|
268
|
+
in_container: bool = False,
|
269
|
+
with_image: str = "artefacts",
|
270
|
+
rebuild_container: bool = False,
|
271
|
+
with_gui: bool = False,
|
281
272
|
):
|
282
|
-
"""
|
273
|
+
"""
|
274
|
+
Run JOBNAME locally
|
275
|
+
|
276
|
+
* Directly in the shell by default.
|
277
|
+
* Inside a packaged container when using the --in-container option.
|
278
|
+
"""
|
279
|
+
if in_container:
|
280
|
+
if rebuild_container or not ctx.invoke(containers.check, name=with_image):
|
281
|
+
ctx.invoke(
|
282
|
+
containers.build, path=".", dockerfile="Dockerfile", name=with_image
|
283
|
+
)
|
284
|
+
return ctx.invoke(
|
285
|
+
containers.run,
|
286
|
+
image=with_image,
|
287
|
+
jobname=jobname,
|
288
|
+
config=config,
|
289
|
+
with_gui=with_gui,
|
290
|
+
)
|
291
|
+
|
283
292
|
warpconfig = read_config(config)
|
284
293
|
|
285
294
|
project_id = warpconfig["project"]
|
@@ -334,7 +343,7 @@ def run(
|
|
334
343
|
)
|
335
344
|
except AuthenticationError:
|
336
345
|
raise click.ClickException(
|
337
|
-
"Unable to authenticate, check your
|
346
|
+
"Unable to authenticate (Stage: Job initialisation), please check your project name and API key"
|
338
347
|
)
|
339
348
|
|
340
349
|
job_success = True
|
@@ -346,7 +355,7 @@ def run(
|
|
346
355
|
run = warpjob.new_run(scenario)
|
347
356
|
except AuthenticationError:
|
348
357
|
raise click.ClickException(
|
349
|
-
"Unable to authenticate, check your
|
358
|
+
"Unable to authenticate (Stage: Job run), please check your project name and API key"
|
350
359
|
)
|
351
360
|
if framework is not None and framework.startswith("ros2:"):
|
352
361
|
from artefacts.cli.ros2 import run_ros2_tests
|
@@ -474,7 +483,7 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
474
483
|
click.echo(f"Packaging source...")
|
475
484
|
|
476
485
|
with tempfile.NamedTemporaryFile(
|
477
|
-
prefix=project_id, suffix=".tgz", delete=True
|
486
|
+
prefix=project_id.split("/")[-1], suffix=".tgz", delete=True
|
478
487
|
) as temp_file:
|
479
488
|
# get list of patterns to be ignored in .artefactsignore
|
480
489
|
ignore_file = Path(project_folder) / Path(".artefactsignore")
|
@@ -512,7 +521,13 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
512
521
|
)
|
513
522
|
|
514
523
|
if not upload_urls_response.ok:
|
515
|
-
|
524
|
+
try:
|
525
|
+
result = upload_urls_response.json()
|
526
|
+
except requests.exceptions.JSONDecodeError:
|
527
|
+
raise click.ClickException(
|
528
|
+
f"Apologies, problem in interacting with the Artefacts backend: {upload_urls_response.status_code} {upload_urls_response.reason}. Response text: {upload_urls_response.text}."
|
529
|
+
)
|
530
|
+
|
516
531
|
if (
|
517
532
|
upload_urls_response.status_code == 403
|
518
533
|
and result["message"] == "Not allowed"
|
@@ -611,6 +626,7 @@ artefacts.add_command(config)
|
|
611
626
|
artefacts.add_command(hello)
|
612
627
|
artefacts.add_command(run)
|
613
628
|
artefacts.add_command(run_remote)
|
629
|
+
artefacts.add_command(containers.containers)
|
614
630
|
|
615
631
|
|
616
632
|
if __name__ == "__main__":
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from artefacts.cli.constants import DEFAULT_API_URL
|
6
|
+
from artefacts.cli.utils import config_validation, read_config
|
7
|
+
from artefacts.cli.containers.utils import ContainerMgr
|
8
|
+
|
9
|
+
|
10
|
+
@click.group()
|
11
|
+
@click.option("--debug/--no-debug", default=False)
|
12
|
+
@click.pass_context
|
13
|
+
def containers(ctx: click.Context, debug: bool):
|
14
|
+
ctx.ensure_object(dict)
|
15
|
+
ctx.obj["debug"] = debug
|
16
|
+
|
17
|
+
|
18
|
+
@containers.command()
|
19
|
+
@click.option(
|
20
|
+
"--path",
|
21
|
+
default=".",
|
22
|
+
help="Path to the root of the project, where a Dockerfile is available.",
|
23
|
+
)
|
24
|
+
@click.option(
|
25
|
+
"--dockerfile",
|
26
|
+
default="Dockerfile",
|
27
|
+
help="File name of the container definition file. Defaults to the standard Dockerfile inside the project root (see --path)",
|
28
|
+
)
|
29
|
+
@click.option(
|
30
|
+
"--name",
|
31
|
+
required=False,
|
32
|
+
help="Name for the generated container",
|
33
|
+
)
|
34
|
+
@click.pass_context
|
35
|
+
def build(ctx: click.Context, path: str, dockerfile: str, name: str):
|
36
|
+
if not os.path.exists(os.path.join(path, dockerfile)):
|
37
|
+
raise click.ClickException(
|
38
|
+
f"No {dockerfile} found here. I cannot build the container."
|
39
|
+
)
|
40
|
+
if name is None:
|
41
|
+
name = "artefacts"
|
42
|
+
handler = ContainerMgr()
|
43
|
+
image, _ = handler.build(path=path, name=name)
|
44
|
+
print(f"Package complete in image: {image}")
|
45
|
+
|
46
|
+
|
47
|
+
@containers.command()
|
48
|
+
@click.argument("name")
|
49
|
+
@click.pass_context
|
50
|
+
def check(ctx: click.Context, name: str):
|
51
|
+
if name is None:
|
52
|
+
name = "artefacts"
|
53
|
+
handler = ContainerMgr()
|
54
|
+
result = handler.check(name)
|
55
|
+
if ctx.parent is None:
|
56
|
+
# Print only if the command is called directly.
|
57
|
+
print(f"Package {name} exists and ready to use.")
|
58
|
+
return result
|
59
|
+
|
60
|
+
|
61
|
+
@containers.command()
|
62
|
+
@click.argument("image")
|
63
|
+
@click.argument("jobname")
|
64
|
+
@click.option(
|
65
|
+
"--config",
|
66
|
+
callback=config_validation,
|
67
|
+
default="artefacts.yaml",
|
68
|
+
help="Artefacts config file.",
|
69
|
+
)
|
70
|
+
@click.option(
|
71
|
+
"--with-gui",
|
72
|
+
"with_gui",
|
73
|
+
default=False,
|
74
|
+
help="Show any GUI if any is created by the test runs. By default, UI elements are run but hidden---only test logs are returned. Please note GUI often assume an X11 environment, typically with Qt, so this may not work without a appropriate environment.",
|
75
|
+
)
|
76
|
+
@click.pass_context
|
77
|
+
def run(ctx: click.Context, image: str, jobname: str, config: str, with_gui: bool):
|
78
|
+
try:
|
79
|
+
artefacts_config = read_config(config)
|
80
|
+
except FileNotFoundError:
|
81
|
+
raise click.ClickException(f"Project config file not found: {config}")
|
82
|
+
project = artefacts_config["project"]
|
83
|
+
handler = ContainerMgr()
|
84
|
+
params = dict(
|
85
|
+
image=image,
|
86
|
+
project=project,
|
87
|
+
jobname=jobname,
|
88
|
+
# Hidden setting primarily useful to Artefacts developers
|
89
|
+
api_url=os.environ.get("ARTEFACTS_API_URL", DEFAULT_API_URL),
|
90
|
+
with_gui=with_gui,
|
91
|
+
)
|
92
|
+
container, logs = handler.run(**params)
|
93
|
+
if container:
|
94
|
+
print(f"Package run complete: Container Id for inspection: {container['Id']}")
|
95
|
+
else:
|
96
|
+
print(f"Package run failed:")
|
97
|
+
for entry in logs:
|
98
|
+
print("\t- " + entry)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
from collections.abc import Generator
|
2
|
+
import configparser
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Tuple, Union
|
6
|
+
|
7
|
+
from artefacts.cli.constants import DEFAULT_API_URL
|
8
|
+
|
9
|
+
|
10
|
+
class CMgr:
|
11
|
+
def build(self, **kwargs) -> Tuple[str, Generator]:
|
12
|
+
"""
|
13
|
+
Returns the build image ID (e.g. sha256:abcdefghi)
|
14
|
+
and an iterator over the build log entries.
|
15
|
+
"""
|
16
|
+
raise NotImplemented()
|
17
|
+
|
18
|
+
def check(self, image: str) -> bool:
|
19
|
+
"""
|
20
|
+
Checks whether a target image exists locally.
|
21
|
+
"""
|
22
|
+
raise NotImplemented()
|
23
|
+
|
24
|
+
def run(
|
25
|
+
self,
|
26
|
+
image: str,
|
27
|
+
project: str,
|
28
|
+
jobname: str = None,
|
29
|
+
artefacts_dir: str = Path("~/.artefacts").expanduser(),
|
30
|
+
api_url: str = DEFAULT_API_URL,
|
31
|
+
with_gui: bool = False,
|
32
|
+
) -> Tuple[Any, Generator]:
|
33
|
+
"""
|
34
|
+
Returns a container (Any type as depends on the framework)
|
35
|
+
and an iterator over the container log entries.
|
36
|
+
"""
|
37
|
+
raise NotImplemented()
|
38
|
+
|
39
|
+
def _valid_artefacts_api_key(
|
40
|
+
self, project: str, path: Union[str, Path] = Path("~/.artefacts").expanduser()
|
41
|
+
) -> bool:
|
42
|
+
"""
|
43
|
+
Check if a valid API key is available to embed in containers.
|
44
|
+
|
45
|
+
1. Check overrides with the ARTEFACTS_KEY environment variable.
|
46
|
+
2. If `path` is not given, check the default .artefacts folder for the config file.
|
47
|
+
3. If `path` is given, check the file directly is a file, or check for a `config` file if a folder.
|
48
|
+
|
49
|
+
When a config file is found, we check here if the API key for the `project` is available.
|
50
|
+
|
51
|
+
`path` set to None is an error, and aborts execution.
|
52
|
+
"""
|
53
|
+
if not path:
|
54
|
+
raise Exception(
|
55
|
+
"`path` must be a string, a Path object, or excluded from the kwargs"
|
56
|
+
)
|
57
|
+
if os.environ.get("ARTEFACTS_KEY", None):
|
58
|
+
return True
|
59
|
+
path = Path(path) # Ensure we have a Path object
|
60
|
+
config = configparser.ConfigParser()
|
61
|
+
if path.is_dir():
|
62
|
+
config.read(path / "config")
|
63
|
+
else:
|
64
|
+
config.read(path)
|
65
|
+
try:
|
66
|
+
return config[project].get("apikey") != None
|
67
|
+
except KeyError:
|
68
|
+
return False
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from collections.abc import Generator
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
import platform
|
6
|
+
from typing import Any, Tuple
|
7
|
+
from uuid import uuid4
|
8
|
+
|
9
|
+
from artefacts.cli.constants import DEFAULT_API_URL
|
10
|
+
from artefacts.cli.containers import CMgr
|
11
|
+
from artefacts.cli.utils import ensure_available
|
12
|
+
|
13
|
+
ensure_available("docker")
|
14
|
+
|
15
|
+
import docker
|
16
|
+
from docker import APIClient
|
17
|
+
|
18
|
+
|
19
|
+
class DockerManager(CMgr):
|
20
|
+
def __init__(self):
|
21
|
+
self.client = APIClient()
|
22
|
+
|
23
|
+
def build(self, **kwargs) -> Tuple[str, Generator]:
|
24
|
+
kwargs["tag"] = kwargs.pop("name")
|
25
|
+
logs = []
|
26
|
+
img_id = None
|
27
|
+
for entry in self.client.build(**kwargs):
|
28
|
+
line_data = [
|
29
|
+
json.loads(v) for v in entry.decode("utf-8").split("\r\n") if len(v) > 0
|
30
|
+
]
|
31
|
+
for data in line_data:
|
32
|
+
if "stream" in data:
|
33
|
+
line = data["stream"].strip()
|
34
|
+
if not line.startswith("---") and len(line) > 0:
|
35
|
+
print(line)
|
36
|
+
logs.append(line)
|
37
|
+
elif "aux" in data and "ID" in data["aux"]:
|
38
|
+
img_id = data["aux"]["ID"]
|
39
|
+
if img_id is None:
|
40
|
+
img_id = self.client.inspect_image(kwargs["tag"])["Id"]
|
41
|
+
return img_id, iter(logs)
|
42
|
+
|
43
|
+
def check(
|
44
|
+
self,
|
45
|
+
image: str,
|
46
|
+
) -> bool:
|
47
|
+
return len(self.client.images(name=image)) > 0
|
48
|
+
|
49
|
+
def run(
|
50
|
+
self,
|
51
|
+
image: str,
|
52
|
+
project: str,
|
53
|
+
jobname: str = None,
|
54
|
+
artefacts_dir: str = Path("~/.artefacts").expanduser(),
|
55
|
+
api_url: str = DEFAULT_API_URL,
|
56
|
+
with_gui: bool = False,
|
57
|
+
) -> Tuple[Any, Generator]:
|
58
|
+
if not self._valid_artefacts_api_key(project, artefacts_dir):
|
59
|
+
return None, iter(
|
60
|
+
[
|
61
|
+
"Missing API key for the project. Does `~/.artefacts/config` exist and contain your key?"
|
62
|
+
]
|
63
|
+
)
|
64
|
+
try:
|
65
|
+
env = {
|
66
|
+
"JOB_ID": str(uuid4()),
|
67
|
+
"ARTEFACTS_JOB_NAME": jobname,
|
68
|
+
"ARTEFACTS_API_URL": api_url,
|
69
|
+
}
|
70
|
+
|
71
|
+
if platform.system() in ["Darwin", "Windows"]:
|
72
|
+
# Assume we run in Docker Desktop
|
73
|
+
env["DISPLAY"] = "host.docker.internal:0"
|
74
|
+
else:
|
75
|
+
env["DISPLAY"] = os.environ.get("DISPLAY", ":0")
|
76
|
+
|
77
|
+
if not with_gui:
|
78
|
+
env["QT_QPA_PLATFORM"] = "offscreen"
|
79
|
+
|
80
|
+
container = self.client.create_container(
|
81
|
+
image,
|
82
|
+
environment=env,
|
83
|
+
detach=False,
|
84
|
+
volumes=["/root/.artefacts"],
|
85
|
+
host_config=self.client.create_host_config(
|
86
|
+
binds={
|
87
|
+
artefacts_dir: {
|
88
|
+
"bind": "/root/.artefacts",
|
89
|
+
"mode": "ro",
|
90
|
+
},
|
91
|
+
},
|
92
|
+
network_mode="host",
|
93
|
+
),
|
94
|
+
)
|
95
|
+
self.client.start(container=container.get("Id"))
|
96
|
+
for entry in self.client.logs(container=container.get("Id"), stream=True):
|
97
|
+
print(entry.decode("utf-8").strip())
|
98
|
+
return container, iter([])
|
99
|
+
except docker.errors.ImageNotFound:
|
100
|
+
return None, iter(
|
101
|
+
[f"Image {image} not found by Docker. Perhaps need to build first?"]
|
102
|
+
)
|
103
|
+
except Exception as e:
|
104
|
+
return None, iter([f"Failed to run from {image}. All we know: {e}"])
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from collections.abc import Generator
|
2
|
+
import logging
|
3
|
+
from typing import Any, Tuple
|
4
|
+
|
5
|
+
from artefacts.cli.containers import CMgr
|
6
|
+
|
7
|
+
|
8
|
+
class ContainerMgr:
|
9
|
+
SUPPORTED_PRIORITISED_ENGINES = {
|
10
|
+
1: "docker",
|
11
|
+
# 2: "podman",
|
12
|
+
}
|
13
|
+
|
14
|
+
def __init__(self):
|
15
|
+
self.logger = logging.getLogger(__name__)
|
16
|
+
self.mgr = self._configure()
|
17
|
+
if self.mgr is None:
|
18
|
+
raise Exception(
|
19
|
+
f"Failed to find supported container stack. Please install and start one in {list(self.SUPPORTED_PRIORITISED_ENGINES.values())}, with default settings (custom sockets not supported at this stage)"
|
20
|
+
)
|
21
|
+
|
22
|
+
def _configure(self) -> CMgr:
|
23
|
+
manager = None
|
24
|
+
for idx in sorted(self.SUPPORTED_PRIORITISED_ENGINES):
|
25
|
+
engine = self.SUPPORTED_PRIORITISED_ENGINES[idx]
|
26
|
+
try:
|
27
|
+
handler = getattr(self, f"_configure_{engine}")
|
28
|
+
manager = handler()
|
29
|
+
except AttributeError:
|
30
|
+
self.logger.warning(
|
31
|
+
f"Tried to detect an unsupported engine: {engine}. WIP? Ignore and continue."
|
32
|
+
)
|
33
|
+
except Exception as e:
|
34
|
+
self.logger.warning(
|
35
|
+
f"Problem in detecting {engine} ({e}) Ignore and continue."
|
36
|
+
)
|
37
|
+
if manager is not None:
|
38
|
+
break
|
39
|
+
return manager
|
40
|
+
|
41
|
+
def _configure_docker(self):
|
42
|
+
from artefacts.cli.containers.docker import DockerManager
|
43
|
+
|
44
|
+
return DockerManager()
|
45
|
+
|
46
|
+
# def _configure_podman(self):
|
47
|
+
# from artefacts.cli.containers import podman
|
48
|
+
# return PodmanManager()
|
49
|
+
|
50
|
+
def build(self, **kwargs) -> Tuple[str, Generator]:
|
51
|
+
return self.mgr.build(**kwargs)
|
52
|
+
|
53
|
+
def check(self, image: str) -> bool:
|
54
|
+
return self.mgr.check(image)
|
55
|
+
|
56
|
+
def run(self, **kwargs) -> Tuple[Any, Generator]:
|
57
|
+
return self.mgr.run(**kwargs)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import sys
|
4
|
+
from typing import Any, Union
|
5
|
+
|
6
|
+
import click
|
7
|
+
|
8
|
+
import artefacts_copava as copava
|
9
|
+
|
10
|
+
|
11
|
+
def run_and_save_logs(
|
12
|
+
args, output_path, shell=False, executable=None, env=None, cwd=None
|
13
|
+
):
|
14
|
+
"""
|
15
|
+
Run a command and save stdout and stderr to a file in output_path
|
16
|
+
|
17
|
+
Note: explicitly list used named params instead of using **kwargs to avoid typing issue: https://github.com/microsoft/pyright/issues/455#issuecomment-780076232
|
18
|
+
"""
|
19
|
+
output_file = open(output_path, "wb")
|
20
|
+
proc = subprocess.Popen(
|
21
|
+
args,
|
22
|
+
stdout=subprocess.PIPE, # Capture stdout
|
23
|
+
stderr=subprocess.PIPE, # Capture stderr
|
24
|
+
shell=shell,
|
25
|
+
executable=executable,
|
26
|
+
env=env,
|
27
|
+
cwd=cwd,
|
28
|
+
)
|
29
|
+
# write test-process stdout and stderr into file and stdout
|
30
|
+
if proc.stdout:
|
31
|
+
for line in proc.stdout:
|
32
|
+
decoded_line = line.decode()
|
33
|
+
sys.stdout.write(decoded_line)
|
34
|
+
output_file.write(line)
|
35
|
+
if proc.stderr:
|
36
|
+
for line in proc.stderr:
|
37
|
+
decoded_line = line.decode()
|
38
|
+
sys.stderr.write(decoded_line)
|
39
|
+
output_file.write(line)
|
40
|
+
proc.wait()
|
41
|
+
return proc.returncode
|
42
|
+
|
43
|
+
|
44
|
+
def ensure_available(package: str) -> None:
|
45
|
+
import importlib
|
46
|
+
|
47
|
+
try:
|
48
|
+
importlib.import_module(package)
|
49
|
+
except ImportError:
|
50
|
+
"""
|
51
|
+
Recommended by the Python community
|
52
|
+
https://pip.pypa.io/en/latest/user_guide/#using-pip-from-your-program
|
53
|
+
"""
|
54
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
55
|
+
|
56
|
+
|
57
|
+
def read_config(filename: str) -> dict:
|
58
|
+
try:
|
59
|
+
with open(filename) as f:
|
60
|
+
return copava.parse(f.read())
|
61
|
+
except FileNotFoundError:
|
62
|
+
raise click.ClickException(f"Project config file {filename} not found.")
|
63
|
+
|
64
|
+
|
65
|
+
# Click callback syntax
|
66
|
+
def config_validation(context: dict, param: str, value: str) -> str:
|
67
|
+
if context.params.get("skip_validation", False):
|
68
|
+
return value
|
69
|
+
config = read_config(value)
|
70
|
+
errors = copava.check(config)
|
71
|
+
if len(errors) == 0:
|
72
|
+
return value
|
73
|
+
else:
|
74
|
+
raise click.BadParameter(pretty_print_config_error(errors))
|
75
|
+
|
76
|
+
|
77
|
+
def pretty_print_config_error(
|
78
|
+
errors: Union[str, list, dict], indent: int = 0, prefix: str = "", suffix: str = ""
|
79
|
+
) -> str:
|
80
|
+
if type(errors) is str:
|
81
|
+
header = " " * indent
|
82
|
+
output = header + prefix + errors + suffix
|
83
|
+
elif type(errors) is list:
|
84
|
+
_depth = indent + 1
|
85
|
+
output = []
|
86
|
+
for value in errors:
|
87
|
+
output.append(pretty_print_config_error(value, indent=_depth, prefix="- "))
|
88
|
+
output = os.linesep.join(output)
|
89
|
+
elif type(errors) is dict:
|
90
|
+
_depth = indent + 1
|
91
|
+
output = []
|
92
|
+
for key, value in errors.items():
|
93
|
+
output.append(pretty_print_config_error(key, indent=indent, suffix=":"))
|
94
|
+
output.append(pretty_print_config_error(value, indent=_depth))
|
95
|
+
output = os.linesep.join(output)
|
96
|
+
else:
|
97
|
+
# Must not happen, so broad definition, but we want to know fast.
|
98
|
+
raise Exception(f"Unacceptable data type for config error formatting: {errors}")
|
99
|
+
return output
|
@@ -1,6 +1,6 @@
|
|
1
1
|
version: 0.1.0
|
2
2
|
|
3
|
-
project:
|
3
|
+
project: artefacts-tests/cli-run-remote-testing
|
4
4
|
|
5
5
|
on:
|
6
6
|
push:
|
@@ -23,7 +23,7 @@ jobs:
|
|
23
23
|
timeout: 5 # minutes
|
24
24
|
scenarios:
|
25
25
|
defaults: # Global to all scenarios, and overriden in specific scenarios.
|
26
|
-
output_dirs:
|
26
|
+
output_dirs:
|
27
27
|
- /tmp/outputs/
|
28
28
|
subscriptions:
|
29
29
|
pose: turtle1/pose
|
@@ -55,4 +55,4 @@ jobs:
|
|
55
55
|
pose: turtle1/pose
|
56
56
|
settings:
|
57
57
|
- name: turtle
|
58
|
-
ros_testfile: infra-tests/turtlesim2/launch_turtle.py
|
58
|
+
ros_testfile: infra-tests/turtlesim2/launch_turtle.py
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.17
|
4
4
|
Author-email: FD <fabian@artefacts.com>, AGC <alejandro@artefacts.com>, TN <tomo@artefacts.com>, EP <eric@artefacts.com>
|
5
5
|
Project-URL: Homepage, https://github.com/art-e-fact/artefacts-client
|
6
6
|
Project-URL: Bug Tracker, https://github.com/art-e-fact/artefacts-client/issues
|
@@ -18,9 +18,11 @@ Requires-Dist: mcap-ros2-support
|
|
18
18
|
Requires-Dist: PyYAML>=6.0
|
19
19
|
Requires-Dist: requests>=2.27.1
|
20
20
|
Requires-Dist: setuptools-scm
|
21
|
+
Requires-Dist: setuptools>=74
|
21
22
|
Provides-Extra: dev
|
22
23
|
Requires-Dist: awscli; extra == "dev"
|
23
24
|
Requires-Dist: build; extra == "dev"
|
25
|
+
Requires-Dist: docker; extra == "dev"
|
24
26
|
Requires-Dist: lark; extra == "dev"
|
25
27
|
Requires-Dist: pyre-check; extra == "dev"
|
26
28
|
Requires-Dist: pytest; extra == "dev"
|
@@ -5,6 +5,7 @@ pyproject.toml
|
|
5
5
|
pytest.ini
|
6
6
|
artefacts/cli/__init__.py
|
7
7
|
artefacts/cli/app.py
|
8
|
+
artefacts/cli/app_containers.py
|
8
9
|
artefacts/cli/bagparser.py
|
9
10
|
artefacts/cli/constants.py
|
10
11
|
artefacts/cli/other.py
|
@@ -14,6 +15,9 @@ artefacts/cli/ros2.py
|
|
14
15
|
artefacts/cli/utils.py
|
15
16
|
artefacts/cli/utils_ros.py
|
16
17
|
artefacts/cli/version.py
|
18
|
+
artefacts/cli/containers/__init__.py
|
19
|
+
artefacts/cli/containers/docker.py
|
20
|
+
artefacts/cli/containers/utils.py
|
17
21
|
artefacts/wrappers/artefacts_ros1_meta.launch
|
18
22
|
artefacts_cli.egg-info/PKG-INFO
|
19
23
|
artefacts_cli.egg-info/SOURCES.txt
|
@@ -40,6 +44,7 @@ tests/__init__.py
|
|
40
44
|
tests/conftest.py
|
41
45
|
tests/test_config_validation.py
|
42
46
|
tests/cli/__init__.py
|
47
|
+
tests/cli/test_app_containers.py
|
43
48
|
tests/cli/test_cli.py
|
44
49
|
tests/cli/test_config_validation.py
|
45
50
|
tests/cli/test_other.py
|
@@ -50,4 +55,5 @@ tests/cli/test_warp.py
|
|
50
55
|
tests/fixtures/artefacts_deprecated.yaml
|
51
56
|
tests/fixtures/artefacts_ros1.yaml
|
52
57
|
tests/fixtures/warp-env-param.yaml
|
53
|
-
tests/fixtures/warp.yaml
|
58
|
+
tests/fixtures/warp.yaml
|
59
|
+
tests/utils/docker_mock.py
|
@@ -28,6 +28,7 @@ dependencies = [
|
|
28
28
|
"PyYAML>=6.0",
|
29
29
|
"requests>=2.27.1",
|
30
30
|
"setuptools-scm",
|
31
|
+
"setuptools>=74", # setuptools-scm requires without version, but does not work with oldish versions, so let's ensure a recent, tested one.
|
31
32
|
]
|
32
33
|
dynamic = ["version"]
|
33
34
|
|
@@ -42,6 +43,7 @@ local_scheme = "no-local-version"
|
|
42
43
|
dev = [
|
43
44
|
"awscli",
|
44
45
|
"build",
|
46
|
+
"docker",
|
45
47
|
"lark",
|
46
48
|
"pyre-check",
|
47
49
|
"pytest",
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import os
|
2
|
+
from uuid import uuid4
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from artefacts.cli.app_containers import containers
|
7
|
+
|
8
|
+
|
9
|
+
def test_container_package_exists(cli_runner):
|
10
|
+
result = cli_runner.invoke(containers, [])
|
11
|
+
assert result.exit_code == 0
|
12
|
+
|
13
|
+
|
14
|
+
def test_container_package_build_specific_dockerfile(
|
15
|
+
cli_runner, dockerfile_available, docker_mocker
|
16
|
+
):
|
17
|
+
dockerfile = "non_standard_dockerfile"
|
18
|
+
result = cli_runner.invoke(containers, ["build", "--dockerfile", dockerfile])
|
19
|
+
dockerfile_available.assert_any_call(os.path.join(".", dockerfile))
|
20
|
+
assert result.exit_code == 0
|
21
|
+
|
22
|
+
|
23
|
+
def test_container_package_build_specific_dockerfile_missing(
|
24
|
+
cli_runner, dockerfile_not_available
|
25
|
+
):
|
26
|
+
dockerfile = "non_standard_dockerfile"
|
27
|
+
result = cli_runner.invoke(containers, ["build", "--dockerfile", dockerfile])
|
28
|
+
dockerfile_not_available.assert_any_call(os.path.join(".", dockerfile))
|
29
|
+
assert result.exit_code == 1
|
30
|
+
assert (
|
31
|
+
result.output.strip()
|
32
|
+
== f"Error: No {dockerfile} found here. I cannot build the container."
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
def test_container_package_build_specific_image_name(
|
37
|
+
cli_runner, dockerfile_available, docker_mocker
|
38
|
+
):
|
39
|
+
name = str(uuid4())
|
40
|
+
before = len(docker_mocker.images())
|
41
|
+
result = cli_runner.invoke(containers, ["build", "--name", name])
|
42
|
+
assert result.exit_code == 0
|
43
|
+
assert len(docker_mocker.images()) == before + 1
|
44
|
+
assert docker_mocker.get_image(name).Repository == name
|
45
|
+
|
46
|
+
|
47
|
+
def test_container_package_build_default_image_name(
|
48
|
+
cli_runner, dockerfile_available, docker_mocker
|
49
|
+
):
|
50
|
+
before = len(docker_mocker.images())
|
51
|
+
result = cli_runner.invoke(containers, ["build"])
|
52
|
+
assert result.exit_code == 0
|
53
|
+
assert len(docker_mocker.images()) == before + 1
|
54
|
+
assert docker_mocker.get_image("artefacts") is not None
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
import os
|
4
|
+
|
5
|
+
from click.testing import CliRunner
|
6
|
+
import docker
|
7
|
+
|
8
|
+
from tests.utils import docker_mock
|
9
|
+
|
10
|
+
|
11
|
+
def dockerfile_presence(mocker, value: bool):
|
12
|
+
original = os.path.exists
|
13
|
+
|
14
|
+
def exists(path):
|
15
|
+
if "dockerfile" in path.lower():
|
16
|
+
return value
|
17
|
+
else:
|
18
|
+
return original(path)
|
19
|
+
|
20
|
+
return mocker.patch("os.path.exists", side_effect=exists, autospec=True)
|
21
|
+
|
22
|
+
|
23
|
+
@pytest.fixture(scope="function")
|
24
|
+
def dockerfile_available(mocker):
|
25
|
+
return dockerfile_presence(mocker, True)
|
26
|
+
|
27
|
+
|
28
|
+
@pytest.fixture(scope="function")
|
29
|
+
def dockerfile_not_available(mocker):
|
30
|
+
return dockerfile_presence(mocker, False)
|
31
|
+
|
32
|
+
|
33
|
+
@pytest.fixture(scope="module")
|
34
|
+
def docker_mocker(module_mocker):
|
35
|
+
test_client = docker_mock.make_fake_api_client()
|
36
|
+
module_mocker.patch("docker.APIClient", return_value=test_client)
|
37
|
+
return test_client
|
38
|
+
|
39
|
+
|
40
|
+
@pytest.fixture(scope="module")
|
41
|
+
def cli_runner():
|
42
|
+
return CliRunner()
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# Fake Docker client
|
2
|
+
|
3
|
+
import copy
|
4
|
+
from dataclasses import dataclass, asdict
|
5
|
+
import hashlib
|
6
|
+
from random import randint
|
7
|
+
from unittest import mock
|
8
|
+
|
9
|
+
from docker import DockerClient, APIClient
|
10
|
+
from docker.constants import DEFAULT_DOCKER_API_VERSION
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class SimpleImage:
|
15
|
+
Id: str
|
16
|
+
Created: str
|
17
|
+
Repository: str
|
18
|
+
RepoTags: list
|
19
|
+
|
20
|
+
|
21
|
+
class TestDockerAPIClient(APIClient):
|
22
|
+
def __init__(self, *args, **kwargs):
|
23
|
+
super().__init__(*args, **kwargs)
|
24
|
+
self.fake_server_db = {}
|
25
|
+
self.fake_image_id_base = hashlib.new("sha256")
|
26
|
+
|
27
|
+
def build(self, *args, **kwargs):
|
28
|
+
if kwargs["tag"] in self.fake_server_db:
|
29
|
+
original = self.fake_server_db[kwargs["tag"]]
|
30
|
+
for img in original:
|
31
|
+
# Mimick what Docker seems to be doing
|
32
|
+
img.Repository = ""
|
33
|
+
img.RepoTags = []
|
34
|
+
else:
|
35
|
+
original = []
|
36
|
+
|
37
|
+
# Generate a random yet compliant image ID
|
38
|
+
# Note the use of update, to ensure calling many times will be on different input (so unique IDs).
|
39
|
+
self.fake_image_id_base.update(bytes(randint(0, 100)))
|
40
|
+
iid = "sha256:" + self.fake_image_id_base.hexdigest()
|
41
|
+
|
42
|
+
new_collection = [
|
43
|
+
SimpleImage(
|
44
|
+
**{
|
45
|
+
"Id": iid,
|
46
|
+
"Created": "1 min ago",
|
47
|
+
"Repository": kwargs["tag"],
|
48
|
+
"RepoTags": [kwargs["tag"] + ":latest"],
|
49
|
+
}
|
50
|
+
)
|
51
|
+
]
|
52
|
+
# Ensure the new entry is in the first place
|
53
|
+
new_collection.extend(original)
|
54
|
+
self.fake_server_db[kwargs["tag"]] = new_collection
|
55
|
+
|
56
|
+
return [b'"fake"', b'"test"', b'"logs"']
|
57
|
+
|
58
|
+
def images(self, *args, **kwargs):
|
59
|
+
if kwargs.get("name", None):
|
60
|
+
return self.fake_server_db.get(kwargs["name"])
|
61
|
+
else:
|
62
|
+
return [v for vs in self.fake_server_db.values() for v in vs]
|
63
|
+
|
64
|
+
def get_image(self, name):
|
65
|
+
# Index 0 is the latest
|
66
|
+
return self.fake_server_db.get(name)[0]
|
67
|
+
|
68
|
+
def inspect_image(self, name):
|
69
|
+
return asdict(self.get_image(name))
|
70
|
+
|
71
|
+
|
72
|
+
class TestDockerClient(DockerClient):
|
73
|
+
def __init__(self, *args, **kwargs):
|
74
|
+
super().__init__(*args, **kwargs)
|
75
|
+
self.api = TestDockerAPIClient(version=kwargs["version"])
|
76
|
+
|
77
|
+
|
78
|
+
def make_fake_api_client():
|
79
|
+
"""
|
80
|
+
Incomplete fake API client.
|
81
|
+
"""
|
82
|
+
return TestDockerAPIClient(version=DEFAULT_DOCKER_API_VERSION)
|
83
|
+
|
84
|
+
|
85
|
+
def make_fake_client():
|
86
|
+
"""
|
87
|
+
Test client with a fake API client.
|
88
|
+
"""
|
89
|
+
return TestDockerClient(version=DEFAULT_DOCKER_API_VERSION)
|
@@ -1,35 +0,0 @@
|
|
1
|
-
import subprocess
|
2
|
-
import sys
|
3
|
-
|
4
|
-
|
5
|
-
def run_and_save_logs(
|
6
|
-
args, output_path, shell=False, executable=None, env=None, cwd=None
|
7
|
-
):
|
8
|
-
"""
|
9
|
-
Run a command and save stdout and stderr to a file in output_path
|
10
|
-
|
11
|
-
Note: explicitly list used named params instead of using **kwargs to avoid typing issue: https://github.com/microsoft/pyright/issues/455#issuecomment-780076232
|
12
|
-
"""
|
13
|
-
output_file = open(output_path, "wb")
|
14
|
-
proc = subprocess.Popen(
|
15
|
-
args,
|
16
|
-
stdout=subprocess.PIPE, # Capture stdout
|
17
|
-
stderr=subprocess.PIPE, # Capture stderr
|
18
|
-
shell=shell,
|
19
|
-
executable=executable,
|
20
|
-
env=env,
|
21
|
-
cwd=cwd,
|
22
|
-
)
|
23
|
-
# write test-process stdout and stderr into file and stdout
|
24
|
-
if proc.stdout:
|
25
|
-
for line in proc.stdout:
|
26
|
-
decoded_line = line.decode()
|
27
|
-
sys.stdout.write(decoded_line)
|
28
|
-
output_file.write(line)
|
29
|
-
if proc.stderr:
|
30
|
-
for line in proc.stderr:
|
31
|
-
decoded_line = line.decode()
|
32
|
-
sys.stderr.write(decoded_line)
|
33
|
-
output_file.write(line)
|
34
|
-
proc.wait()
|
35
|
-
return proc.returncode
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|