gpustack-runtime 0.1.38.post4__tar.gz → 0.1.39__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.
Files changed (111) hide show
  1. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/PKG-INFO +6 -4
  2. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/README.md +1 -0
  3. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/_version.py +2 -2
  4. gpustack_runtime-0.1.39/gpustack_runtime/_version_appendix.py +1 -0
  5. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/deployer/__init__.py +24 -49
  6. gpustack_runtime-0.1.39/gpustack_runtime/deployer/__patches__.py +455 -0
  7. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/deployer/__types__.py +60 -27
  8. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/deployer/docker.py +115 -41
  9. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/deployer/kuberentes.py +23 -22
  10. gpustack_runtime-0.1.39/gpustack_runtime/deployer/podman.py +2114 -0
  11. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/amd.py +4 -13
  12. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/hygon.py +1 -1
  13. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/nvidia.py +1 -1
  14. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyhsa/__init__.py +7 -7
  15. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyrocmsmi/__init__.py +9 -3
  16. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/envs.py +217 -46
  17. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/pyproject.toml +5 -3
  18. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/ruff.toml +3 -2
  19. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/uv.lock +22 -6
  20. gpustack_runtime-0.1.38.post4/gpustack_runtime/_version_appendix.py +0 -1
  21. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.codespelldict +0 -0
  22. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.codespellrc +0 -0
  23. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.dockerignore +0 -0
  24. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.gitattributes +0 -0
  25. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.gitignore +0 -0
  26. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.pre-commit-config.yaml +0 -0
  27. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/.python-version +0 -0
  28. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/LICENSE +0 -0
  29. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/Makefile +0 -0
  30. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/deploy/manifests/docker-compose.yaml +0 -0
  31. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/deploy/manifests/kubernetes.yaml +0 -0
  32. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/docs/index.md +0 -0
  33. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/docs/modules/gpustack_runtime.deployer.md +0 -0
  34. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/docs/modules/gpustack_runtime.detector.md +0 -0
  35. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/docs/modules/gpustack_runtime.md +0 -0
  36. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/__init__.py +0 -0
  37. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/__main__.py +0 -0
  38. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/_version.pyi +0 -0
  39. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/cmds/__init__.py +0 -0
  40. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/cmds/__types__.py +0 -0
  41. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/cmds/deployer.py +0 -0
  42. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/cmds/detector.py +0 -0
  43. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/cmds/images.py +0 -0
  44. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/deployer/__utils__.py +0 -0
  45. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/__init__.py +0 -0
  46. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/__types__.py +0 -0
  47. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/__utils__.py +0 -0
  48. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/ascend.py +0 -0
  49. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/cambricon.py +0 -0
  50. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/iluvatar.py +0 -0
  51. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/metax.py +0 -0
  52. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/mthreads.py +0 -0
  53. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyacl/__init__.py +0 -0
  54. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyamdgpu/__init__.py +0 -0
  55. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyamdsmi/__init__.py +0 -0
  56. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pycuda/__init__.py +0 -0
  57. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pydcmi/__init__.py +0 -0
  58. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyixml/__init__.py +0 -0
  59. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pymtml/__init__.py +0 -0
  60. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pymxsml/__init__.py +0 -0
  61. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pymxsml/mxsml.py +0 -0
  62. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pymxsml/mxsml_extension.py +0 -0
  63. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pymxsml/mxsml_mcm.py +0 -0
  64. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/detector/pyrocmcore/__init__.py +0 -0
  65. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/gpustack_runtime/logging.py +0 -0
  66. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/hatch.toml +0 -0
  67. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/mkdocs.yml +0 -0
  68. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/pack/Dockerfile +0 -0
  69. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/pack/Dockerfile.dummy +0 -0
  70. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/pytest.ini +0 -0
  71. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/fixtures/__init__.py +0 -0
  72. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/fixtures/test_compare_versions.json +0 -0
  73. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/fixtures/test_correct_runner_image.json +0 -0
  74. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/fixtures/test_make_image_with.json +0 -0
  75. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/fixtures/test_nginx_entrypoint.sh +0 -0
  76. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/fixtures/test_replace_image_with.json +0 -0
  77. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/deployer/test_utils.py +0 -0
  78. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/fixtures/__init__.py +0 -0
  79. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/README.md +0 -0
  80. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_amd_mi300x.json +0 -0
  81. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json +0 -0
  82. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_ascend_310p3.json +0 -0
  83. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_ascend_910b2.json +0 -0
  84. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_hygon_k100ai.json +0 -0
  85. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_metax_c500.json +0 -0
  86. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_gb10.json +0 -0
  87. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h100.json +0 -0
  88. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_h200.json +0 -0
  89. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4080super.json +0 -0
  90. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx4090d.json +0 -0
  91. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/detect_output_nvidia_rtx5090d.json +0 -0
  92. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_amd_mi300x.json +0 -0
  93. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_amd_rx7800xt.json +0 -0
  94. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_ascend_310p3.json +0 -0
  95. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_ascend_910b2.json +0 -0
  96. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_hygon_k100ai.json +0 -0
  97. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_metax_c500.json +0 -0
  98. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h100.json +0 -0
  99. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_h200.json +0 -0
  100. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4080super.json +0 -0
  101. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx4090d.json +0 -0
  102. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/samples/topology_output_nvidia_rtx5090d.json +0 -0
  103. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_amd.py +0 -0
  104. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_ascend.py +0 -0
  105. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_cambricon.py +0 -0
  106. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_hygon.py +0 -0
  107. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_iluvatar.py +0 -0
  108. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_metax.py +0 -0
  109. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_mthreads.py +0 -0
  110. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/tests/gpustack_runtime/detector/test_nvidia.py +0 -0
  111. {gpustack_runtime-0.1.38.post4 → gpustack_runtime-0.1.39}/uv.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gpustack-runtime
