moru 0.1.0__py3-none-any.whl
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.
- moru/__init__.py +174 -0
- moru/api/__init__.py +164 -0
- moru/api/client/__init__.py +8 -0
- moru/api/client/api/__init__.py +1 -0
- moru/api/client/api/sandboxes/__init__.py +1 -0
- moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- moru/api/client/api/sandboxes/get_sandboxes.py +176 -0
- moru/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- moru/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- moru/api/client/api/sandboxes/post_sandboxes.py +172 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- moru/api/client/api/templates/__init__.py +1 -0
- moru/api/client/api/templates/delete_templates_template_id.py +157 -0
- moru/api/client/api/templates/get_templates.py +172 -0
- moru/api/client/api/templates/get_templates_template_id.py +195 -0
- moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +217 -0
- moru/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- moru/api/client/api/templates/patch_templates_template_id.py +183 -0
- moru/api/client/api/templates/post_templates.py +172 -0
- moru/api/client/api/templates/post_templates_template_id.py +181 -0
- moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- moru/api/client/api/templates/post_v2_templates.py +172 -0
- moru/api/client/api/templates/post_v3_templates.py +172 -0
- moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- moru/api/client/client.py +286 -0
- moru/api/client/errors.py +16 -0
- moru/api/client/models/__init__.py +123 -0
- moru/api/client/models/aws_registry.py +85 -0
- moru/api/client/models/aws_registry_type.py +8 -0
- moru/api/client/models/build_log_entry.py +89 -0
- moru/api/client/models/build_status_reason.py +95 -0
- moru/api/client/models/connect_sandbox.py +59 -0
- moru/api/client/models/created_access_token.py +100 -0
- moru/api/client/models/created_team_api_key.py +166 -0
- moru/api/client/models/disk_metrics.py +91 -0
- moru/api/client/models/error.py +67 -0
- moru/api/client/models/gcp_registry.py +69 -0
- moru/api/client/models/gcp_registry_type.py +8 -0
- moru/api/client/models/general_registry.py +77 -0
- moru/api/client/models/general_registry_type.py +8 -0
- moru/api/client/models/identifier_masking_details.py +83 -0
- moru/api/client/models/listed_sandbox.py +154 -0
- moru/api/client/models/log_level.py +11 -0
- moru/api/client/models/max_team_metric.py +78 -0
- moru/api/client/models/mcp_type_0.py +44 -0
- moru/api/client/models/new_access_token.py +59 -0
- moru/api/client/models/new_sandbox.py +172 -0
- moru/api/client/models/new_team_api_key.py +59 -0
- moru/api/client/models/node.py +155 -0
- moru/api/client/models/node_detail.py +165 -0
- moru/api/client/models/node_metrics.py +122 -0
- moru/api/client/models/node_status.py +11 -0
- moru/api/client/models/node_status_change.py +79 -0
- moru/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- moru/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- moru/api/client/models/resumed_sandbox.py +68 -0
- moru/api/client/models/sandbox.py +145 -0
- moru/api/client/models/sandbox_detail.py +183 -0
- moru/api/client/models/sandbox_log.py +70 -0
- moru/api/client/models/sandbox_log_entry.py +93 -0
- moru/api/client/models/sandbox_log_entry_fields.py +44 -0
- moru/api/client/models/sandbox_logs.py +91 -0
- moru/api/client/models/sandbox_metric.py +118 -0
- moru/api/client/models/sandbox_network_config.py +92 -0
- moru/api/client/models/sandbox_state.py +9 -0
- moru/api/client/models/sandboxes_with_metrics.py +59 -0
- moru/api/client/models/team.py +83 -0
- moru/api/client/models/team_api_key.py +158 -0
- moru/api/client/models/team_metric.py +86 -0
- moru/api/client/models/team_user.py +68 -0
- moru/api/client/models/template.py +217 -0
- moru/api/client/models/template_build.py +139 -0
- moru/api/client/models/template_build_file_upload.py +70 -0
- moru/api/client/models/template_build_info.py +126 -0
- moru/api/client/models/template_build_request.py +115 -0
- moru/api/client/models/template_build_request_v2.py +88 -0
- moru/api/client/models/template_build_request_v3.py +88 -0
- moru/api/client/models/template_build_start_v2.py +184 -0
- moru/api/client/models/template_build_status.py +11 -0
- moru/api/client/models/template_legacy.py +207 -0
- moru/api/client/models/template_request_response_v3.py +83 -0
- moru/api/client/models/template_step.py +91 -0
- moru/api/client/models/template_update_request.py +59 -0
- moru/api/client/models/template_with_builds.py +148 -0
- moru/api/client/models/update_team_api_key.py +59 -0
- moru/api/client/py.typed +1 -0
- moru/api/client/types.py +54 -0
- moru/api/client_async/__init__.py +50 -0
- moru/api/client_sync/__init__.py +52 -0
- moru/api/metadata.py +14 -0
- moru/connection_config.py +217 -0
- moru/envd/api.py +59 -0
- moru/envd/filesystem/filesystem_connect.py +193 -0
- moru/envd/filesystem/filesystem_pb2.py +76 -0
- moru/envd/filesystem/filesystem_pb2.pyi +233 -0
- moru/envd/process/process_connect.py +155 -0
- moru/envd/process/process_pb2.py +92 -0
- moru/envd/process/process_pb2.pyi +304 -0
- moru/envd/rpc.py +61 -0
- moru/envd/versions.py +6 -0
- moru/exceptions.py +95 -0
- moru/sandbox/commands/command_handle.py +69 -0
- moru/sandbox/commands/main.py +39 -0
- moru/sandbox/filesystem/filesystem.py +94 -0
- moru/sandbox/filesystem/watch_handle.py +60 -0
- moru/sandbox/main.py +210 -0
- moru/sandbox/mcp.py +1120 -0
- moru/sandbox/network.py +8 -0
- moru/sandbox/sandbox_api.py +210 -0
- moru/sandbox/signature.py +45 -0
- moru/sandbox/utils.py +34 -0
- moru/sandbox_async/commands/command.py +336 -0
- moru/sandbox_async/commands/command_handle.py +196 -0
- moru/sandbox_async/commands/pty.py +240 -0
- moru/sandbox_async/filesystem/filesystem.py +531 -0
- moru/sandbox_async/filesystem/watch_handle.py +62 -0
- moru/sandbox_async/main.py +734 -0
- moru/sandbox_async/paginator.py +69 -0
- moru/sandbox_async/sandbox_api.py +325 -0
- moru/sandbox_async/utils.py +7 -0
- moru/sandbox_sync/commands/command.py +328 -0
- moru/sandbox_sync/commands/command_handle.py +150 -0
- moru/sandbox_sync/commands/pty.py +230 -0
- moru/sandbox_sync/filesystem/filesystem.py +518 -0
- moru/sandbox_sync/filesystem/watch_handle.py +69 -0
- moru/sandbox_sync/main.py +726 -0
- moru/sandbox_sync/paginator.py +69 -0
- moru/sandbox_sync/sandbox_api.py +308 -0
- moru/template/consts.py +30 -0
- moru/template/dockerfile_parser.py +275 -0
- moru/template/logger.py +232 -0
- moru/template/main.py +1360 -0
- moru/template/readycmd.py +138 -0
- moru/template/types.py +105 -0
- moru/template/utils.py +320 -0
- moru/template_async/build_api.py +202 -0
- moru/template_async/main.py +366 -0
- moru/template_sync/build_api.py +199 -0
- moru/template_sync/main.py +371 -0
- moru-0.1.0.dist-info/METADATA +63 -0
- moru-0.1.0.dist-info/RECORD +152 -0
- moru-0.1.0.dist-info/WHEEL +4 -0
- moru-0.1.0.dist-info/licenses/LICENSE +9 -0
- moru_connect/__init__.py +1 -0
- moru_connect/client.py +493 -0
moru/template/main.py
ADDED
|
@@ -0,0 +1,1360 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, List, Optional, Union, Literal
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from moru.exceptions import BuildException
|
|
7
|
+
from moru.template.consts import STACK_TRACE_DEPTH, RESOLVE_SYMLINKS
|
|
8
|
+
from moru.template.dockerfile_parser import parse_dockerfile
|
|
9
|
+
from moru.template.readycmd import ReadyCmd, wait_for_file
|
|
10
|
+
from moru.template.types import (
|
|
11
|
+
CopyItem,
|
|
12
|
+
Instruction,
|
|
13
|
+
TemplateType,
|
|
14
|
+
RegistryConfig,
|
|
15
|
+
InstructionType,
|
|
16
|
+
)
|
|
17
|
+
from moru.template.utils import (
|
|
18
|
+
calculate_files_hash,
|
|
19
|
+
get_caller_directory,
|
|
20
|
+
pad_octal,
|
|
21
|
+
read_dockerignore,
|
|
22
|
+
read_gcp_service_account_json,
|
|
23
|
+
get_caller_frame,
|
|
24
|
+
)
|
|
25
|
+
from types import TracebackType
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TemplateBuilder:
|
|
29
|
+
"""
|
|
30
|
+
Builder class for adding instructions to an Moru template.
|
|
31
|
+
|
|
32
|
+
All methods return self to allow method chaining.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, template: "TemplateBase"):
|
|
36
|
+
self._template = template
|
|
37
|
+
|
|
38
|
+
def copy(
|
|
39
|
+
self,
|
|
40
|
+
src: Union[Union[str, Path], List[Union[str, Path]]],
|
|
41
|
+
dest: Union[str, Path],
|
|
42
|
+
force_upload: Optional[Literal[True]] = None,
|
|
43
|
+
user: Optional[str] = None,
|
|
44
|
+
mode: Optional[int] = None,
|
|
45
|
+
resolve_symlinks: Optional[bool] = None,
|
|
46
|
+
) -> "TemplateBuilder":
|
|
47
|
+
"""
|
|
48
|
+
Copy files or directories from the local filesystem into the template.
|
|
49
|
+
|
|
50
|
+
:param src: Source file(s) or directory path(s) to copy
|
|
51
|
+
:param dest: Destination path in the template
|
|
52
|
+
:param force_upload: Force upload even if files are cached
|
|
53
|
+
:param user: User and optionally group (user:group) to own the files
|
|
54
|
+
:param mode: File permissions in octal format (e.g., 0o755)
|
|
55
|
+
:param resolve_symlinks: Whether to resolve symlinks
|
|
56
|
+
|
|
57
|
+
:return: `TemplateBuilder` class
|
|
58
|
+
|
|
59
|
+
Example
|
|
60
|
+
```python
|
|
61
|
+
template.copy('requirements.txt', '/home/user/')
|
|
62
|
+
template.copy(['app.py', 'config.py'], '/app/', mode=0o755)
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
srcs = [src] if isinstance(src, (str, Path)) else src
|
|
66
|
+
|
|
67
|
+
for src_item in srcs:
|
|
68
|
+
args = [
|
|
69
|
+
str(src_item),
|
|
70
|
+
str(dest),
|
|
71
|
+
user or "",
|
|
72
|
+
pad_octal(mode) if mode else "",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
instruction: Instruction = {
|
|
76
|
+
"type": InstructionType.COPY,
|
|
77
|
+
"args": args,
|
|
78
|
+
"force": force_upload or self._template._force_next_layer,
|
|
79
|
+
"forceUpload": force_upload,
|
|
80
|
+
"resolveSymlinks": resolve_symlinks,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
self._template._instructions.append(instruction)
|
|
84
|
+
|
|
85
|
+
self._template._collect_stack_trace()
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def copy_items(self, items: List[CopyItem]) -> "TemplateBuilder":
|
|
89
|
+
"""
|
|
90
|
+
Copy multiple files or directories using a list of copy items.
|
|
91
|
+
|
|
92
|
+
:param items: List of CopyItem dictionaries with src, dest, and optional parameters
|
|
93
|
+
|
|
94
|
+
:return: `TemplateBuilder` class
|
|
95
|
+
|
|
96
|
+
Example
|
|
97
|
+
```python
|
|
98
|
+
template.copy_items([
|
|
99
|
+
{'src': 'app.py', 'dest': '/app/'},
|
|
100
|
+
{'src': 'config.py', 'dest': '/app/', 'mode': 0o644}
|
|
101
|
+
])
|
|
102
|
+
```
|
|
103
|
+
"""
|
|
104
|
+
self._template._run_in_new_stack_trace_context(
|
|
105
|
+
lambda: [
|
|
106
|
+
self.copy(
|
|
107
|
+
item["src"],
|
|
108
|
+
item["dest"],
|
|
109
|
+
item.get("forceUpload"),
|
|
110
|
+
item.get("user"),
|
|
111
|
+
item.get("mode"),
|
|
112
|
+
item.get("resolveSymlinks"),
|
|
113
|
+
)
|
|
114
|
+
for item in items
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def remove(
|
|
120
|
+
self,
|
|
121
|
+
path: Union[Union[str, Path], List[Union[str, Path]]],
|
|
122
|
+
force: bool = False,
|
|
123
|
+
recursive: bool = False,
|
|
124
|
+
user: Optional[str] = None,
|
|
125
|
+
) -> "TemplateBuilder":
|
|
126
|
+
"""
|
|
127
|
+
Remove files or directories in the template.
|
|
128
|
+
|
|
129
|
+
:param path: File(s) or directory path(s) to remove
|
|
130
|
+
:param force: Force removal without prompting
|
|
131
|
+
:param recursive: Remove directories recursively
|
|
132
|
+
:param user: User to run the command as
|
|
133
|
+
|
|
134
|
+
:return: `TemplateBuilder` class
|
|
135
|
+
|
|
136
|
+
Example
|
|
137
|
+
```python
|
|
138
|
+
template.remove('/tmp/cache', recursive=True, force=True)
|
|
139
|
+
template.remove('/tmp/cache', recursive=True, force=True, user='root')
|
|
140
|
+
```
|
|
141
|
+
"""
|
|
142
|
+
paths = [path] if isinstance(path, (str, Path)) else path
|
|
143
|
+
args = ["rm"]
|
|
144
|
+
if recursive:
|
|
145
|
+
args.append("-r")
|
|
146
|
+
if force:
|
|
147
|
+
args.append("-f")
|
|
148
|
+
args.extend([str(p) for p in paths])
|
|
149
|
+
|
|
150
|
+
return self._template._run_in_new_stack_trace_context(
|
|
151
|
+
lambda: self.run_cmd(" ".join(args), user=user)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def rename(
|
|
155
|
+
self,
|
|
156
|
+
src: Union[str, Path],
|
|
157
|
+
dest: Union[str, Path],
|
|
158
|
+
force: bool = False,
|
|
159
|
+
user: Optional[str] = None,
|
|
160
|
+
) -> "TemplateBuilder":
|
|
161
|
+
"""
|
|
162
|
+
Rename or move a file or directory in the template.
|
|
163
|
+
|
|
164
|
+
:param src: Source path
|
|
165
|
+
:param dest: Destination path
|
|
166
|
+
:param force: Force rename without prompting
|
|
167
|
+
:param user: User to run the command as
|
|
168
|
+
|
|
169
|
+
:return: `TemplateBuilder` class
|
|
170
|
+
|
|
171
|
+
Example
|
|
172
|
+
```python
|
|
173
|
+
template.rename('/tmp/old.txt', '/tmp/new.txt')
|
|
174
|
+
template.rename('/tmp/old.txt', '/tmp/new.txt', user='root')
|
|
175
|
+
```
|
|
176
|
+
"""
|
|
177
|
+
args = ["mv", str(src), str(dest)]
|
|
178
|
+
if force:
|
|
179
|
+
args.append("-f")
|
|
180
|
+
|
|
181
|
+
return self._template._run_in_new_stack_trace_context(
|
|
182
|
+
lambda: self.run_cmd(" ".join(args), user=user)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def make_dir(
|
|
186
|
+
self,
|
|
187
|
+
path: Union[Union[str, Path], List[Union[str, Path]]],
|
|
188
|
+
mode: Optional[int] = None,
|
|
189
|
+
user: Optional[str] = None,
|
|
190
|
+
) -> "TemplateBuilder":
|
|
191
|
+
"""
|
|
192
|
+
Create directory(ies) in the template.
|
|
193
|
+
|
|
194
|
+
:param path: Directory path(s) to create
|
|
195
|
+
:param mode: Directory permissions in octal format (e.g., 0o755)
|
|
196
|
+
:param user: User to run the command as
|
|
197
|
+
|
|
198
|
+
:return: `TemplateBuilder` class
|
|
199
|
+
|
|
200
|
+
Example
|
|
201
|
+
```python
|
|
202
|
+
template.make_dir('/app/data', mode=0o755)
|
|
203
|
+
template.make_dir(['/app/logs', '/app/cache'])
|
|
204
|
+
template.make_dir('/app/data', mode=0o755, user='root')
|
|
205
|
+
```
|
|
206
|
+
"""
|
|
207
|
+
path_list = [path] if isinstance(path, (str, Path)) else path
|
|
208
|
+
args = ["mkdir", "-p"]
|
|
209
|
+
if mode:
|
|
210
|
+
args.append(f"-m {pad_octal(mode)}")
|
|
211
|
+
args.extend([str(p) for p in path_list])
|
|
212
|
+
|
|
213
|
+
return self._template._run_in_new_stack_trace_context(
|
|
214
|
+
lambda: self.run_cmd(" ".join(args), user=user)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def make_symlink(
|
|
218
|
+
self,
|
|
219
|
+
src: Union[str, Path],
|
|
220
|
+
dest: Union[str, Path],
|
|
221
|
+
user: Optional[str] = None,
|
|
222
|
+
force: bool = False,
|
|
223
|
+
) -> "TemplateBuilder":
|
|
224
|
+
"""
|
|
225
|
+
Create a symbolic link in the template.
|
|
226
|
+
|
|
227
|
+
:param src: Source path (target of the symlink)
|
|
228
|
+
:param dest: Destination path (location of the symlink)
|
|
229
|
+
:param user: User to run the command as
|
|
230
|
+
:param force: Force symlink without prompting
|
|
231
|
+
|
|
232
|
+
:return: `TemplateBuilder` class
|
|
233
|
+
|
|
234
|
+
Example
|
|
235
|
+
```python
|
|
236
|
+
template.make_symlink('/usr/bin/python3', '/usr/bin/python')
|
|
237
|
+
template.make_symlink('/usr/bin/python3', '/usr/bin/python', user='root')
|
|
238
|
+
template.make_symlink('/usr/bin/python3', '/usr/bin/python', force=True)
|
|
239
|
+
```
|
|
240
|
+
"""
|
|
241
|
+
args = ["ln", "-s"]
|
|
242
|
+
if force:
|
|
243
|
+
args.append("-f")
|
|
244
|
+
args.extend([str(src), str(dest)])
|
|
245
|
+
return self._template._run_in_new_stack_trace_context(
|
|
246
|
+
lambda: self.run_cmd(" ".join(args), user=user)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def run_cmd(
|
|
250
|
+
self, command: Union[str, List[str]], user: Optional[str] = None
|
|
251
|
+
) -> "TemplateBuilder":
|
|
252
|
+
"""
|
|
253
|
+
Run a shell command during template build.
|
|
254
|
+
|
|
255
|
+
:param command: Command string or list of commands to run (joined with &&)
|
|
256
|
+
:param user: User to run the command as
|
|
257
|
+
|
|
258
|
+
:return: `TemplateBuilder` class
|
|
259
|
+
|
|
260
|
+
Example
|
|
261
|
+
```python
|
|
262
|
+
template.run_cmd('apt-get update')
|
|
263
|
+
template.run_cmd(['pip install numpy', 'pip install pandas'])
|
|
264
|
+
template.run_cmd('apt-get install vim', user='root')
|
|
265
|
+
```
|
|
266
|
+
"""
|
|
267
|
+
commands = [command] if isinstance(command, str) else command
|
|
268
|
+
args = [" && ".join(commands)]
|
|
269
|
+
|
|
270
|
+
if user:
|
|
271
|
+
args.append(user)
|
|
272
|
+
|
|
273
|
+
instruction: Instruction = {
|
|
274
|
+
"type": InstructionType.RUN,
|
|
275
|
+
"args": args,
|
|
276
|
+
"force": self._template._force_next_layer,
|
|
277
|
+
"forceUpload": None,
|
|
278
|
+
}
|
|
279
|
+
self._template._instructions.append(instruction)
|
|
280
|
+
self._template._collect_stack_trace()
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def set_workdir(self, workdir: Union[str, Path]) -> "TemplateBuilder":
|
|
284
|
+
"""
|
|
285
|
+
Set the working directory for subsequent commands in the template.
|
|
286
|
+
|
|
287
|
+
:param workdir: Path to set as the working directory
|
|
288
|
+
|
|
289
|
+
:return: `TemplateBuilder` class
|
|
290
|
+
|
|
291
|
+
Example
|
|
292
|
+
```python
|
|
293
|
+
template.set_workdir('/app')
|
|
294
|
+
```
|
|
295
|
+
"""
|
|
296
|
+
instruction: Instruction = {
|
|
297
|
+
"type": InstructionType.WORKDIR,
|
|
298
|
+
"args": [str(workdir)],
|
|
299
|
+
"force": self._template._force_next_layer,
|
|
300
|
+
"forceUpload": None,
|
|
301
|
+
}
|
|
302
|
+
self._template._instructions.append(instruction)
|
|
303
|
+
self._template._collect_stack_trace()
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def set_user(self, user: str) -> "TemplateBuilder":
|
|
307
|
+
"""
|
|
308
|
+
Set the user for subsequent commands in the template.
|
|
309
|
+
|
|
310
|
+
:param user: Username to set
|
|
311
|
+
|
|
312
|
+
:return: `TemplateBuilder` class
|
|
313
|
+
|
|
314
|
+
Example
|
|
315
|
+
```python
|
|
316
|
+
template.set_user('root')
|
|
317
|
+
```
|
|
318
|
+
"""
|
|
319
|
+
instruction: Instruction = {
|
|
320
|
+
"type": InstructionType.USER,
|
|
321
|
+
"args": [user],
|
|
322
|
+
"force": self._template._force_next_layer,
|
|
323
|
+
"forceUpload": None,
|
|
324
|
+
}
|
|
325
|
+
self._template._instructions.append(instruction)
|
|
326
|
+
self._template._collect_stack_trace()
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def pip_install(
|
|
330
|
+
self, packages: Optional[Union[str, List[str]]] = None, g: bool = True
|
|
331
|
+
) -> "TemplateBuilder":
|
|
332
|
+
"""
|
|
333
|
+
Install Python packages using pip.
|
|
334
|
+
|
|
335
|
+
:param packages: Package name(s) to install. If None, runs 'pip install .' in the current directory
|
|
336
|
+
:param g: Install packages globally (default: True). If False, installs for user only
|
|
337
|
+
|
|
338
|
+
:return: `TemplateBuilder` class
|
|
339
|
+
|
|
340
|
+
Example
|
|
341
|
+
```python
|
|
342
|
+
template.pip_install('numpy')
|
|
343
|
+
template.pip_install(['pandas', 'scikit-learn'])
|
|
344
|
+
template.pip_install('numpy', g=False) # Install for user only
|
|
345
|
+
template.pip_install() # Installs from current directory
|
|
346
|
+
```
|
|
347
|
+
"""
|
|
348
|
+
if isinstance(packages, str):
|
|
349
|
+
packages = [packages]
|
|
350
|
+
|
|
351
|
+
args = ["pip", "install"]
|
|
352
|
+
if not g:
|
|
353
|
+
args.append("--user")
|
|
354
|
+
if packages:
|
|
355
|
+
args.extend(packages)
|
|
356
|
+
else:
|
|
357
|
+
args.append(".")
|
|
358
|
+
|
|
359
|
+
return self._template._run_in_new_stack_trace_context(
|
|
360
|
+
lambda: self.run_cmd(" ".join(args), user="root" if g else None)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def npm_install(
|
|
364
|
+
self,
|
|
365
|
+
packages: Optional[Union[str, List[str]]] = None,
|
|
366
|
+
g: Optional[bool] = False,
|
|
367
|
+
dev: Optional[bool] = False,
|
|
368
|
+
) -> "TemplateBuilder":
|
|
369
|
+
"""
|
|
370
|
+
Install Node.js packages using npm.
|
|
371
|
+
|
|
372
|
+
:param packages: Package name(s) to install. If None, installs from package.json
|
|
373
|
+
:param g: Install packages globally
|
|
374
|
+
:param dev: Install packages as dev dependencies
|
|
375
|
+
|
|
376
|
+
:return: `TemplateBuilder` class
|
|
377
|
+
|
|
378
|
+
Example
|
|
379
|
+
```python
|
|
380
|
+
template.npm_install('express')
|
|
381
|
+
template.npm_install(['lodash', 'axios'])
|
|
382
|
+
template.npm_install('typescript', g=True)
|
|
383
|
+
template.npm_install() # Installs from package.json
|
|
384
|
+
```
|
|
385
|
+
"""
|
|
386
|
+
if isinstance(packages, str):
|
|
387
|
+
packages = [packages]
|
|
388
|
+
|
|
389
|
+
args = ["npm", "install"]
|
|
390
|
+
if g:
|
|
391
|
+
args.append("-g")
|
|
392
|
+
if dev:
|
|
393
|
+
args.append("--save-dev")
|
|
394
|
+
if packages:
|
|
395
|
+
args.extend(packages)
|
|
396
|
+
|
|
397
|
+
return self._template._run_in_new_stack_trace_context(
|
|
398
|
+
lambda: self.run_cmd(" ".join(args), user="root" if g else None)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def bun_install(
|
|
402
|
+
self,
|
|
403
|
+
packages: Optional[Union[str, List[str]]] = None,
|
|
404
|
+
g: Optional[bool] = False,
|
|
405
|
+
dev: Optional[bool] = False,
|
|
406
|
+
) -> "TemplateBuilder":
|
|
407
|
+
"""
|
|
408
|
+
Install Bun packages using bun.
|
|
409
|
+
|
|
410
|
+
:param packages: Package name(s) to install. If None, installs from package.json
|
|
411
|
+
:param g: Install packages globally
|
|
412
|
+
:param dev: Install packages as dev dependencies
|
|
413
|
+
|
|
414
|
+
:return: `TemplateBuilder` class
|
|
415
|
+
|
|
416
|
+
Example
|
|
417
|
+
```python
|
|
418
|
+
template.bun_install('express')
|
|
419
|
+
template.bun_install(['lodash', 'axios'])
|
|
420
|
+
template.bun_install('tsx', g=True)
|
|
421
|
+
template.bun_install('typescript', dev=True)
|
|
422
|
+
template.bun_install() // Installs from package.json
|
|
423
|
+
```
|
|
424
|
+
"""
|
|
425
|
+
if isinstance(packages, str):
|
|
426
|
+
packages = [packages]
|
|
427
|
+
|
|
428
|
+
args = ["bun", "install"]
|
|
429
|
+
if g:
|
|
430
|
+
args.append("-g")
|
|
431
|
+
if dev:
|
|
432
|
+
args.append("--dev")
|
|
433
|
+
if packages:
|
|
434
|
+
args.extend(packages)
|
|
435
|
+
|
|
436
|
+
return self._template._run_in_new_stack_trace_context(
|
|
437
|
+
lambda: self.run_cmd(" ".join(args), user="root" if g else None)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def apt_install(
|
|
441
|
+
self, packages: Union[str, List[str]], no_install_recommends: bool = False
|
|
442
|
+
) -> "TemplateBuilder":
|
|
443
|
+
"""
|
|
444
|
+
Install system packages using apt-get.
|
|
445
|
+
|
|
446
|
+
:param packages: Package name(s) to install
|
|
447
|
+
:param no_install_recommends: Whether to install recommended packages
|
|
448
|
+
|
|
449
|
+
:return: `TemplateBuilder` class
|
|
450
|
+
|
|
451
|
+
Example
|
|
452
|
+
```python
|
|
453
|
+
template.apt_install('vim')
|
|
454
|
+
template.apt_install(['git', 'curl', 'wget'])
|
|
455
|
+
```
|
|
456
|
+
"""
|
|
457
|
+
if isinstance(packages, str):
|
|
458
|
+
packages = [packages]
|
|
459
|
+
|
|
460
|
+
return self._template._run_in_new_stack_trace_context(
|
|
461
|
+
lambda: self.run_cmd(
|
|
462
|
+
[
|
|
463
|
+
"apt-get update",
|
|
464
|
+
f"DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get install -y {'--no-install-recommends ' if no_install_recommends else ''}{' '.join(packages)}",
|
|
465
|
+
],
|
|
466
|
+
user="root",
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
def add_mcp_server(self, servers: Union[str, List[str]]) -> "TemplateBuilder":
|
|
471
|
+
"""
|
|
472
|
+
Install MCP servers using mcp-gateway.
|
|
473
|
+
|
|
474
|
+
Note: Requires a base image with mcp-gateway pre-installed (e.g., mcp-gateway).
|
|
475
|
+
|
|
476
|
+
:param servers: MCP server name(s)
|
|
477
|
+
|
|
478
|
+
:return: `TemplateBuilder` class
|
|
479
|
+
|
|
480
|
+
Example
|
|
481
|
+
```python
|
|
482
|
+
template.add_mcp_server('exa')
|
|
483
|
+
template.add_mcp_server(['brave', 'firecrawl', 'duckduckgo'])
|
|
484
|
+
```
|
|
485
|
+
"""
|
|
486
|
+
if self._template._base_template != "mcp-gateway":
|
|
487
|
+
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
|
|
488
|
+
stack_trace = None
|
|
489
|
+
if caller_frame is not None:
|
|
490
|
+
stack_trace = TracebackType(
|
|
491
|
+
tb_next=None,
|
|
492
|
+
tb_frame=caller_frame,
|
|
493
|
+
tb_lasti=caller_frame.f_lasti,
|
|
494
|
+
tb_lineno=caller_frame.f_lineno,
|
|
495
|
+
)
|
|
496
|
+
raise BuildException(
|
|
497
|
+
"MCP servers can only be added to mcp-gateway template"
|
|
498
|
+
).with_traceback(stack_trace)
|
|
499
|
+
|
|
500
|
+
if isinstance(servers, str):
|
|
501
|
+
servers = [servers]
|
|
502
|
+
|
|
503
|
+
return self._template._run_in_new_stack_trace_context(
|
|
504
|
+
lambda: self.run_cmd(f"mcp-gateway pull {' '.join(servers)}", user="root")
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def git_clone(
|
|
508
|
+
self,
|
|
509
|
+
url: str,
|
|
510
|
+
path: Optional[Union[str, Path]] = None,
|
|
511
|
+
branch: Optional[str] = None,
|
|
512
|
+
depth: Optional[int] = None,
|
|
513
|
+
user: Optional[str] = None,
|
|
514
|
+
) -> "TemplateBuilder":
|
|
515
|
+
"""
|
|
516
|
+
Clone a git repository into the template.
|
|
517
|
+
|
|
518
|
+
:param url: Git repository URL
|
|
519
|
+
:param path: Destination path for the clone
|
|
520
|
+
:param branch: Branch to clone
|
|
521
|
+
:param depth: Clone depth for shallow clones
|
|
522
|
+
:param user: User to run the command as
|
|
523
|
+
|
|
524
|
+
:return: `TemplateBuilder` class
|
|
525
|
+
|
|
526
|
+
Example
|
|
527
|
+
```python
|
|
528
|
+
template.git_clone('https://github.com/user/repo.git', '/app/repo')
|
|
529
|
+
template.git_clone('https://github.com/user/repo.git', branch='main', depth=1)
|
|
530
|
+
template.git_clone('https://github.com/user/repo.git', '/app/repo', user='root')
|
|
531
|
+
```
|
|
532
|
+
"""
|
|
533
|
+
args = ["git", "clone", url]
|
|
534
|
+
if branch:
|
|
535
|
+
args.append(f"--branch {branch}")
|
|
536
|
+
args.append("--single-branch")
|
|
537
|
+
if depth:
|
|
538
|
+
args.append(f"--depth {depth}")
|
|
539
|
+
if path:
|
|
540
|
+
args.append(str(path))
|
|
541
|
+
return self._template._run_in_new_stack_trace_context(
|
|
542
|
+
lambda: self.run_cmd(" ".join(args), user=user)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def beta_dev_container_prebuild(
|
|
546
|
+
self,
|
|
547
|
+
devcontainer_directory: Union[str, Path],
|
|
548
|
+
) -> "TemplateBuilder":
|
|
549
|
+
"""
|
|
550
|
+
Prebuild a devcontainer from the specified directory during the build process.
|
|
551
|
+
|
|
552
|
+
:param devcontainer_directory: Path to the devcontainer directory
|
|
553
|
+
|
|
554
|
+
:return: `TemplateBuilder` class
|
|
555
|
+
|
|
556
|
+
Example
|
|
557
|
+
```python
|
|
558
|
+
template.git_clone('https://myrepo.com/project.git', '/my-devcontainer')
|
|
559
|
+
template.beta_dev_container_prebuild('/my-devcontainer')
|
|
560
|
+
```
|
|
561
|
+
"""
|
|
562
|
+
if self._template._base_template != "devcontainer":
|
|
563
|
+
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
|
|
564
|
+
stack_trace = None
|
|
565
|
+
if caller_frame is not None:
|
|
566
|
+
stack_trace = TracebackType(
|
|
567
|
+
tb_next=None,
|
|
568
|
+
tb_frame=caller_frame,
|
|
569
|
+
tb_lasti=caller_frame.f_lasti,
|
|
570
|
+
tb_lineno=caller_frame.f_lineno,
|
|
571
|
+
)
|
|
572
|
+
raise BuildException(
|
|
573
|
+
"Devcontainers can only used in the devcontainer template"
|
|
574
|
+
).with_traceback(stack_trace)
|
|
575
|
+
|
|
576
|
+
return self._template._run_in_new_stack_trace_context(
|
|
577
|
+
lambda: self.run_cmd(
|
|
578
|
+
f"devcontainer build --workspace-folder {devcontainer_directory}",
|
|
579
|
+
user="root",
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def beta_set_dev_container_start(
|
|
584
|
+
self,
|
|
585
|
+
devcontainer_directory: Union[str, Path],
|
|
586
|
+
) -> "TemplateFinal":
|
|
587
|
+
"""
|
|
588
|
+
Start a devcontainer from the specified directory and set it as the start command.
|
|
589
|
+
|
|
590
|
+
This method returns `TemplateFinal`, which means it must be the last method in the chain.
|
|
591
|
+
|
|
592
|
+
:param devcontainer_directory: Path to the devcontainer directory
|
|
593
|
+
|
|
594
|
+
:return: `TemplateFinal` class
|
|
595
|
+
|
|
596
|
+
Example
|
|
597
|
+
```python
|
|
598
|
+
# Simple start
|
|
599
|
+
template.git_clone('https://myrepo.com/project.git', '/my-devcontainer')
|
|
600
|
+
template.beta_set_devcontainer_start('/my-devcontainer')
|
|
601
|
+
|
|
602
|
+
# With prebuild
|
|
603
|
+
template.git_clone('https://myrepo.com/project.git', '/my-devcontainer')
|
|
604
|
+
template.beta_dev_container_prebuild('/my-devcontainer')
|
|
605
|
+
template.beta_set_dev_container_start('/my-devcontainer')
|
|
606
|
+
```
|
|
607
|
+
"""
|
|
608
|
+
if self._template._base_template != "devcontainer":
|
|
609
|
+
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
|
|
610
|
+
stack_trace = None
|
|
611
|
+
if caller_frame is not None:
|
|
612
|
+
stack_trace = TracebackType(
|
|
613
|
+
tb_next=None,
|
|
614
|
+
tb_frame=caller_frame,
|
|
615
|
+
tb_lasti=caller_frame.f_lasti,
|
|
616
|
+
tb_lineno=caller_frame.f_lineno,
|
|
617
|
+
)
|
|
618
|
+
raise BuildException(
|
|
619
|
+
"Devcontainers can only used in the devcontainer template"
|
|
620
|
+
).with_traceback(stack_trace)
|
|
621
|
+
|
|
622
|
+
def _set_start():
|
|
623
|
+
return self.set_start_cmd(
|
|
624
|
+
"sudo devcontainer up --workspace-folder "
|
|
625
|
+
+ str(devcontainer_directory)
|
|
626
|
+
+ " && sudo /prepare-exec.sh "
|
|
627
|
+
+ str(devcontainer_directory)
|
|
628
|
+
+ " | sudo tee /devcontainer.sh > /dev/null && sudo chmod +x /devcontainer.sh && sudo touch /devcontainer.up",
|
|
629
|
+
wait_for_file("/devcontainer.up"),
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
return self._template._run_in_new_stack_trace_context(_set_start)
|
|
633
|
+
|
|
634
|
+
def set_envs(self, envs: Dict[str, str]) -> "TemplateBuilder":
|
|
635
|
+
"""
|
|
636
|
+
Set environment variables.
|
|
637
|
+
Note: Environment variables defined here are available only during template build.
|
|
638
|
+
|
|
639
|
+
:param envs: Dictionary of environment variable names and values
|
|
640
|
+
|
|
641
|
+
:return: `TemplateBuilder` class
|
|
642
|
+
|
|
643
|
+
Example
|
|
644
|
+
```python
|
|
645
|
+
template.set_envs({'NODE_ENV': 'production', 'PORT': '8080'})
|
|
646
|
+
```
|
|
647
|
+
"""
|
|
648
|
+
if len(envs) == 0:
|
|
649
|
+
return self
|
|
650
|
+
|
|
651
|
+
instruction: Instruction = {
|
|
652
|
+
"type": InstructionType.ENV,
|
|
653
|
+
"args": [item for key, value in envs.items() for item in [key, value]],
|
|
654
|
+
"force": self._template._force_next_layer,
|
|
655
|
+
"forceUpload": None,
|
|
656
|
+
}
|
|
657
|
+
self._template._instructions.append(instruction)
|
|
658
|
+
self._template._collect_stack_trace()
|
|
659
|
+
return self
|
|
660
|
+
|
|
661
|
+
def skip_cache(self) -> "TemplateBuilder":
|
|
662
|
+
"""
|
|
663
|
+
Skip cache for all subsequent build instructions from this point.
|
|
664
|
+
|
|
665
|
+
Call this before any instruction to force it and all following layers
|
|
666
|
+
to be rebuilt, ignoring any cached layers.
|
|
667
|
+
|
|
668
|
+
:return: `TemplateBuilder` class
|
|
669
|
+
|
|
670
|
+
Example
|
|
671
|
+
```python
|
|
672
|
+
template.skip_cache().run_cmd('apt-get update')
|
|
673
|
+
```
|
|
674
|
+
"""
|
|
675
|
+
self._template._force_next_layer = True
|
|
676
|
+
return self
|
|
677
|
+
|
|
678
|
+
def set_start_cmd(
|
|
679
|
+
self, start_cmd: str, ready_cmd: Union[str, ReadyCmd]
|
|
680
|
+
) -> "TemplateFinal":
|
|
681
|
+
"""
|
|
682
|
+
Set the command to start when the sandbox launches and the ready check command.
|
|
683
|
+
|
|
684
|
+
:param start_cmd: Command to run when the sandbox starts
|
|
685
|
+
:param ready_cmd: Command or ReadyCmd to check if the sandbox is ready
|
|
686
|
+
|
|
687
|
+
:return: `TemplateFinal` class
|
|
688
|
+
|
|
689
|
+
Example
|
|
690
|
+
```python
|
|
691
|
+
# Using a string command
|
|
692
|
+
template.set_start_cmd(
|
|
693
|
+
'python app.py',
|
|
694
|
+
'curl http://localhost:8000/health'
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Using ReadyCmd helpers
|
|
698
|
+
from moru import wait_for_port, wait_for_url
|
|
699
|
+
|
|
700
|
+
template.set_start_cmd(
|
|
701
|
+
'python -m http.server 8000',
|
|
702
|
+
wait_for_port(8000)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
template.set_start_cmd(
|
|
706
|
+
'npm start',
|
|
707
|
+
wait_for_url('http://localhost:3000/health', 200)
|
|
708
|
+
)
|
|
709
|
+
```
|
|
710
|
+
"""
|
|
711
|
+
self._template._start_cmd = start_cmd
|
|
712
|
+
|
|
713
|
+
if isinstance(ready_cmd, ReadyCmd):
|
|
714
|
+
ready_cmd = ready_cmd.get_cmd()
|
|
715
|
+
|
|
716
|
+
self._template._ready_cmd = ready_cmd
|
|
717
|
+
self._template._collect_stack_trace()
|
|
718
|
+
return TemplateFinal(self._template)
|
|
719
|
+
|
|
720
|
+
def set_ready_cmd(self, ready_cmd: Union[str, ReadyCmd]) -> "TemplateFinal":
|
|
721
|
+
"""
|
|
722
|
+
Set the command to check if the sandbox is ready.
|
|
723
|
+
|
|
724
|
+
:param ready_cmd: Command or ReadyCmd to check if the sandbox is ready
|
|
725
|
+
|
|
726
|
+
:return: `TemplateFinal` class
|
|
727
|
+
|
|
728
|
+
Example
|
|
729
|
+
```python
|
|
730
|
+
# Using a string command
|
|
731
|
+
template.set_ready_cmd('curl http://localhost:8000/health')
|
|
732
|
+
|
|
733
|
+
# Using ReadyCmd helpers
|
|
734
|
+
from moru import wait_for_port, wait_for_file, wait_for_process
|
|
735
|
+
|
|
736
|
+
template.set_ready_cmd(wait_for_port(3000))
|
|
737
|
+
|
|
738
|
+
template.set_ready_cmd(wait_for_file('/tmp/ready'))
|
|
739
|
+
|
|
740
|
+
template.set_ready_cmd(wait_for_process('nginx'))
|
|
741
|
+
```
|
|
742
|
+
"""
|
|
743
|
+
if isinstance(ready_cmd, ReadyCmd):
|
|
744
|
+
ready_cmd = ready_cmd.get_cmd()
|
|
745
|
+
|
|
746
|
+
self._template._ready_cmd = ready_cmd
|
|
747
|
+
self._template._collect_stack_trace()
|
|
748
|
+
return TemplateFinal(self._template)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class TemplateFinal:
|
|
752
|
+
"""
|
|
753
|
+
Final template state after start/ready commands are set.
|
|
754
|
+
"""
|
|
755
|
+
|
|
756
|
+
def __init__(self, template: "TemplateBase"):
|
|
757
|
+
self._template = template
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class TemplateBase:
|
|
761
|
+
"""
|
|
762
|
+
Base class for building Moru sandbox templates.
|
|
763
|
+
"""
|
|
764
|
+
|
|
765
|
+
_logs_refresh_frequency = 0.2
|
|
766
|
+
|
|
767
|
+
def __init__(
|
|
768
|
+
self,
|
|
769
|
+
file_context_path: Optional[Union[str, Path]] = None,
|
|
770
|
+
file_ignore_patterns: Optional[List[str]] = None,
|
|
771
|
+
):
|
|
772
|
+
"""
|
|
773
|
+
Create a new template builder instance.
|
|
774
|
+
|
|
775
|
+
:param file_context_path: Base path for resolving relative file paths in copy operations
|
|
776
|
+
:param file_ignore_patterns: List of glob patterns to ignore when copying files
|
|
777
|
+
"""
|
|
778
|
+
self._default_base_image: str = "ghcr.io/moru-ai/base"
|
|
779
|
+
self._base_image: Optional[str] = self._default_base_image
|
|
780
|
+
self._base_template: Optional[str] = None
|
|
781
|
+
self._registry_config: Optional[RegistryConfig] = None
|
|
782
|
+
self._start_cmd: Optional[str] = None
|
|
783
|
+
self._ready_cmd: Optional[str] = None
|
|
784
|
+
# Force the whole template to be rebuilt
|
|
785
|
+
self._force: bool = False
|
|
786
|
+
# Force the next layer to be rebuilt
|
|
787
|
+
self._force_next_layer: bool = False
|
|
788
|
+
self._instructions: List[Instruction] = []
|
|
789
|
+
# If no file_context_path is provided, use the caller's directory
|
|
790
|
+
self._file_context_path = (
|
|
791
|
+
file_context_path.as_posix()
|
|
792
|
+
if isinstance(file_context_path, Path)
|
|
793
|
+
else (file_context_path or get_caller_directory(STACK_TRACE_DEPTH) or ".")
|
|
794
|
+
)
|
|
795
|
+
self._file_ignore_patterns: List[str] = file_ignore_patterns or []
|
|
796
|
+
self._stack_traces: List[Union[TracebackType, None]] = []
|
|
797
|
+
self._stack_traces_enabled: bool = True
|
|
798
|
+
self._stack_traces_override: Optional[Union[TracebackType, None]] = None
|
|
799
|
+
|
|
800
|
+
def skip_cache(self) -> "TemplateBase":
|
|
801
|
+
"""
|
|
802
|
+
Skip cache for all subsequent build instructions from this point.
|
|
803
|
+
|
|
804
|
+
:return: `TemplateBase` class
|
|
805
|
+
|
|
806
|
+
Example
|
|
807
|
+
```python
|
|
808
|
+
template.skip_cache().from_python_image('3.11')
|
|
809
|
+
```
|
|
810
|
+
"""
|
|
811
|
+
self._force_next_layer = True
|
|
812
|
+
return self
|
|
813
|
+
|
|
814
|
+
def _collect_stack_trace(
|
|
815
|
+
self, stack_traces_depth: int = STACK_TRACE_DEPTH
|
|
816
|
+
) -> "TemplateBase":
|
|
817
|
+
"""
|
|
818
|
+
Collect the current stack trace for debugging purposes.
|
|
819
|
+
|
|
820
|
+
:param stack_traces_depth: Depth to traverse in the call stack
|
|
821
|
+
|
|
822
|
+
:return: `TemplateBase` class
|
|
823
|
+
"""
|
|
824
|
+
if not self._stack_traces_enabled:
|
|
825
|
+
return self
|
|
826
|
+
|
|
827
|
+
# Use the override if set, otherwise get the caller frame
|
|
828
|
+
if self._stack_traces_override is not None:
|
|
829
|
+
self._stack_traces.append(self._stack_traces_override)
|
|
830
|
+
return self
|
|
831
|
+
|
|
832
|
+
stack = get_caller_frame(stack_traces_depth)
|
|
833
|
+
if stack is None:
|
|
834
|
+
self._stack_traces.append(None)
|
|
835
|
+
return self
|
|
836
|
+
|
|
837
|
+
# Create a traceback object from the caller frame
|
|
838
|
+
capture_stack_trace = TracebackType(
|
|
839
|
+
tb_next=None,
|
|
840
|
+
tb_frame=stack,
|
|
841
|
+
tb_lasti=stack.f_lasti,
|
|
842
|
+
tb_lineno=stack.f_lineno,
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
self._stack_traces.append(capture_stack_trace)
|
|
846
|
+
return self
|
|
847
|
+
|
|
848
|
+
def _disable_stack_trace(self) -> "TemplateBase":
|
|
849
|
+
"""
|
|
850
|
+
Temporarily disable stack trace collection.
|
|
851
|
+
|
|
852
|
+
:return: `TemplateBase` class
|
|
853
|
+
"""
|
|
854
|
+
self._stack_traces_enabled = False
|
|
855
|
+
return self
|
|
856
|
+
|
|
857
|
+
def _enable_stack_trace(self) -> "TemplateBase":
|
|
858
|
+
"""
|
|
859
|
+
Re-enable stack trace collection.
|
|
860
|
+
|
|
861
|
+
:return: `TemplateBase` class
|
|
862
|
+
"""
|
|
863
|
+
self._stack_traces_enabled = True
|
|
864
|
+
return self
|
|
865
|
+
|
|
866
|
+
def _run_in_new_stack_trace_context(self, fn):
|
|
867
|
+
"""
|
|
868
|
+
Execute a function in a clean stack trace context.
|
|
869
|
+
|
|
870
|
+
:param fn: Function to execute
|
|
871
|
+
|
|
872
|
+
:return: The result of the function
|
|
873
|
+
"""
|
|
874
|
+
self._disable_stack_trace()
|
|
875
|
+
result = fn()
|
|
876
|
+
self._enable_stack_trace()
|
|
877
|
+
self._collect_stack_trace(STACK_TRACE_DEPTH + 1)
|
|
878
|
+
return result
|
|
879
|
+
|
|
880
|
+
def _run_in_stack_trace_override_context(
|
|
881
|
+
self, fn, stack_trace_override: Optional[Union[TracebackType, None]]
|
|
882
|
+
):
|
|
883
|
+
"""
|
|
884
|
+
Execute a function with a manual stack trace override.
|
|
885
|
+
|
|
886
|
+
:param fn: Function to execute
|
|
887
|
+
:param stack_trace_override: Stack trace to use instead of auto-collecting
|
|
888
|
+
|
|
889
|
+
:return: The result of the function
|
|
890
|
+
"""
|
|
891
|
+
self._stack_traces_override = stack_trace_override
|
|
892
|
+
result = fn()
|
|
893
|
+
self._stack_traces_override = None
|
|
894
|
+
return result
|
|
895
|
+
|
|
896
|
+
# Built-in image mixins
|
|
897
|
+
def from_debian_image(self, variant: str = "stable") -> TemplateBuilder:
|
|
898
|
+
"""
|
|
899
|
+
Start template from a Debian base image.
|
|
900
|
+
|
|
901
|
+
:param variant: Debian image variant
|
|
902
|
+
|
|
903
|
+
:return: `TemplateBuilder` class
|
|
904
|
+
|
|
905
|
+
Example
|
|
906
|
+
```python
|
|
907
|
+
Template().from_debian_image('bookworm')
|
|
908
|
+
```
|
|
909
|
+
"""
|
|
910
|
+
return self._run_in_new_stack_trace_context(
|
|
911
|
+
lambda: self.from_image(f"debian:{variant}")
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
def from_ubuntu_image(self, variant: str = "latest") -> TemplateBuilder:
|
|
915
|
+
"""
|
|
916
|
+
Start template from an Ubuntu base image.
|
|
917
|
+
|
|
918
|
+
:param variant: Ubuntu image variant (default: 'latest')
|
|
919
|
+
|
|
920
|
+
:return: `TemplateBuilder` class
|
|
921
|
+
|
|
922
|
+
Example
|
|
923
|
+
```python
|
|
924
|
+
Template().from_ubuntu_image('24.04')
|
|
925
|
+
```
|
|
926
|
+
"""
|
|
927
|
+
return self._run_in_new_stack_trace_context(
|
|
928
|
+
lambda: self.from_image(f"ubuntu:{variant}")
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
def from_python_image(self, version: str = "3") -> TemplateBuilder:
|
|
932
|
+
"""
|
|
933
|
+
Start template from a Python base image.
|
|
934
|
+
|
|
935
|
+
:param version: Python version (default: '3')
|
|
936
|
+
|
|
937
|
+
:return: `TemplateBuilder` class
|
|
938
|
+
|
|
939
|
+
Example
|
|
940
|
+
```python
|
|
941
|
+
Template().from_python_image('3')
|
|
942
|
+
```
|
|
943
|
+
"""
|
|
944
|
+
return self._run_in_new_stack_trace_context(
|
|
945
|
+
lambda: self.from_image(f"python:{version}")
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
def from_node_image(self, variant: str = "lts") -> TemplateBuilder:
|
|
949
|
+
"""
|
|
950
|
+
Start template from a Node.js base image.
|
|
951
|
+
|
|
952
|
+
:param variant: Node.js image variant (default: 'lts')
|
|
953
|
+
|
|
954
|
+
:return: `TemplateBuilder` class
|
|
955
|
+
|
|
956
|
+
Example
|
|
957
|
+
```python
|
|
958
|
+
Template().from_node_image('24')
|
|
959
|
+
```
|
|
960
|
+
"""
|
|
961
|
+
return self._run_in_new_stack_trace_context(
|
|
962
|
+
lambda: self.from_image(f"node:{variant}")
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
def from_bun_image(self, variant: str = "latest") -> TemplateBuilder:
|
|
966
|
+
"""
|
|
967
|
+
Start template from a Bun base image.
|
|
968
|
+
|
|
969
|
+
:param variant: Bun image variant (default: 'latest')
|
|
970
|
+
|
|
971
|
+
:return: `TemplateBuilder` class
|
|
972
|
+
"""
|
|
973
|
+
return self._run_in_new_stack_trace_context(
|
|
974
|
+
lambda: self.from_image(f"oven/bun:{variant}")
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
def from_base_image(self) -> TemplateBuilder:
|
|
978
|
+
"""
|
|
979
|
+
Start template from the Moru base image (ghcr.io/moru-ai/base:latest).
|
|
980
|
+
|
|
981
|
+
:return: `TemplateBuilder` class
|
|
982
|
+
|
|
983
|
+
Example
|
|
984
|
+
```python
|
|
985
|
+
Template().from_base_image()
|
|
986
|
+
```
|
|
987
|
+
"""
|
|
988
|
+
return self._run_in_new_stack_trace_context(
|
|
989
|
+
lambda: self.from_image(self._default_base_image)
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
def from_image(
|
|
993
|
+
self,
|
|
994
|
+
image: str,
|
|
995
|
+
username: Optional[str] = None,
|
|
996
|
+
password: Optional[str] = None,
|
|
997
|
+
) -> TemplateBuilder:
|
|
998
|
+
"""
|
|
999
|
+
Start template from a Docker image.
|
|
1000
|
+
|
|
1001
|
+
:param image: Docker image name (e.g., 'ubuntu:24.04')
|
|
1002
|
+
:param username: Username for private registry authentication
|
|
1003
|
+
:param password: Password for private registry authentication
|
|
1004
|
+
|
|
1005
|
+
:return: `TemplateBuilder` class
|
|
1006
|
+
|
|
1007
|
+
Example
|
|
1008
|
+
```python
|
|
1009
|
+
Template().from_image('python:3')
|
|
1010
|
+
|
|
1011
|
+
# With credentials (optional)
|
|
1012
|
+
Template().from_image('myregistry.com/myimage:latest', username='user', password='pass')
|
|
1013
|
+
```
|
|
1014
|
+
"""
|
|
1015
|
+
self._base_image = image
|
|
1016
|
+
self._base_template = None
|
|
1017
|
+
|
|
1018
|
+
# Set the registry config if provided
|
|
1019
|
+
if username and password:
|
|
1020
|
+
self._registry_config = {
|
|
1021
|
+
"type": "registry",
|
|
1022
|
+
"username": username,
|
|
1023
|
+
"password": password,
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
# If we should force the next layer and it's a FROM command, invalidate whole template
|
|
1027
|
+
if self._force_next_layer:
|
|
1028
|
+
self._force = True
|
|
1029
|
+
|
|
1030
|
+
self._collect_stack_trace()
|
|
1031
|
+
return TemplateBuilder(self)
|
|
1032
|
+
|
|
1033
|
+
def from_template(self, template: str) -> TemplateBuilder:
|
|
1034
|
+
"""
|
|
1035
|
+
Start template from an existing Moru template.
|
|
1036
|
+
|
|
1037
|
+
:param template: Moru template ID or alias
|
|
1038
|
+
|
|
1039
|
+
:return: `TemplateBuilder` class
|
|
1040
|
+
|
|
1041
|
+
Example
|
|
1042
|
+
```python
|
|
1043
|
+
Template().from_template('my-base-template')
|
|
1044
|
+
```
|
|
1045
|
+
"""
|
|
1046
|
+
self._base_template = template
|
|
1047
|
+
self._base_image = None
|
|
1048
|
+
|
|
1049
|
+
# If we should force the next layer and it's a FROM command, invalidate whole template
|
|
1050
|
+
if self._force_next_layer:
|
|
1051
|
+
self._force = True
|
|
1052
|
+
|
|
1053
|
+
self._collect_stack_trace()
|
|
1054
|
+
return TemplateBuilder(self)
|
|
1055
|
+
|
|
1056
|
+
def from_dockerfile(self, dockerfile_content_or_path: str) -> TemplateBuilder:
|
|
1057
|
+
"""
|
|
1058
|
+
Parse a Dockerfile and convert it to Template SDK format.
|
|
1059
|
+
|
|
1060
|
+
:param dockerfile_content_or_path: Either the Dockerfile content as a string, or a path to a Dockerfile file
|
|
1061
|
+
|
|
1062
|
+
:return: `TemplateBuilder` class
|
|
1063
|
+
|
|
1064
|
+
Example
|
|
1065
|
+
```python
|
|
1066
|
+
Template().from_dockerfile('Dockerfile')
|
|
1067
|
+
Template().from_dockerfile('FROM python:3\\nRUN pip install numpy')
|
|
1068
|
+
```
|
|
1069
|
+
"""
|
|
1070
|
+
# Create a TemplateBuilder first to use its methods
|
|
1071
|
+
builder = TemplateBuilder(self)
|
|
1072
|
+
|
|
1073
|
+
# Get the caller frame to use for stack trace override
|
|
1074
|
+
# -1 as we're going up the call stack from the parse_dockerfile function
|
|
1075
|
+
caller_frame = get_caller_frame(STACK_TRACE_DEPTH - 1)
|
|
1076
|
+
stack_trace_override = None
|
|
1077
|
+
if caller_frame is not None:
|
|
1078
|
+
stack_trace_override = TracebackType(
|
|
1079
|
+
tb_next=None,
|
|
1080
|
+
tb_frame=caller_frame,
|
|
1081
|
+
tb_lasti=caller_frame.f_lasti,
|
|
1082
|
+
tb_lineno=caller_frame.f_lineno,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
# Parse the dockerfile using the builder as the interface
|
|
1086
|
+
base_image = self._run_in_stack_trace_override_context(
|
|
1087
|
+
lambda: parse_dockerfile(dockerfile_content_or_path, builder),
|
|
1088
|
+
stack_trace_override,
|
|
1089
|
+
)
|
|
1090
|
+
self._base_image = base_image
|
|
1091
|
+
|
|
1092
|
+
# If we should force the next layer and it's a FROM command, invalidate whole template
|
|
1093
|
+
if self._force_next_layer:
|
|
1094
|
+
self._force = True
|
|
1095
|
+
|
|
1096
|
+
self._collect_stack_trace()
|
|
1097
|
+
return builder
|
|
1098
|
+
|
|
1099
|
+
def from_aws_registry(
|
|
1100
|
+
self,
|
|
1101
|
+
image: str,
|
|
1102
|
+
access_key_id: str,
|
|
1103
|
+
secret_access_key: str,
|
|
1104
|
+
region: str,
|
|
1105
|
+
) -> TemplateBuilder:
|
|
1106
|
+
"""
|
|
1107
|
+
Start template from an AWS ECR registry image.
|
|
1108
|
+
|
|
1109
|
+
:param image: Docker image name from AWS ECR
|
|
1110
|
+
:param access_key_id: AWS access key ID
|
|
1111
|
+
:param secret_access_key: AWS secret access key
|
|
1112
|
+
:param region: AWS region
|
|
1113
|
+
|
|
1114
|
+
:return: `TemplateBuilder` class
|
|
1115
|
+
|
|
1116
|
+
Example
|
|
1117
|
+
```python
|
|
1118
|
+
Template().from_aws_registry(
|
|
1119
|
+
'123456789.dkr.ecr.us-west-2.amazonaws.com/myimage:latest',
|
|
1120
|
+
access_key_id='AKIA...',
|
|
1121
|
+
secret_access_key='...',
|
|
1122
|
+
region='us-west-2'
|
|
1123
|
+
)
|
|
1124
|
+
```
|
|
1125
|
+
"""
|
|
1126
|
+
self._base_image = image
|
|
1127
|
+
self._base_template = None
|
|
1128
|
+
|
|
1129
|
+
# Set the registry config if provided
|
|
1130
|
+
self._registry_config = {
|
|
1131
|
+
"type": "aws",
|
|
1132
|
+
"awsAccessKeyId": access_key_id,
|
|
1133
|
+
"awsSecretAccessKey": secret_access_key,
|
|
1134
|
+
"awsRegion": region,
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
# If we should force the next layer and it's a FROM command, invalidate whole template
|
|
1138
|
+
if self._force_next_layer:
|
|
1139
|
+
self._force = True
|
|
1140
|
+
|
|
1141
|
+
self._collect_stack_trace()
|
|
1142
|
+
return TemplateBuilder(self)
|
|
1143
|
+
|
|
1144
|
+
def from_gcp_registry(
|
|
1145
|
+
self, image: str, service_account_json: Union[str, dict]
|
|
1146
|
+
) -> TemplateBuilder:
|
|
1147
|
+
"""
|
|
1148
|
+
Start template from a GCP Artifact Registry or Container Registry image.
|
|
1149
|
+
|
|
1150
|
+
:param image: Docker image name from GCP registry
|
|
1151
|
+
:param service_account_json: Service account JSON string, dict, or path to JSON file
|
|
1152
|
+
|
|
1153
|
+
:return: `TemplateBuilder` class
|
|
1154
|
+
|
|
1155
|
+
Example
|
|
1156
|
+
```python
|
|
1157
|
+
Template().from_gcp_registry(
|
|
1158
|
+
'gcr.io/myproject/myimage:latest',
|
|
1159
|
+
service_account_json='path/to/service-account.json'
|
|
1160
|
+
)
|
|
1161
|
+
```
|
|
1162
|
+
"""
|
|
1163
|
+
self._base_image = image
|
|
1164
|
+
self._base_template = None
|
|
1165
|
+
|
|
1166
|
+
# Set the registry config if provided
|
|
1167
|
+
self._registry_config = {
|
|
1168
|
+
"type": "gcp",
|
|
1169
|
+
"serviceAccountJson": read_gcp_service_account_json(
|
|
1170
|
+
self._file_context_path, service_account_json
|
|
1171
|
+
),
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
# If we should force the next layer and it's a FROM command, invalidate whole template
|
|
1175
|
+
if self._force_next_layer:
|
|
1176
|
+
self._force = True
|
|
1177
|
+
|
|
1178
|
+
self._collect_stack_trace()
|
|
1179
|
+
return TemplateBuilder(self)
|
|
1180
|
+
|
|
1181
|
+
@staticmethod
|
|
1182
|
+
def to_json(template: "TemplateClass") -> str:
|
|
1183
|
+
"""
|
|
1184
|
+
Convert a template to JSON representation.
|
|
1185
|
+
|
|
1186
|
+
:param template: The template to convert (TemplateBuilder or TemplateFinal instance)
|
|
1187
|
+
|
|
1188
|
+
:return: JSON string representation of the template
|
|
1189
|
+
|
|
1190
|
+
Example
|
|
1191
|
+
```python
|
|
1192
|
+
template = Template().from_python_image('3').copy('app.py', '/app/')
|
|
1193
|
+
json_str = TemplateBase.to_json(template)
|
|
1194
|
+
```
|
|
1195
|
+
"""
|
|
1196
|
+
return json.dumps(
|
|
1197
|
+
template._template._serialize(
|
|
1198
|
+
template._template._instructions_with_hashes()
|
|
1199
|
+
),
|
|
1200
|
+
indent=2,
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
@staticmethod
|
|
1204
|
+
def to_dockerfile(template: "TemplateClass") -> str:
|
|
1205
|
+
"""
|
|
1206
|
+
Convert a template to Dockerfile format.
|
|
1207
|
+
|
|
1208
|
+
Note: Templates based on other Moru templates cannot be converted to Dockerfile.
|
|
1209
|
+
|
|
1210
|
+
:param template: The template to convert (TemplateBuilder or TemplateFinal instance)
|
|
1211
|
+
|
|
1212
|
+
:return: Dockerfile string representation
|
|
1213
|
+
|
|
1214
|
+
:raises ValueError: If the template is based on another Moru template or has no base image
|
|
1215
|
+
|
|
1216
|
+
Example
|
|
1217
|
+
```python
|
|
1218
|
+
template = Template().from_python_image('3').copy('app.py', '/app/')
|
|
1219
|
+
dockerfile = TemplateBase.to_dockerfile(template)
|
|
1220
|
+
```
|
|
1221
|
+
"""
|
|
1222
|
+
if template._template._base_template is not None:
|
|
1223
|
+
raise ValueError(
|
|
1224
|
+
"Cannot convert template built from another template to Dockerfile. "
|
|
1225
|
+
"Templates based on other templates can only be built using the Moru API."
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
if template._template._base_image is None:
|
|
1229
|
+
raise ValueError("No base image specified for template")
|
|
1230
|
+
|
|
1231
|
+
dockerfile = f"FROM {template._template._base_image}\n"
|
|
1232
|
+
|
|
1233
|
+
for instruction in template._template._instructions:
|
|
1234
|
+
if instruction["type"] == InstructionType.RUN:
|
|
1235
|
+
dockerfile += f"RUN {instruction['args'][0]}\n"
|
|
1236
|
+
continue
|
|
1237
|
+
|
|
1238
|
+
if instruction["type"] == InstructionType.COPY:
|
|
1239
|
+
dockerfile += (
|
|
1240
|
+
f"COPY {instruction['args'][0]} {instruction['args'][1]}\n"
|
|
1241
|
+
)
|
|
1242
|
+
continue
|
|
1243
|
+
|
|
1244
|
+
if instruction["type"] == InstructionType.ENV:
|
|
1245
|
+
args = instruction["args"]
|
|
1246
|
+
values = []
|
|
1247
|
+
for i in range(0, len(args), 2):
|
|
1248
|
+
values.append(f"{args[i]}={args[i + 1]}")
|
|
1249
|
+
dockerfile += f"ENV {' '.join(values)}\n"
|
|
1250
|
+
continue
|
|
1251
|
+
|
|
1252
|
+
dockerfile += (
|
|
1253
|
+
f"{instruction['type'].value} {' '.join(instruction['args'])}\n"
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
if template._template._start_cmd:
|
|
1257
|
+
dockerfile += f"ENTRYPOINT {template._template._start_cmd}\n"
|
|
1258
|
+
|
|
1259
|
+
return dockerfile
|
|
1260
|
+
|
|
1261
|
+
def _instructions_with_hashes(
|
|
1262
|
+
self,
|
|
1263
|
+
) -> List[Instruction]:
|
|
1264
|
+
"""
|
|
1265
|
+
Add file hashes to COPY instructions for cache invalidation.
|
|
1266
|
+
|
|
1267
|
+
:return: Copy of instructions list with filesHash added to COPY instructions
|
|
1268
|
+
"""
|
|
1269
|
+
steps: List[Instruction] = []
|
|
1270
|
+
|
|
1271
|
+
for index, instruction in enumerate(self._instructions):
|
|
1272
|
+
step: Instruction = {
|
|
1273
|
+
"type": instruction["type"],
|
|
1274
|
+
"args": instruction["args"],
|
|
1275
|
+
"force": instruction["force"],
|
|
1276
|
+
"forceUpload": instruction.get("forceUpload"),
|
|
1277
|
+
"resolveSymlinks": instruction.get("resolveSymlinks"),
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if instruction["type"] == InstructionType.COPY:
|
|
1281
|
+
stack_trace = None
|
|
1282
|
+
if index + 1 < len(self._stack_traces):
|
|
1283
|
+
stack_trace = self._stack_traces[index + 1]
|
|
1284
|
+
|
|
1285
|
+
args = instruction.get("args", [])
|
|
1286
|
+
src = args[0] if len(args) > 0 else None
|
|
1287
|
+
dest = args[1] if len(args) > 1 else None
|
|
1288
|
+
if src is None or dest is None:
|
|
1289
|
+
raise ValueError("Source path and destination path are required")
|
|
1290
|
+
|
|
1291
|
+
resolve_symlinks = instruction.get("resolveSymlinks")
|
|
1292
|
+
step["filesHash"] = calculate_files_hash(
|
|
1293
|
+
src,
|
|
1294
|
+
dest,
|
|
1295
|
+
self._file_context_path,
|
|
1296
|
+
[
|
|
1297
|
+
*self._file_ignore_patterns,
|
|
1298
|
+
*read_dockerignore(self._file_context_path),
|
|
1299
|
+
],
|
|
1300
|
+
resolve_symlinks
|
|
1301
|
+
if resolve_symlinks is not None
|
|
1302
|
+
else RESOLVE_SYMLINKS,
|
|
1303
|
+
stack_trace,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
steps.append(step)
|
|
1307
|
+
|
|
1308
|
+
return steps
|
|
1309
|
+
|
|
1310
|
+
def _serialize(self, steps: List[Instruction]) -> TemplateType:
|
|
1311
|
+
"""
|
|
1312
|
+
Serialize the template to the API request format.
|
|
1313
|
+
|
|
1314
|
+
:param steps: List of build instructions with file hashes
|
|
1315
|
+
|
|
1316
|
+
:return: Template data formatted for the API
|
|
1317
|
+
"""
|
|
1318
|
+
_steps: List[Instruction] = []
|
|
1319
|
+
|
|
1320
|
+
for _, instruction in enumerate(steps):
|
|
1321
|
+
step: Instruction = {
|
|
1322
|
+
"type": instruction.get("type"),
|
|
1323
|
+
"args": instruction.get("args"),
|
|
1324
|
+
"force": instruction.get("force"),
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
files_hash = instruction.get("filesHash")
|
|
1328
|
+
if files_hash is not None:
|
|
1329
|
+
step["filesHash"] = files_hash
|
|
1330
|
+
|
|
1331
|
+
force_upload = instruction.get("forceUpload")
|
|
1332
|
+
if force_upload is not None:
|
|
1333
|
+
step["forceUpload"] = force_upload
|
|
1334
|
+
|
|
1335
|
+
_steps.append(step)
|
|
1336
|
+
|
|
1337
|
+
template_data: TemplateType = {
|
|
1338
|
+
"steps": _steps,
|
|
1339
|
+
"force": self._force,
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if self._base_image is not None:
|
|
1343
|
+
template_data["fromImage"] = self._base_image
|
|
1344
|
+
|
|
1345
|
+
if self._base_template is not None:
|
|
1346
|
+
template_data["fromTemplate"] = self._base_template
|
|
1347
|
+
|
|
1348
|
+
if self._registry_config is not None:
|
|
1349
|
+
template_data["fromImageRegistry"] = self._registry_config
|
|
1350
|
+
|
|
1351
|
+
if self._start_cmd is not None:
|
|
1352
|
+
template_data["startCmd"] = self._start_cmd
|
|
1353
|
+
|
|
1354
|
+
if self._ready_cmd is not None:
|
|
1355
|
+
template_data["readyCmd"] = self._ready_cmd
|
|
1356
|
+
|
|
1357
|
+
return template_data
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
TemplateClass = Union[TemplateFinal, TemplateBuilder]
|