3
- Version: 0.1.38.post4
3
+ Version: 0.1.39
4
4
  Summary: GPUStack Runtime is library for detecting GPU resources and launching GPU workloads.
5
5
  Project-URL: Homepage, https://github.com/gpustack/runtime
6
6
  Project-URL: Bug Tracker, https://github.com/gpustack/gpustack/issues
@@ -14,10 +14,11 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Requires-Dist: argcomplete>=3.6.3
17
- Requires-Dist: docker
18
- Requires-Dist: gpustack-runner==0.1.22.post6
19
- Requires-Dist: kubernetes
17
+ Requires-Dist: docker>=7.1.0
18
+ Requires-Dist: gpustack-runner>=0.1.23.post1
19
+ Requires-Dist: kubernetes>=33.1.0
20
20
  Requires-Dist: nvidia-ml-py>=13.580.65
21
+ Requires-Dist: podman==5.6.0
21
22
  Requires-Dist: pyyaml
22
23
  Requires-Dist: tqdm
23
24
  Description-Content-Type: text/markdown
@@ -43,6 +44,7 @@ GPUStack Runtime enables GPU workload management on the following platforms:
43
44
 
44
45
  - Docker
45
46
  - Kubernetes
47
+ - Podman (>=4.9, Experimental support via `CONTAINER_HOST=http+unix:///path/to/podman/socket` environment variable)
46
48
 
47
49
  ## License
48
50
 
@@ -19,6 +19,7 @@ GPUStack Runtime enables GPU workload management on the following platforms:
19
19
 
20
20
  - Docker
21
21
  - Kubernetes
22
+ - Podman (>=4.9, Experimental support via `CONTAINER_HOST=http+unix:///path/to/podman/socket` environment variable)
22
23
 
23
24
  ## License
24
25
 
@@ -27,8 +27,8 @@ version_tuple: VERSION_TUPLE
27
27
  __commit_id__: COMMIT_ID
28
28
  commit_id: COMMIT_ID
29
29
 
30
- __version__ = version = '0.1.38.post4'
31
- __version_tuple__ = version_tuple = (0, 1, 38, 'post4')
30
+ __version__ = version = '0.1.39'
31
+ __version_tuple__ = version_tuple = (0, 1, 39)
32
32
  try:
33
33
  from ._version_appendix import git_commit
34
34
  __commit_id__ = commit_id = git_commit
@@ -0,0 +1 @@
1
+ git_commit = "c8c93ed"
@@ -41,6 +41,11 @@ from .kuberentes import (
41
41
  KubernetesWorkloadPlan,
42
42
  KubernetesWorkloadStatus,
43
43
  )
44
+ from .podman import (
45
+ PodmanDeployer,
46
+ PodmanWorkloadPlan,
47
+ PodmanWorkloadStatus,
48
+ )
44
49
 
45
50
  if TYPE_CHECKING:
46
51
  from collections.abc import AsyncGenerator, Generator
@@ -50,6 +55,7 @@ if TYPE_CHECKING:
50
55
  _DEPLOYERS: list[Deployer] = [
51
56
  DockerDeployer(),
52
57
  KubernetesDeployer(),
58
+ PodmanDeployer(),
53
59
  ]
54
60
  """
55
61
  List of all deployers.
@@ -60,6 +66,14 @@ _DEPLOYERS_MAP: dict[str, Deployer] = {dep.name: dep for dep in _DEPLOYERS}
60
66
  Mapping from deployer name to deployer.
61
67
  """
62
68
 
69
+ _NO_AVAILABLE_DEPLOYER_MSG = (
70
+ "No available deployer. "
71
+ "Please provide a container runtime, e.g. "
72
+ "bind mount the host `/var/run/docker.sock` on Docker, "
73
+ "or allow (in-)cluster access on Kubernetes, "
74
+ "or bind mount the host `/run/podman/podman.sock` on Podman."
75
+ )
76
+
63
77
 
64
78
  def supported_list() -> list[Deployer]:
65
79
  """
@@ -98,13 +112,7 @@ def create_workload(workload: WorkloadPlan):
98
112
  dep.create(workload=workload)
99
113
  return
100
114
 
101
- msg = (
102
- "No available deployer. "
103
- "Please provide a container runtime, e.g. "
104
- "bind mount the host `/var/run/docker.sock` on Docker, "
105
- "or allow (in-)cluster access on Kubernetes"
106
- )
107
- raise UnsupportedError(msg)
115
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
108
116
 
109
117
 
110
118
  def get_workload(
@@ -136,13 +144,7 @@ def get_workload(
136
144
 
137
145
  return dep.get(name=name, namespace=namespace)
138
146
 
139
- msg = (
140
- "No available deployer. "
141
- "Please provide a container runtime, e.g. "
142
- "bind mount the host `/var/run/docker.sock` on Docker, "
143
- "or allow (in-)cluster access on Kubernetes"
144
- )
145
- raise UnsupportedError(msg)
147
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
146
148
 
147
149
 
148
150
  def delete_workload(
@@ -174,13 +176,7 @@ def delete_workload(
174
176
 
175
177
  return dep.delete(name=name, namespace=namespace)
176
178
 
177
- msg = (
178
- "No available deployer. "
179
- "Please provide a container runtime, e.g. "
180
- "bind mount the host `/var/run/docker.sock` on Docker, "
181
- "or allow (in-)cluster access on Kubernetes"
182
- )
183
- raise UnsupportedError(msg)
179
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
184
180
 
185
181
 
186
182
  def list_workloads(
@@ -212,13 +208,7 @@ def list_workloads(
212
208
 
213
209
  return dep.list(namespace=namespace, labels=labels)
214
210
 
215
- msg = (
216
- "No available deployer. "
217
- "Please provide a container runtime, e.g. "
218
- "bind mount the host `/var/run/docker.sock` on Docker, "
219
- "or allow (in-)cluster access on Kubernetes"
220
- )
221
- raise UnsupportedError(msg)
211
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
222
212
 
223
213
 
224
214
  def logs_workload(
@@ -273,13 +263,7 @@ def logs_workload(
273
263
  follow=follow,
274
264
  )
275
265
 
276
- msg = (
277
- "No available deployer. "
278
- "Please provide a container runtime, e.g. "
279
- "bind mount the host `/var/run/docker.sock` on Docker, "
280
- "or allow (in-)cluster access on Kubernetes"
281
- )
282
- raise UnsupportedError(msg)
266
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
283
267
 
284
268
 
285
269
  async def async_logs_workload(
@@ -334,13 +318,7 @@ async def async_logs_workload(
334
318
  follow=follow,
335
319
  )
336
320
 
337
- msg = (
338
- "No available deployer. "
339
- "Please provide a container runtime, e.g. "
340
- "bind mount the host `/var/run/docker.sock` on Docker, "
341
- "or allow (in-)cluster access on Kubernetes"
342
- )
343
- raise UnsupportedError(msg)
321
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
344
322
 
345
323
 
346
324
  def exec_workload(
@@ -392,13 +370,7 @@ def exec_workload(
392
370
  args=args,
393
371
  )
394
372
 
395
- msg = (
396
- "No available deployer. "
397
- "Please provide a container runtime, e.g. "
398
- "bind mount the host `/var/run/docker.sock` on Docker, "
399
- "or allow (in-)cluster access on Kubernetes"
400
- )
401
- raise UnsupportedError(msg)
373
+ raise UnsupportedError(_NO_AVAILABLE_DEPLOYER_MSG)
402
374
 
403
375
 
404
376
  __all__ = [
@@ -424,6 +396,9 @@ __all__ = [
424
396
  "KubernetesWorkloadPlan",
425
397
  "KubernetesWorkloadStatus",
426
398
  "OperationError",
399
+ "PodmanDeployer",
400
+ "PodmanWorkloadPlan",
401
+ "PodmanWorkloadStatus",
427
402
  "UnsupportedError",
428
403
  "WorkloadExecStream",
429
404
  "WorkloadOperationToken",
@@ -0,0 +1,455 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import copy
5
+ import re
6
+ from collections.abc import MutableMapping
7
+ from typing import Any
8
+
9
+ from podman.domain.containers_create import NAMED_VOLUME_PATTERN, CreateMixin
10
+ from podman.domain.pods import Pod
11
+ from podman.domain.secrets import Secret
12
+
13
+
14
+ # NB(thxCode): Modify from podman-py 5.6.0 source code to fit our need,
15
+ # see https://github.com/containers/podman-py/blob/435e7a904b92560b8c0d883ed8994f57eea27171/podman/domain/containers_create.py#L432-L844.
16
+ def patch_render_payload(kwargs: MutableMapping[str, Any]) -> dict[str, Any]:
17
+ """Map create/run kwargs into body parameters."""
18
+ args = copy.copy(kwargs)
19
+
20
+ if "links" in args:
21
+ if len(args["links"]) > 0:
22
+ msg = "'links' are not supported by Podman service."
23
+ raise ValueError(msg)
24
+ del args["links"]
25
+
26
+ # Ignore these keywords
27
+ for key in (
28
+ "cpu_count",
29
+ "cpu_percent",
30
+ "nano_cpus",
31
+ "platform", # used by caller
32
+ "remove", # used by caller
33
+ "stderr", # used by caller
34
+ "stdout", # used by caller
35
+ "stream", # used by caller
36
+ "detach", # used by caller
37
+ "volume_driver",
38
+ ):
39
+ with contextlib.suppress(KeyError):
40
+ del args[key]
41
+
42
+ # Handle environment variables
43
+ environment = args.pop("environment", None)
44
+ if environment is not None:
45
+ if isinstance(environment, list):
46
+ try:
47
+ environment = CreateMixin._convert_env_list_to_dict(environment)
48
+ except ValueError as e:
49
+ msg = (
50
+ "Failed to convert environment variables list to dictionary. "
51
+ f"Error: {e!s}"
52
+ )
53
+ raise ValueError(
54
+ msg,
55
+ ) from e
56
+ elif not isinstance(environment, dict):
57
+ msg = (
58
+ "Environment variables must be provided as either a dictionary "
59
+ "or a list of strings in the format ['KEY=value']"
60
+ )
61
+ raise TypeError(
62
+ msg,
63
+ )
64
+
65
+ # These keywords are not supported for various reasons.
66
+ unsupported_keys = set(
67
+ args.keys(),
68
+ ).intersection(
69
+ (
70
+ "blkio_weight",
71
+ "blkio_weight_device", # FIXME In addition to device Major/Minor include path
72
+ "device_cgroup_rules", # FIXME Where to map for Podman API?
73
+ "device_read_bps", # FIXME In addition to device Major/Minor include path
74
+ "device_read_iops", # FIXME In addition to device Major/Minor include path
75
+ "device_requests", # FIXME In addition to device Major/Minor include path
76
+ "device_write_bps", # FIXME In addition to device Major/Minor include path
77
+ "device_write_iops", # FIXME In addition to device Major/Minor include path
78
+ "domainname",
79
+ "network_disabled", # FIXME Where to map for Podman API?
80
+ "storage_opt", # FIXME Where to map for Podman API?
81
+ "tmpfs", # FIXME Where to map for Podman API?
82
+ ),
83
+ )
84
+ if len(unsupported_keys) > 0:
85
+ msg = (
86
+ f"""Keyword(s) '{" ,".join(unsupported_keys)}' are"""
87
+ f""" currently not supported by Podman API."""
88
+ )
89
+ raise TypeError(
90
+ msg,
91
+ )
92
+
93
+ def pop(k):
94
+ return args.pop(k, None)
95
+
96
+ def normalize_nsmode(
97
+ mode: str | MutableMapping[str, str],
98
+ ) -> dict[str, str]:
99
+ if isinstance(mode, dict):
100
+ return mode
101
+ return {"nsmode": mode}
102
+
103
+ def to_bytes(size: int | str | None) -> int | None:
104
+ """
105
+ Converts str or int to bytes.
106
+ Input can be in the following forms :
107
+ 0) None - e.g. None -> returns None
108
+ 1) int - e.g. 100 == 100 bytes
109
+ 2) str - e.g. '100' == 100 bytes
110
+ 3) str with suffix - available suffixes:
111
+ b | B - bytes
112
+ k | K = kilobytes
113
+ m | M = megabytes
114
+ g | G = gigabytes
115
+ e.g. '100m' == 104857600 bytes.
116
+ """
117
+ size_type = type(size)
118
+ if size is None:
119
+ return size
120
+ if size_type is int:
121
+ return size
122
+ if size_type is str:
123
+ try:
124
+ return int(size)
125
+ except ValueError as bad_size:
126
+ mapping = {"b": 0, "k": 1, "m": 2, "g": 3}
127
+ mapping_regex = "".join(mapping.keys())
128
+ search = re.search(rf"^(\d+)([{mapping_regex}])$", size.lower())
129
+ if search:
130
+ return int(search.group(1)) * (1024 ** mapping[search.group(2)])
131
+ msg = f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g. '100m')"
132
+ raise TypeError(
133
+ msg,
134
+ ) from bad_size
135
+ else:
136
+ msg = (
137
+ f"Passed size {size} should be a type of unicode, str "
138
+ f"or int (found : {size_type})"
139
+ )
140
+ raise TypeError(
141
+ msg,
142
+ )
143
+
144
+ # Transform keywords into parameters
145
+ params = {
146
+ "annotations": pop("annotations"), # TODO document, podman only
147
+ "apparmor_profile": pop("apparmor_profile"), # TODO document, podman only
148
+ "cap_add": pop("cap_add"),
149
+ "cap_drop": pop("cap_drop"),
150
+ "cgroup_parent": pop("cgroup_parent"),
151
+ "cgroups_mode": pop("cgroups_mode"), # TODO document, podman only
152
+ "cni_networks": [pop("network")],
153
+ "command": args.pop("command", args.pop("cmd", None)),
154
+ "conmon_pid_file": pop("conmon_pid_file"), # TODO document, podman only
155
+ "containerCreateCommand": pop(
156
+ "containerCreateCommand",
157
+ ), # TODO document, podman only
158
+ "devices": [],
159
+ "dns_option": pop("dns_opt"),
160
+ "dns_search": pop("dns_search"),
161
+ "dns_server": pop("dns"),
162
+ "entrypoint": pop("entrypoint"),
163
+ "env": environment,
164
+ "env_host": pop("env_host"), # TODO document, podman only
165
+ "expose": {},
166
+ "groups": pop("group_add"),
167
+ "healthconfig": pop("healthcheck"),
168
+ "health_check_on_failure_action": pop("health_check_on_failure_action"),
169
+ "hostadd": [],
170
+ "hostname": pop("hostname"),
171
+ "httpproxy": pop("use_config_proxy"),
172
+ "idmappings": pop("idmappings"), # TODO document, podman only
173
+ "image": pop("image"),
174
+ "image_volume_mode": pop("image_volume_mode"), # TODO document, podman only
175
+ "image_volumes": pop("image_volumes"), # TODO document, podman only
176
+ "init": pop("init"),
177
+ "init_path": pop("init_path"),
178
+ "isolation": pop("isolation"),
179
+ "labels": pop("labels"),
180
+ "log_configuration": {},
181
+ "lxc_config": pop("lxc_config"),
182
+ "mask": pop("masked_paths"),
183
+ "mounts": [],
184
+ "name": pop("name"),
185
+ "namespace": pop("namespace"), # TODO What is this for?
186
+ "network_options": pop("network_options"), # TODO document, podman only
187
+ "networks": pop("networks"),
188
+ "no_new_privileges": pop("no_new_privileges"), # TODO document, podman only
189
+ "oci_runtime": pop("runtime"),
190
+ "oom_score_adj": pop("oom_score_adj"),
191
+ "overlay_volumes": pop("overlay_volumes"), # TODO document, podman only
192
+ "portmappings": [],
193
+ "privileged": pop("privileged"),
194
+ "procfs_opts": pop("procfs_opts"), # TODO document, podman only
195
+ "publish_image_ports": pop("publish_all_ports"),
196
+ "r_limits": [],
197
+ "raw_image_name": pop("raw_image_name"), # TODO document, podman only
198
+ "read_only_filesystem": pop("read_only"),
199
+ "read_write_tmpfs": pop("read_write_tmpfs"),
200
+ "remove": args.pop("remove", args.pop("auto_remove", None)),
201
+ "resource_limits": {},
202
+ "rootfs": pop("rootfs"),
203
+ "rootfs_propagation": pop("rootfs_propagation"),
204
+ "sdnotifyMode": pop("sdnotifyMode"), # TODO document, podman only
205
+ "seccomp_policy": pop("seccomp_policy"), # TODO document, podman only
206
+ "seccomp_profile_path": pop(
207
+ "seccomp_profile_path",
208
+ ), # TODO document, podman only
209
+ "secrets": [], # TODO document, podman only
210
+ "selinux_opts": pop("security_opt"),
211
+ "shm_size": to_bytes(pop("shm_size")),
212
+ "static_mac": pop("mac_address"),
213
+ "stdin": pop("stdin_open"),
214
+ "stop_signal": pop("stop_signal"),
215
+ "stop_timeout": pop("stop_timeout"), # TODO document, podman only
216
+ "sysctl": pop("sysctls"),
217
+ "systemd": pop("systemd"), # TODO document, podman only
218
+ "terminal": pop("tty"),
219
+ "timezone": pop("timezone"),
220
+ "umask": pop("umask"), # TODO document, podman only
221
+ "unified": pop("unified"), # TODO document, podman only
222
+ "unmask": pop("unmasked_paths"), # TODO document, podman only
223
+ "use_image_hosts": pop("use_image_hosts"), # TODO document, podman only
224
+ "use_image_resolve_conf": pop(
225
+ "use_image_resolve_conf",
226
+ ), # TODO document, podman only
227
+ "user": pop("user"),
228
+ "version": pop("version"),
229
+ "volumes": [],
230
+ "volumes_from": pop("volumes_from"),
231
+ "work_dir": pop("workdir") or pop("working_dir"),
232
+ }
233
+
234
+ for device in args.pop("devices", []):
235
+ params["devices"].append({"path": device})
236
+
237
+ for item in args.pop("exposed_ports", []):
238
+ port, protocol = item.split("/")
239
+ params["expose"][int(port)] = protocol
240
+
241
+ for hostname, ip in args.pop("extra_hosts", {}).items():
242
+ params["hostadd"].append(f"{hostname}:{ip}")
243
+
244
+ if "log_config" in args:
245
+ params["log_configuration"]["driver"] = args["log_config"].get("Type")
246
+
247
+ if "Config" in args["log_config"]:
248
+ params["log_configuration"]["path"] = args["log_config"]["Config"].get(
249
+ "path",
250
+ )
251
+ params["log_configuration"]["size"] = args["log_config"]["Config"].get(
252
+ "size",
253
+ )
254
+ params["log_configuration"]["options"] = args["log_config"]["Config"].get(
255
+ "options",
256
+ )
257
+ args.pop("log_config")
258
+
259
+ for item in args.pop("mounts", []):
260
+ normalized_item = {key.lower(): value for key, value in item.items()}
261
+ mount_point = {
262
+ "destination": normalized_item.get("target"),
263
+ "options": [],
264
+ "source": normalized_item.get("source"),
265
+ "type": normalized_item.get("type"),
266
+ }
267
+
268
+ # some names are different for podman-py vs REST API due to compatibility with docker
269
+ # some (e.g. chown) despite listed in podman-run documentation fails with error
270
+ names_dict = {"read_only": "ro", "chown": "U"}
271
+
272
+ options = []
273
+ simple_options = ["propagation", "relabel"]
274
+ bool_options = ["read_only", "U", "chown"]
275
+ regular_options = ["consistency", "mode", "size"]
276
+
277
+ for k, v in item.items():
278
+ _k = k.lower()
279
+ option_name = names_dict.get(_k, _k)
280
+ if _k in bool_options and v is True:
281
+ options.append(option_name)
282
+ elif _k in regular_options:
283
+ options.append(f"{option_name}={v}")
284
+ elif _k in simple_options:
285
+ options.append(v)
286
+
287
+ mount_point["options"] = options
288
+
289
+ params["mounts"].append(mount_point)
290
+
291
+ if "pod" in args:
292
+ pod = args.pop("pod")
293
+ if isinstance(pod, Pod):
294
+ pod = pod.id
295
+ params["pod"] = pod # TODO document, podman only
296
+
297
+ def parse_host_port(_container_port, _protocol, _host):
298
+ result = []
299
+ port_map = {"container_port": int(_container_port), "protocol": _protocol}
300
+ if _host is None:
301
+ result.append(port_map)
302
+ elif isinstance(_host, int) or (isinstance(_host, str) and _host.isdigit()):
303
+ port_map["host_port"] = int(_host)
304
+ result.append(port_map)
305
+ elif isinstance(_host, tuple):
306
+ port_map["host_ip"] = _host[0]
307
+ port_map["host_port"] = int(_host[1])
308
+ result.append(port_map)
309
+ elif isinstance(_host, list):
310
+ for host_list in _host:
311
+ host_list_result = parse_host_port(
312
+ _container_port,
313
+ _protocol,
314
+ host_list,
315
+ )
316
+ result.extend(host_list_result)
317
+ elif isinstance(_host, dict):
318
+ _host_port = _host.get("port")
319
+ if _host_port is not None:
320
+ if isinstance(_host_port, int) or (
321
+ isinstance(_host_port, str) and _host_port.isdigit()
322
+ ):
323
+ port_map["host_port"] = int(_host_port)
324
+ elif isinstance(_host_port, tuple):
325
+ port_map["host_ip"] = _host_port[0]
326
+ port_map["host_port"] = int(_host_port[1])
327
+ if _host.get("range"):
328
+ port_map["range"] = _host.get("range")
329
+ if _host.get("ip"):
330
+ port_map["host_ip"] = _host.get("ip")
331
+ result.append(port_map)
332
+ return result
333
+
334
+ for container, host in args.pop("ports", {}).items():
335
+ # avoid redefinition of the loop variable, then ensure it's a string
336
+ str_container = container
337
+ if isinstance(str_container, int):
338
+ str_container = str(str_container)
339
+
340
+ if "/" in str_container:
341
+ container_port, protocol = str_container.split("/")
342
+ else:
343
+ container_port, protocol = str_container, "tcp"
344
+
345
+ port_map_list = parse_host_port(container_port, protocol, host)
346
+ params["portmappings"].extend(port_map_list)
347
+
348
+ if "restart_policy" in args:
349
+ params["restart_policy"] = args["restart_policy"].get("Name")
350
+ params["restart_tries"] = args["restart_policy"].get("MaximumRetryCount")
351
+ args.pop("restart_policy")
352
+
353
+ params["resource_limits"]["pids"] = {"limit": args.pop("pids_limit", None)}
354
+
355
+ params["resource_limits"]["cpu"] = {
356
+ "cpus": args.pop("cpuset_cpus", None),
357
+ "mems": args.pop("cpuset_mems", None),
358
+ "period": args.pop("cpu_period", None),
359
+ "quota": args.pop("cpu_quota", None),
360
+ "realtimePeriod": args.pop("cpu_rt_period", None),
361
+ "realtimeRuntime": args.pop("cpu_rt_runtime", None),
362
+ "shares": args.pop("cpu_shares", None),
363
+ }
364
+
365
+ params["resource_limits"]["memory"] = {
366
+ "disableOOMKiller": args.pop("oom_kill_disable", None),
367
+ "kernel": to_bytes(args.pop("kernel_memory", None)),
368
+ "kernelTCP": args.pop("kernel_memory_tcp", None),
369
+ "limit": to_bytes(args.pop("mem_limit", None)),
370
+ "reservation": to_bytes(args.pop("mem_reservation", None)),
371
+ "swap": to_bytes(args.pop("memswap_limit", None)),
372
+ "swappiness": args.pop("mem_swappiness", None),
373
+ "useHierarchy": args.pop("mem_use_hierarchy", None),
374
+ }
375
+
376
+ for item in args.pop("ulimits", []):
377
+ params["r_limits"].append(
378
+ {
379
+ "type": item["Name"],
380
+ "hard": item["Hard"],
381
+ "soft": item["Soft"],
382
+ },
383
+ )
384
+
385
+ for item in args.pop("volumes", {}).items():
386
+ key, value = item
387
+ extended_mode = value.get("extended_mode", [])
388
+ if not isinstance(extended_mode, list):
389
+ msg = "'extended_mode' value should be a list"
390
+ raise ValueError(msg)
391
+
392
+ options = extended_mode
393
+ mode = value.get("mode")
394
+ if mode is not None:
395
+ if not isinstance(mode, str):
396
+ msg = "'mode' value should be a str"
397
+ raise ValueError(msg)
398
+ options.append(mode)
399
+
400
+ # The Podman API only supports named volumes through the ``volume`` parameter. Directory
401
+ # mounting needs to happen through the ``mounts`` parameter. Luckily the translation
402
+ # isn't too complicated so we can just do it for the user if we suspect that the key
403
+ # isn't a named volume.
404
+ if NAMED_VOLUME_PATTERN.match(key):
405
+ volume = {"Name": key, "Dest": value["bind"], "Options": options}
406
+ params["volumes"].append(volume)
407
+ else:
408
+ mount_point = {
409
+ "destination": value["bind"],
410
+ "options": options,
411
+ "source": key,
412
+ "type": "bind",
413
+ }
414
+ params["mounts"].append(mount_point)
415
+
416
+ for item in args.pop("secrets", []):
417
+ if isinstance(item, Secret):
418
+ params["secrets"].append({"source": item.id})
419
+ elif isinstance(item, str):
420
+ params["secrets"].append({"source": item})
421
+ elif isinstance(item, dict):
422
+ secret = {}
423
+ secret_opts = ["source", "target", "uid", "gid", "mode"]
424
+ for k, v in item.items():
425
+ if k in secret_opts:
426
+ secret.update({k: v})
427
+ params["secrets"].append(secret)
428
+
429
+ if "secret_env" in args:
430
+ params["secret_env"] = args.pop("secret_env", {})
431
+
432
+ if "cgroupns" in args:
433
+ params["cgroupns"] = normalize_nsmode(args.pop("cgroupns"))
434
+
435
+ if "ipc_mode" in args:
436
+ params["ipcns"] = normalize_nsmode(args.pop("ipc_mode"))
437
+
438
+ if "network_mode" in args:
439
+ params["netns"] = normalize_nsmode(args.pop("network_mode"))
440
+
441
+ if "pid_mode" in args:
442
+ params["pidns"] = normalize_nsmode(args.pop("pid_mode"))
443
+
444
+ if "userns_mode" in args:
445
+ params["userns"] = normalize_nsmode(args.pop("userns_mode"))
446
+
447
+ if "uts_mode" in args:
448
+ params["utsns"] = normalize_nsmode(args.pop("uts_mode"))
449
+
450
+ if len(args) > 0:
451
+ raise TypeError(
452
+ "Unknown keyword argument(s): " + " ,".join(f"'{k}'" for k in args),
453
+ )
454
+
455
+ return params