runnable 0.12.3__py3-none-any.whl → 0.14.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.
- runnable/__init__.py +0 -11
- runnable/catalog.py +27 -5
- runnable/cli.py +122 -26
- runnable/datastore.py +71 -35
- runnable/defaults.py +0 -1
- runnable/entrypoints.py +107 -32
- runnable/exceptions.py +6 -2
- runnable/executor.py +28 -9
- runnable/graph.py +37 -12
- runnable/integration.py +7 -2
- runnable/nodes.py +15 -17
- runnable/parameters.py +27 -8
- runnable/pickler.py +1 -1
- runnable/sdk.py +101 -33
- runnable/secrets.py +3 -1
- runnable/tasks.py +246 -34
- runnable/utils.py +41 -13
- {runnable-0.12.3.dist-info → runnable-0.14.0.dist-info}/METADATA +25 -31
- runnable-0.14.0.dist-info/RECORD +24 -0
- {runnable-0.12.3.dist-info → runnable-0.14.0.dist-info}/WHEEL +1 -1
- runnable-0.14.0.dist-info/entry_points.txt +40 -0
- runnable/extensions/__init__.py +0 -0
- runnable/extensions/catalog/__init__.py +0 -21
- runnable/extensions/catalog/file_system/__init__.py +0 -0
- runnable/extensions/catalog/file_system/implementation.py +0 -234
- runnable/extensions/catalog/k8s_pvc/__init__.py +0 -0
- runnable/extensions/catalog/k8s_pvc/implementation.py +0 -16
- runnable/extensions/catalog/k8s_pvc/integration.py +0 -59
- runnable/extensions/executor/__init__.py +0 -649
- runnable/extensions/executor/argo/__init__.py +0 -0
- runnable/extensions/executor/argo/implementation.py +0 -1194
- runnable/extensions/executor/argo/specification.yaml +0 -51
- runnable/extensions/executor/k8s_job/__init__.py +0 -0
- runnable/extensions/executor/k8s_job/implementation_FF.py +0 -259
- runnable/extensions/executor/k8s_job/integration_FF.py +0 -69
- runnable/extensions/executor/local/__init__.py +0 -0
- runnable/extensions/executor/local/implementation.py +0 -71
- runnable/extensions/executor/local_container/__init__.py +0 -0
- runnable/extensions/executor/local_container/implementation.py +0 -446
- runnable/extensions/executor/mocked/__init__.py +0 -0
- runnable/extensions/executor/mocked/implementation.py +0 -154
- runnable/extensions/executor/retry/__init__.py +0 -0
- runnable/extensions/executor/retry/implementation.py +0 -168
- runnable/extensions/nodes.py +0 -855
- runnable/extensions/run_log_store/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_file_system/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_file_system/implementation.py +0 -111
- runnable/extensions/run_log_store/chunked_k8s_pvc/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_k8s_pvc/implementation.py +0 -21
- runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py +0 -61
- runnable/extensions/run_log_store/db/implementation_FF.py +0 -157
- runnable/extensions/run_log_store/db/integration_FF.py +0 -0
- runnable/extensions/run_log_store/file_system/__init__.py +0 -0
- runnable/extensions/run_log_store/file_system/implementation.py +0 -140
- runnable/extensions/run_log_store/generic_chunked.py +0 -557
- runnable/extensions/run_log_store/k8s_pvc/__init__.py +0 -0
- runnable/extensions/run_log_store/k8s_pvc/implementation.py +0 -21
- runnable/extensions/run_log_store/k8s_pvc/integration.py +0 -56
- runnable/extensions/secrets/__init__.py +0 -0
- runnable/extensions/secrets/dotenv/__init__.py +0 -0
- runnable/extensions/secrets/dotenv/implementation.py +0 -100
- runnable-0.12.3.dist-info/RECORD +0 -64
- runnable-0.12.3.dist-info/entry_points.txt +0 -41
- {runnable-0.12.3.dist-info → runnable-0.14.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,1194 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import logging
|
3
|
-
import random
|
4
|
-
import shlex
|
5
|
-
import string
|
6
|
-
from abc import ABC, abstractmethod
|
7
|
-
from collections import OrderedDict
|
8
|
-
from typing import Dict, List, Optional, Union, cast
|
9
|
-
|
10
|
-
from pydantic import (
|
11
|
-
BaseModel,
|
12
|
-
ConfigDict,
|
13
|
-
Field,
|
14
|
-
computed_field,
|
15
|
-
field_serializer,
|
16
|
-
field_validator,
|
17
|
-
)
|
18
|
-
from pydantic.functional_serializers import PlainSerializer
|
19
|
-
from ruamel.yaml import YAML
|
20
|
-
from typing_extensions import Annotated
|
21
|
-
|
22
|
-
from runnable import defaults, exceptions, integration, utils
|
23
|
-
from runnable.defaults import TypeMapVariable
|
24
|
-
from runnable.extensions.executor import GenericExecutor
|
25
|
-
from runnable.extensions.nodes import DagNode, MapNode, ParallelNode
|
26
|
-
from runnable.graph import Graph, create_node, search_node_by_internal_name
|
27
|
-
from runnable.integration import BaseIntegration
|
28
|
-
from runnable.nodes import BaseNode
|
29
|
-
|
30
|
-
logger = logging.getLogger(defaults.NAME)
|
31
|
-
|
32
|
-
# TODO: Leave the run log in consistent state.
|
33
|
-
|
34
|
-
"""
|
35
|
-
executor:
|
36
|
-
type: argo
|
37
|
-
config:
|
38
|
-
image: # apply to template
|
39
|
-
max_workflow_duration: # Apply to spec
|
40
|
-
nodeSelector: #Apply to spec
|
41
|
-
parallelism: #apply to spec
|
42
|
-
resources: # convert to podSpecPath
|
43
|
-
limits:
|
44
|
-
requests:
|
45
|
-
retryStrategy:
|
46
|
-
max_step_duration: # apply to templateDefaults
|
47
|
-
step_timeout: # apply to templateDefaults
|
48
|
-
tolerations: # apply to spec
|
49
|
-
imagePullPolicy: # apply to template
|
50
|
-
|
51
|
-
overrides:
|
52
|
-
override:
|
53
|
-
tolerations: # template
|
54
|
-
image: # container
|
55
|
-
max_step_duration: # template
|
56
|
-
step_timeout: #template
|
57
|
-
nodeSelector: #template
|
58
|
-
parallelism: # this need to applied for map
|
59
|
-
resources: # container
|
60
|
-
imagePullPolicy: #container
|
61
|
-
retryStrategy: # template
|
62
|
-
"""
|
63
|
-
|
64
|
-
|
65
|
-
class SecretEnvVar(BaseModel):
|
66
|
-
"""
|
67
|
-
Renders:
|
68
|
-
env:
|
69
|
-
- name: MYSECRETPASSWORD
|
70
|
-
valueFrom:
|
71
|
-
secretKeyRef:
|
72
|
-
name: my-secret
|
73
|
-
key: mypassword
|
74
|
-
"""
|
75
|
-
|
76
|
-
environment_variable: str = Field(serialization_alias="name")
|
77
|
-
secret_name: str = Field(exclude=True)
|
78
|
-
secret_key: str = Field(exclude=True)
|
79
|
-
|
80
|
-
@computed_field # type: ignore
|
81
|
-
@property
|
82
|
-
def valueFrom(self) -> Dict[str, Dict[str, str]]:
|
83
|
-
return {
|
84
|
-
"secretKeyRef": {
|
85
|
-
"name": self.secret_name,
|
86
|
-
"key": self.secret_key,
|
87
|
-
}
|
88
|
-
}
|
89
|
-
|
90
|
-
|
91
|
-
class EnvVar(BaseModel):
|
92
|
-
"""
|
93
|
-
Renders:
|
94
|
-
parameters: # in arguments
|
95
|
-
- name: x
|
96
|
-
value: 3 # This is optional for workflow parameters
|
97
|
-
|
98
|
-
"""
|
99
|
-
|
100
|
-
name: str
|
101
|
-
value: Union[str, int, float] = Field(default="")
|
102
|
-
|
103
|
-
|
104
|
-
class Parameter(BaseModel):
|
105
|
-
name: str
|
106
|
-
value: Optional[str] = None
|
107
|
-
|
108
|
-
@field_serializer("name")
|
109
|
-
def serialize_name(self, name: str) -> str:
|
110
|
-
return f"{str(name)}"
|
111
|
-
|
112
|
-
@field_serializer("value")
|
113
|
-
def serialize_value(self, value: str) -> str:
|
114
|
-
return f"{value}"
|
115
|
-
|
116
|
-
|
117
|
-
class OutputParameter(Parameter):
|
118
|
-
"""
|
119
|
-
Renders:
|
120
|
-
- name: step-name
|
121
|
-
valueFrom:
|
122
|
-
path: /tmp/output.txt
|
123
|
-
"""
|
124
|
-
|
125
|
-
path: str = Field(default="/tmp/output.txt", exclude=True)
|
126
|
-
|
127
|
-
@computed_field # type: ignore
|
128
|
-
@property
|
129
|
-
def valueFrom(self) -> Dict[str, str]:
|
130
|
-
return {"path": self.path}
|
131
|
-
|
132
|
-
|
133
|
-
class Argument(BaseModel):
|
134
|
-
"""
|
135
|
-
Templates are called with arguments, which become inputs for the template
|
136
|
-
Renders:
|
137
|
-
arguments:
|
138
|
-
parameters:
|
139
|
-
- name: The name of the parameter
|
140
|
-
value: The value of the parameter
|
141
|
-
"""
|
142
|
-
|
143
|
-
name: str
|
144
|
-
value: str
|
145
|
-
|
146
|
-
@field_serializer("name")
|
147
|
-
def serialize_name(self, name: str) -> str:
|
148
|
-
return f"{str(name)}"
|
149
|
-
|
150
|
-
@field_serializer("value")
|
151
|
-
def serialize_value(self, value: str) -> str:
|
152
|
-
return f"{value}"
|
153
|
-
|
154
|
-
|
155
|
-
class Request(BaseModel):
|
156
|
-
"""
|
157
|
-
The default requests
|
158
|
-
"""
|
159
|
-
|
160
|
-
memory: str = "1Gi"
|
161
|
-
cpu: str = "250m"
|
162
|
-
|
163
|
-
|
164
|
-
VendorGPU = Annotated[
|
165
|
-
Optional[int],
|
166
|
-
PlainSerializer(lambda x: str(x), return_type=str, when_used="unless-none"),
|
167
|
-
]
|
168
|
-
|
169
|
-
|
170
|
-
class Limit(Request):
|
171
|
-
"""
|
172
|
-
The default limits
|
173
|
-
"""
|
174
|
-
|
175
|
-
gpu: VendorGPU = Field(default=None, serialization_alias="nvidia.com/gpu")
|
176
|
-
|
177
|
-
|
178
|
-
class Resources(BaseModel):
|
179
|
-
limits: Limit = Field(default=Limit(), serialization_alias="limits")
|
180
|
-
requests: Request = Field(default=Request(), serialization_alias="requests")
|
181
|
-
|
182
|
-
|
183
|
-
class BackOff(BaseModel):
|
184
|
-
duration_in_seconds: int = Field(default=2 * 60, serialization_alias="duration")
|
185
|
-
factor: float = Field(default=2, serialization_alias="factor")
|
186
|
-
max_duration: int = Field(default=60 * 60, serialization_alias="maxDuration")
|
187
|
-
|
188
|
-
@field_serializer("duration_in_seconds")
|
189
|
-
def cast_duration_as_str(self, duration_in_seconds: int, _info) -> str:
|
190
|
-
return str(duration_in_seconds)
|
191
|
-
|
192
|
-
@field_serializer("max_duration")
|
193
|
-
def cast_mas_duration_as_str(self, max_duration: int, _info) -> str:
|
194
|
-
return str(max_duration)
|
195
|
-
|
196
|
-
|
197
|
-
class Retry(BaseModel):
|
198
|
-
limit: int = 0
|
199
|
-
retry_policy: str = Field(default="Always", serialization_alias="retryPolicy")
|
200
|
-
back_off: BackOff = Field(default=BackOff(), serialization_alias="backoff")
|
201
|
-
|
202
|
-
@field_serializer("limit")
|
203
|
-
def cast_limit_as_str(self, limit: int, _info) -> str:
|
204
|
-
return str(limit)
|
205
|
-
|
206
|
-
|
207
|
-
class Toleration(BaseModel):
|
208
|
-
effect: str
|
209
|
-
key: str
|
210
|
-
operator: str
|
211
|
-
value: str
|
212
|
-
|
213
|
-
|
214
|
-
class TemplateDefaults(BaseModel):
|
215
|
-
max_step_duration: int = Field(
|
216
|
-
default=60 * 60 * 2,
|
217
|
-
serialization_alias="activeDeadlineSeconds",
|
218
|
-
gt=0,
|
219
|
-
description="Max run time of a step",
|
220
|
-
)
|
221
|
-
|
222
|
-
@computed_field # type: ignore
|
223
|
-
@property
|
224
|
-
def timeout(self) -> str:
|
225
|
-
return f"{self.max_step_duration + 60*60}s"
|
226
|
-
|
227
|
-
|
228
|
-
ShlexCommand = Annotated[str, PlainSerializer(lambda x: shlex.split(x), return_type=List[str])]
|
229
|
-
|
230
|
-
|
231
|
-
class Container(BaseModel):
|
232
|
-
image: str
|
233
|
-
command: ShlexCommand
|
234
|
-
volume_mounts: Optional[List["ContainerVolume"]] = Field(default=None, serialization_alias="volumeMounts")
|
235
|
-
image_pull_policy: str = Field(default="", serialization_alias="imagePullPolicy")
|
236
|
-
resources: Optional[Resources] = Field(default=None, serialization_alias="resources")
|
237
|
-
|
238
|
-
env_vars: List[EnvVar] = Field(default_factory=list, exclude=True)
|
239
|
-
secrets_from_k8s: List[SecretEnvVar] = Field(default_factory=list, exclude=True)
|
240
|
-
|
241
|
-
@computed_field # type: ignore
|
242
|
-
@property
|
243
|
-
def env(self) -> Optional[List[Union[EnvVar, SecretEnvVar]]]:
|
244
|
-
if not self.env_vars and not self.secrets_from_k8s:
|
245
|
-
return None
|
246
|
-
|
247
|
-
return self.env_vars + self.secrets_from_k8s
|
248
|
-
|
249
|
-
|
250
|
-
class DagTaskTemplate(BaseModel):
|
251
|
-
"""
|
252
|
-
dag:
|
253
|
-
tasks:
|
254
|
-
name: A
|
255
|
-
template: nested-diamond
|
256
|
-
arguments:
|
257
|
-
parameters: [{name: message, value: A}]
|
258
|
-
"""
|
259
|
-
|
260
|
-
name: str
|
261
|
-
template: str
|
262
|
-
depends: List[str] = []
|
263
|
-
arguments: Optional[List[Argument]] = Field(default=None)
|
264
|
-
with_param: Optional[str] = Field(default=None, serialization_alias="withParam")
|
265
|
-
|
266
|
-
@field_serializer("depends")
|
267
|
-
def transform_depends_as_str(self, depends: List[str]) -> str:
|
268
|
-
return " || ".join(depends)
|
269
|
-
|
270
|
-
@field_serializer("arguments", when_used="unless-none")
|
271
|
-
def empty_arguments_to_none(self, arguments: List[Argument]) -> Dict[str, List[Argument]]:
|
272
|
-
return {"parameters": arguments}
|
273
|
-
|
274
|
-
|
275
|
-
class ContainerTemplate(BaseModel):
|
276
|
-
# These templates are used for actual execution nodes.
|
277
|
-
name: str
|
278
|
-
active_deadline_seconds: Optional[int] = Field(default=None, serialization_alias="activeDeadlineSeconds", gt=0)
|
279
|
-
node_selector: Optional[Dict[str, str]] = Field(default=None, serialization_alias="nodeSelector")
|
280
|
-
retry_strategy: Optional[Retry] = Field(default=None, serialization_alias="retryStrategy")
|
281
|
-
tolerations: Optional[List[Toleration]] = Field(default=None, serialization_alias="tolerations")
|
282
|
-
|
283
|
-
container: Container
|
284
|
-
|
285
|
-
outputs: Optional[List[OutputParameter]] = Field(default=None, serialization_alias="outputs")
|
286
|
-
inputs: Optional[List[Parameter]] = Field(default=None, serialization_alias="inputs")
|
287
|
-
|
288
|
-
def __hash__(self):
|
289
|
-
return hash(self.name)
|
290
|
-
|
291
|
-
@field_serializer("outputs", when_used="unless-none")
|
292
|
-
def reshape_outputs(self, outputs: List[OutputParameter]) -> Dict[str, List[OutputParameter]]:
|
293
|
-
return {"parameters": outputs}
|
294
|
-
|
295
|
-
@field_serializer("inputs", when_used="unless-none")
|
296
|
-
def reshape_inputs(self, inputs: List[Parameter]) -> Dict[str, List[Parameter]]:
|
297
|
-
return {"parameters": inputs}
|
298
|
-
|
299
|
-
|
300
|
-
class DagTemplate(BaseModel):
|
301
|
-
# These are used for parallel, map nodes dag definition
|
302
|
-
name: str = "runnable-dag"
|
303
|
-
tasks: List[DagTaskTemplate] = Field(default=[], exclude=True)
|
304
|
-
inputs: Optional[List[Parameter]] = Field(default=None, serialization_alias="inputs")
|
305
|
-
parallelism: Optional[int] = None
|
306
|
-
fail_fast: bool = Field(default=False, serialization_alias="failFast")
|
307
|
-
|
308
|
-
@field_validator("parallelism")
|
309
|
-
@classmethod
|
310
|
-
def validate_parallelism(cls, parallelism: Optional[int]) -> Optional[int]:
|
311
|
-
if parallelism is not None and parallelism <= 0:
|
312
|
-
raise ValueError("Parallelism must be a positive integer greater than 0")
|
313
|
-
return parallelism
|
314
|
-
|
315
|
-
@computed_field # type: ignore
|
316
|
-
@property
|
317
|
-
def dag(self) -> Dict[str, List[DagTaskTemplate]]:
|
318
|
-
return {"tasks": self.tasks}
|
319
|
-
|
320
|
-
@field_serializer("inputs", when_used="unless-none")
|
321
|
-
def reshape_inputs(self, inputs: List[Parameter], _info) -> Dict[str, List[Parameter]]:
|
322
|
-
return {"parameters": inputs}
|
323
|
-
|
324
|
-
|
325
|
-
class Volume(BaseModel):
|
326
|
-
"""
|
327
|
-
spec config requires, name and persistentVolumeClaim
|
328
|
-
step requires name and mountPath
|
329
|
-
"""
|
330
|
-
|
331
|
-
name: str
|
332
|
-
claim: str = Field(exclude=True)
|
333
|
-
mount_path: str = Field(serialization_alias="mountPath", exclude=True)
|
334
|
-
|
335
|
-
@computed_field # type: ignore
|
336
|
-
@property
|
337
|
-
def persistentVolumeClaim(self) -> Dict[str, str]:
|
338
|
-
return {"claimName": self.claim}
|
339
|
-
|
340
|
-
|
341
|
-
class ContainerVolume(BaseModel):
|
342
|
-
name: str
|
343
|
-
mount_path: str = Field(serialization_alias="mountPath")
|
344
|
-
|
345
|
-
|
346
|
-
class UserVolumeMounts(BaseModel):
|
347
|
-
"""
|
348
|
-
The volume specification as user defines it.
|
349
|
-
"""
|
350
|
-
|
351
|
-
name: str # This is the name of the PVC on K8s
|
352
|
-
mount_path: str # This is mount path on the container
|
353
|
-
|
354
|
-
|
355
|
-
class NodeRenderer(ABC):
|
356
|
-
allowed_node_types: List[str] = []
|
357
|
-
|
358
|
-
def __init__(self, executor: "ArgoExecutor", node: BaseNode) -> None:
|
359
|
-
self.executor = executor
|
360
|
-
self.node = node
|
361
|
-
|
362
|
-
@abstractmethod
|
363
|
-
def render(self, list_of_iter_values: Optional[List] = None):
|
364
|
-
pass
|
365
|
-
|
366
|
-
|
367
|
-
class ExecutionNode(NodeRenderer):
|
368
|
-
allowed_node_types = ["task", "stub", "success", "fail"]
|
369
|
-
|
370
|
-
def render(self, list_of_iter_values: Optional[List] = None):
|
371
|
-
"""
|
372
|
-
Compose the map variable and create the execution command.
|
373
|
-
Create an input to the command.
|
374
|
-
create_container_template : creates an argument for the list of iter values
|
375
|
-
"""
|
376
|
-
map_variable = self.executor.compose_map_variable(list_of_iter_values)
|
377
|
-
command = utils.get_node_execution_command(
|
378
|
-
self.node,
|
379
|
-
over_write_run_id=self.executor._run_id_placeholder,
|
380
|
-
map_variable=map_variable,
|
381
|
-
log_level=self.executor._log_level,
|
382
|
-
)
|
383
|
-
|
384
|
-
inputs = []
|
385
|
-
if list_of_iter_values:
|
386
|
-
for val in list_of_iter_values:
|
387
|
-
inputs.append(Parameter(name=val))
|
388
|
-
|
389
|
-
# Create the container template
|
390
|
-
container_template = self.executor.create_container_template(
|
391
|
-
working_on=self.node,
|
392
|
-
command=command,
|
393
|
-
inputs=inputs,
|
394
|
-
)
|
395
|
-
|
396
|
-
self.executor._container_templates.append(container_template)
|
397
|
-
|
398
|
-
|
399
|
-
class DagNodeRenderer(NodeRenderer):
|
400
|
-
allowed_node_types = ["dag"]
|
401
|
-
|
402
|
-
def render(self, list_of_iter_values: Optional[List] = None):
|
403
|
-
self.node = cast(DagNode, self.node)
|
404
|
-
task_template_arguments = []
|
405
|
-
dag_inputs = []
|
406
|
-
if list_of_iter_values:
|
407
|
-
for value in list_of_iter_values:
|
408
|
-
task_template_arguments.append(Argument(name=value, value="{{inputs.parameters." + value + "}}"))
|
409
|
-
dag_inputs.append(Parameter(name=value))
|
410
|
-
|
411
|
-
clean_name = self.executor.get_clean_name(self.node)
|
412
|
-
fan_out_template = self.executor._create_fan_out_template(
|
413
|
-
composite_node=self.node, list_of_iter_values=list_of_iter_values
|
414
|
-
)
|
415
|
-
fan_out_template.arguments = task_template_arguments if task_template_arguments else None
|
416
|
-
|
417
|
-
fan_in_template = self.executor._create_fan_in_template(
|
418
|
-
composite_node=self.node, list_of_iter_values=list_of_iter_values
|
419
|
-
)
|
420
|
-
fan_in_template.arguments = task_template_arguments if task_template_arguments else None
|
421
|
-
|
422
|
-
self.executor._gather_task_templates_of_dag(
|
423
|
-
self.node.branch,
|
424
|
-
dag_name=f"{clean_name}-branch",
|
425
|
-
list_of_iter_values=list_of_iter_values,
|
426
|
-
)
|
427
|
-
|
428
|
-
branch_template = DagTaskTemplate(
|
429
|
-
name=f"{clean_name}-branch",
|
430
|
-
template=f"{clean_name}-branch",
|
431
|
-
arguments=task_template_arguments if task_template_arguments else None,
|
432
|
-
)
|
433
|
-
branch_template.depends.append(f"{clean_name}-fan-out.Succeeded")
|
434
|
-
fan_in_template.depends.append(f"{clean_name}-branch.Succeeded")
|
435
|
-
fan_in_template.depends.append(f"{clean_name}-branch.Failed")
|
436
|
-
|
437
|
-
self.executor._dag_templates.append(
|
438
|
-
DagTemplate(
|
439
|
-
tasks=[fan_out_template, branch_template, fan_in_template],
|
440
|
-
name=clean_name,
|
441
|
-
inputs=dag_inputs if dag_inputs else None,
|
442
|
-
)
|
443
|
-
)
|
444
|
-
|
445
|
-
|
446
|
-
class ParallelNodeRender(NodeRenderer):
|
447
|
-
allowed_node_types = ["parallel"]
|
448
|
-
|
449
|
-
def render(self, list_of_iter_values: Optional[List] = None):
|
450
|
-
self.node = cast(ParallelNode, self.node)
|
451
|
-
task_template_arguments = []
|
452
|
-
dag_inputs = []
|
453
|
-
if list_of_iter_values:
|
454
|
-
for value in list_of_iter_values:
|
455
|
-
task_template_arguments.append(Argument(name=value, value="{{inputs.parameters." + value + "}}"))
|
456
|
-
dag_inputs.append(Parameter(name=value))
|
457
|
-
|
458
|
-
clean_name = self.executor.get_clean_name(self.node)
|
459
|
-
fan_out_template = self.executor._create_fan_out_template(
|
460
|
-
composite_node=self.node, list_of_iter_values=list_of_iter_values
|
461
|
-
)
|
462
|
-
fan_out_template.arguments = task_template_arguments if task_template_arguments else None
|
463
|
-
|
464
|
-
fan_in_template = self.executor._create_fan_in_template(
|
465
|
-
composite_node=self.node, list_of_iter_values=list_of_iter_values
|
466
|
-
)
|
467
|
-
fan_in_template.arguments = task_template_arguments if task_template_arguments else None
|
468
|
-
|
469
|
-
branch_templates = []
|
470
|
-
for name, branch in self.node.branches.items():
|
471
|
-
branch_name = self.executor.sanitize_name(name)
|
472
|
-
self.executor._gather_task_templates_of_dag(
|
473
|
-
branch,
|
474
|
-
dag_name=f"{clean_name}-{branch_name}",
|
475
|
-
list_of_iter_values=list_of_iter_values,
|
476
|
-
)
|
477
|
-
task_template = DagTaskTemplate(
|
478
|
-
name=f"{clean_name}-{branch_name}",
|
479
|
-
template=f"{clean_name}-{branch_name}",
|
480
|
-
arguments=task_template_arguments if task_template_arguments else None,
|
481
|
-
)
|
482
|
-
task_template.depends.append(f"{clean_name}-fan-out.Succeeded")
|
483
|
-
fan_in_template.depends.append(f"{task_template.name}.Succeeded")
|
484
|
-
fan_in_template.depends.append(f"{task_template.name}.Failed")
|
485
|
-
branch_templates.append(task_template)
|
486
|
-
|
487
|
-
executor_config = self.executor._resolve_executor_config(self.node)
|
488
|
-
|
489
|
-
self.executor._dag_templates.append(
|
490
|
-
DagTemplate(
|
491
|
-
tasks=[fan_out_template] + branch_templates + [fan_in_template],
|
492
|
-
name=clean_name,
|
493
|
-
inputs=dag_inputs if dag_inputs else None,
|
494
|
-
parallelism=executor_config.get("parallelism", None),
|
495
|
-
)
|
496
|
-
)
|
497
|
-
|
498
|
-
|
499
|
-
class MapNodeRender(NodeRenderer):
|
500
|
-
allowed_node_types = ["map"]
|
501
|
-
|
502
|
-
def render(self, list_of_iter_values: Optional[List] = None):
|
503
|
-
self.node = cast(MapNode, self.node)
|
504
|
-
task_template_arguments = []
|
505
|
-
dag_inputs = []
|
506
|
-
|
507
|
-
if not list_of_iter_values:
|
508
|
-
list_of_iter_values = []
|
509
|
-
|
510
|
-
for value in list_of_iter_values:
|
511
|
-
task_template_arguments.append(Argument(name=value, value="{{inputs.parameters." + value + "}}"))
|
512
|
-
dag_inputs.append(Parameter(name=value))
|
513
|
-
|
514
|
-
clean_name = self.executor.get_clean_name(self.node)
|
515
|
-
|
516
|
-
fan_out_template = self.executor._create_fan_out_template(
|
517
|
-
composite_node=self.node, list_of_iter_values=list_of_iter_values
|
518
|
-
)
|
519
|
-
fan_out_template.arguments = task_template_arguments if task_template_arguments else None
|
520
|
-
|
521
|
-
fan_in_template = self.executor._create_fan_in_template(
|
522
|
-
composite_node=self.node, list_of_iter_values=list_of_iter_values
|
523
|
-
)
|
524
|
-
fan_in_template.arguments = task_template_arguments if task_template_arguments else None
|
525
|
-
|
526
|
-
list_of_iter_values.append(self.node.iterate_as)
|
527
|
-
|
528
|
-
self.executor._gather_task_templates_of_dag(
|
529
|
-
self.node.branch,
|
530
|
-
dag_name=f"{clean_name}-map",
|
531
|
-
list_of_iter_values=list_of_iter_values,
|
532
|
-
)
|
533
|
-
|
534
|
-
task_template = DagTaskTemplate(
|
535
|
-
name=f"{clean_name}-map",
|
536
|
-
template=f"{clean_name}-map",
|
537
|
-
arguments=task_template_arguments if task_template_arguments else None,
|
538
|
-
)
|
539
|
-
task_template.with_param = "{{tasks." + f"{clean_name}-fan-out" + ".outputs.parameters." + "iterate-on" + "}}"
|
540
|
-
|
541
|
-
argument = Argument(name=self.node.iterate_as, value="{{item}}")
|
542
|
-
if task_template.arguments is None:
|
543
|
-
task_template.arguments = []
|
544
|
-
task_template.arguments.append(argument)
|
545
|
-
|
546
|
-
task_template.depends.append(f"{clean_name}-fan-out.Succeeded")
|
547
|
-
fan_in_template.depends.append(f"{clean_name}-map.Succeeded")
|
548
|
-
fan_in_template.depends.append(f"{clean_name}-map.Failed")
|
549
|
-
|
550
|
-
executor_config = self.executor._resolve_executor_config(self.node)
|
551
|
-
|
552
|
-
self.executor._dag_templates.append(
|
553
|
-
DagTemplate(
|
554
|
-
tasks=[fan_out_template, task_template, fan_in_template],
|
555
|
-
name=clean_name,
|
556
|
-
inputs=dag_inputs if dag_inputs else None,
|
557
|
-
parallelism=executor_config.get("parallelism", None),
|
558
|
-
fail_fast=executor_config.get("fail_fast", True),
|
559
|
-
)
|
560
|
-
)
|
561
|
-
|
562
|
-
|
563
|
-
def get_renderer(node):
|
564
|
-
renderers = NodeRenderer.__subclasses__()
|
565
|
-
|
566
|
-
for renderer in renderers:
|
567
|
-
if node.node_type in renderer.allowed_node_types:
|
568
|
-
return renderer
|
569
|
-
raise Exception("This node type is not render-able")
|
570
|
-
|
571
|
-
|
572
|
-
class MetaData(BaseModel):
|
573
|
-
generate_name: str = Field(default="runnable-dag-", serialization_alias="generateName")
|
574
|
-
annotations: Optional[Dict[str, str]] = Field(default_factory=dict)
|
575
|
-
labels: Optional[Dict[str, str]] = Field(default_factory=dict)
|
576
|
-
namespace: Optional[str] = Field(default=None)
|
577
|
-
|
578
|
-
|
579
|
-
class Spec(BaseModel):
|
580
|
-
active_deadline_seconds: int = Field(serialization_alias="activeDeadlineSeconds")
|
581
|
-
entrypoint: str = Field(default="runnable-dag")
|
582
|
-
node_selector: Optional[Dict[str, str]] = Field(default_factory=dict, serialization_alias="nodeSelector")
|
583
|
-
tolerations: Optional[List[Toleration]] = Field(default=None, serialization_alias="tolerations")
|
584
|
-
parallelism: Optional[int] = Field(default=None, serialization_alias="parallelism")
|
585
|
-
|
586
|
-
# TODO: This has to be user driven
|
587
|
-
pod_gc: Dict[str, str] = Field(
|
588
|
-
default={"strategy": "OnPodSuccess", "deleteDelayDuration": "600s"},
|
589
|
-
serialization_alias="podGC",
|
590
|
-
)
|
591
|
-
|
592
|
-
retry_strategy: Retry = Field(default=Retry(), serialization_alias="retryStrategy")
|
593
|
-
service_account_name: Optional[str] = Field(default=None, serialization_alias="serviceAccountName")
|
594
|
-
|
595
|
-
templates: List[Union[DagTemplate, ContainerTemplate]] = Field(default_factory=list)
|
596
|
-
template_defaults: Optional[TemplateDefaults] = Field(default=None, serialization_alias="templateDefaults")
|
597
|
-
|
598
|
-
arguments: Optional[List[EnvVar]] = Field(default_factory=list)
|
599
|
-
persistent_volumes: List[UserVolumeMounts] = Field(default_factory=list, exclude=True)
|
600
|
-
|
601
|
-
@field_validator("parallelism")
|
602
|
-
@classmethod
|
603
|
-
def validate_parallelism(cls, parallelism: Optional[int]) -> Optional[int]:
|
604
|
-
if parallelism is not None and parallelism <= 0:
|
605
|
-
raise ValueError("Parallelism must be a positive integer greater than 0")
|
606
|
-
return parallelism
|
607
|
-
|
608
|
-
@computed_field # type: ignore
|
609
|
-
@property
|
610
|
-
def volumes(self) -> List[Volume]:
|
611
|
-
volumes: List[Volume] = []
|
612
|
-
claim_names = {}
|
613
|
-
for i, user_volume in enumerate(self.persistent_volumes):
|
614
|
-
if user_volume.name in claim_names:
|
615
|
-
raise Exception(f"Duplicate claim name {user_volume.name}")
|
616
|
-
claim_names[user_volume.name] = user_volume.name
|
617
|
-
|
618
|
-
volume = Volume(name=f"executor-{i}", claim=user_volume.name, mount_path=user_volume.mount_path)
|
619
|
-
volumes.append(volume)
|
620
|
-
return volumes
|
621
|
-
|
622
|
-
@field_serializer("arguments", when_used="unless-none")
|
623
|
-
def reshape_arguments(self, arguments: List[EnvVar], _info) -> Dict[str, List[EnvVar]]:
|
624
|
-
return {"parameters": arguments}
|
625
|
-
|
626
|
-
|
627
|
-
class Workflow(BaseModel):
|
628
|
-
api_version: str = Field(
|
629
|
-
default="argoproj.io/v1alpha1",
|
630
|
-
serialization_alias="apiVersion",
|
631
|
-
)
|
632
|
-
kind: str = "Workflow"
|
633
|
-
metadata: MetaData = Field(default=MetaData())
|
634
|
-
spec: Spec
|
635
|
-
|
636
|
-
|
637
|
-
class Override(BaseModel):
|
638
|
-
model_config = ConfigDict(extra="ignore")
|
639
|
-
|
640
|
-
image: str
|
641
|
-
tolerations: Optional[List[Toleration]] = Field(default=None)
|
642
|
-
|
643
|
-
max_step_duration_in_seconds: int = Field(
|
644
|
-
default=2 * 60 * 60, # 2 hours
|
645
|
-
gt=0,
|
646
|
-
)
|
647
|
-
|
648
|
-
node_selector: Optional[Dict[str, str]] = Field(
|
649
|
-
default=None,
|
650
|
-
serialization_alias="nodeSelector",
|
651
|
-
)
|
652
|
-
|
653
|
-
parallelism: Optional[int] = Field(
|
654
|
-
default=None,
|
655
|
-
serialization_alias="parallelism",
|
656
|
-
)
|
657
|
-
|
658
|
-
resources: Resources = Field(
|
659
|
-
default=Resources(),
|
660
|
-
serialization_alias="resources",
|
661
|
-
)
|
662
|
-
|
663
|
-
image_pull_policy: str = Field(default="")
|
664
|
-
|
665
|
-
retry_strategy: Retry = Field(
|
666
|
-
default=Retry(),
|
667
|
-
serialization_alias="retryStrategy",
|
668
|
-
description="Common across all templates",
|
669
|
-
)
|
670
|
-
|
671
|
-
@field_validator("parallelism")
|
672
|
-
@classmethod
|
673
|
-
def validate_parallelism(cls, parallelism: Optional[int]) -> Optional[int]:
|
674
|
-
if parallelism is not None and parallelism <= 0:
|
675
|
-
raise ValueError("Parallelism must be a positive integer greater than 0")
|
676
|
-
return parallelism
|
677
|
-
|
678
|
-
|
679
|
-
class ArgoExecutor(GenericExecutor):
|
680
|
-
service_name: str = "argo"
|
681
|
-
_local: bool = False
|
682
|
-
|
683
|
-
# TODO: Add logging level as option.
|
684
|
-
|
685
|
-
model_config = ConfigDict(extra="forbid")
|
686
|
-
|
687
|
-
image: str
|
688
|
-
expose_parameters_as_inputs: bool = True
|
689
|
-
secrets_from_k8s: List[SecretEnvVar] = Field(default_factory=list)
|
690
|
-
output_file: str = "argo-pipeline.yaml"
|
691
|
-
|
692
|
-
# Metadata related fields
|
693
|
-
name: str = Field(default="runnable-dag-", description="Used as an identifier for the workflow")
|
694
|
-
annotations: Dict[str, str] = Field(default_factory=dict)
|
695
|
-
labels: Dict[str, str] = Field(default_factory=dict)
|
696
|
-
|
697
|
-
max_workflow_duration_in_seconds: int = Field(
|
698
|
-
2 * 24 * 60 * 60, # 2 days
|
699
|
-
serialization_alias="activeDeadlineSeconds",
|
700
|
-
gt=0,
|
701
|
-
)
|
702
|
-
node_selector: Optional[Dict[str, str]] = Field(
|
703
|
-
default=None,
|
704
|
-
serialization_alias="nodeSelector",
|
705
|
-
)
|
706
|
-
parallelism: Optional[int] = Field(
|
707
|
-
default=None,
|
708
|
-
serialization_alias="parallelism",
|
709
|
-
)
|
710
|
-
resources: Resources = Field(
|
711
|
-
default=Resources(),
|
712
|
-
serialization_alias="resources",
|
713
|
-
exclude=True,
|
714
|
-
)
|
715
|
-
retry_strategy: Retry = Field(
|
716
|
-
default=Retry(),
|
717
|
-
serialization_alias="retryStrategy",
|
718
|
-
description="Common across all templates",
|
719
|
-
)
|
720
|
-
max_step_duration_in_seconds: int = Field(
|
721
|
-
default=2 * 60 * 60, # 2 hours
|
722
|
-
gt=0,
|
723
|
-
)
|
724
|
-
tolerations: Optional[List[Toleration]] = Field(default=None)
|
725
|
-
image_pull_policy: str = Field(default="")
|
726
|
-
service_account_name: Optional[str] = None
|
727
|
-
persistent_volumes: List[UserVolumeMounts] = Field(default_factory=list)
|
728
|
-
|
729
|
-
_run_id_placeholder: str = "{{workflow.parameters.run_id}}"
|
730
|
-
_log_level: str = "{{workflow.parameters.log_level}}"
|
731
|
-
_container_templates: List[ContainerTemplate] = []
|
732
|
-
_dag_templates: List[DagTemplate] = []
|
733
|
-
_clean_names: Dict[str, str] = {}
|
734
|
-
_container_volumes: List[ContainerVolume] = []
|
735
|
-
|
736
|
-
@field_validator("parallelism")
|
737
|
-
@classmethod
|
738
|
-
def validate_parallelism(cls, parallelism: Optional[int]) -> Optional[int]:
|
739
|
-
if parallelism is not None and parallelism <= 0:
|
740
|
-
raise ValueError("Parallelism must be a positive integer greater than 0")
|
741
|
-
return parallelism
|
742
|
-
|
743
|
-
@computed_field # type: ignore
|
744
|
-
@property
|
745
|
-
def step_timeout(self) -> int:
|
746
|
-
"""
|
747
|
-
Maximum time the step can take to complete, including the pending state.
|
748
|
-
"""
|
749
|
-
return self.max_step_duration_in_seconds + 2 * 60 * 60 # 2 hours + max_step_duration_in_seconds
|
750
|
-
|
751
|
-
@property
|
752
|
-
def metadata(self) -> MetaData:
|
753
|
-
return MetaData(
|
754
|
-
generate_name=self.name,
|
755
|
-
annotations=self.annotations,
|
756
|
-
labels=self.labels,
|
757
|
-
)
|
758
|
-
|
759
|
-
@property
|
760
|
-
def spec(self) -> Spec:
|
761
|
-
return Spec(
|
762
|
-
active_deadline_seconds=self.max_workflow_duration_in_seconds,
|
763
|
-
node_selector=self.node_selector,
|
764
|
-
tolerations=self.tolerations,
|
765
|
-
parallelism=self.parallelism,
|
766
|
-
retry_strategy=self.retry_strategy,
|
767
|
-
service_account_name=self.service_account_name,
|
768
|
-
persistent_volumes=self.persistent_volumes,
|
769
|
-
template_defaults=TemplateDefaults(max_step_duration=self.max_step_duration_in_seconds),
|
770
|
-
)
|
771
|
-
|
772
|
-
def prepare_for_graph_execution(self):
|
773
|
-
"""
|
774
|
-
This method should be called prior to calling execute_graph.
|
775
|
-
Perform any steps required before doing the graph execution.
|
776
|
-
|
777
|
-
The most common implementation is to prepare a run log for the run if the run uses local interactive compute.
|
778
|
-
|
779
|
-
But in cases of actual rendering the job specs (eg: AWS step functions, K8's) we check if the services are OK.
|
780
|
-
We do not set up a run log as its not relevant.
|
781
|
-
"""
|
782
|
-
|
783
|
-
integration.validate(self, self._context.run_log_store)
|
784
|
-
integration.configure_for_traversal(self, self._context.run_log_store)
|
785
|
-
|
786
|
-
integration.validate(self, self._context.catalog_handler)
|
787
|
-
integration.configure_for_traversal(self, self._context.catalog_handler)
|
788
|
-
|
789
|
-
integration.validate(self, self._context.secrets_handler)
|
790
|
-
integration.configure_for_traversal(self, self._context.secrets_handler)
|
791
|
-
|
792
|
-
def prepare_for_node_execution(self):
|
793
|
-
"""
|
794
|
-
Perform any modifications to the services prior to execution of the node.
|
795
|
-
|
796
|
-
Args:
|
797
|
-
node (Node): [description]
|
798
|
-
map_variable (dict, optional): [description]. Defaults to None.
|
799
|
-
"""
|
800
|
-
|
801
|
-
super().prepare_for_node_execution()
|
802
|
-
self._set_up_run_log(exists_ok=True)
|
803
|
-
|
804
|
-
def execute_node(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
805
|
-
step_log = self._context.run_log_store.create_step_log(node.name, node._get_step_log_name(map_variable))
|
806
|
-
|
807
|
-
self.add_code_identities(node=node, step_log=step_log)
|
808
|
-
|
809
|
-
step_log.step_type = node.node_type
|
810
|
-
step_log.status = defaults.PROCESSING
|
811
|
-
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
812
|
-
|
813
|
-
super()._execute_node(node, map_variable=map_variable, **kwargs)
|
814
|
-
|
815
|
-
# Implicit fail
|
816
|
-
if self._context.dag:
|
817
|
-
# functions and notebooks do not have dags
|
818
|
-
_, current_branch = search_node_by_internal_name(dag=self._context.dag, internal_name=node.internal_name)
|
819
|
-
_, next_node_name = self._get_status_and_next_node_name(node, current_branch, map_variable=map_variable)
|
820
|
-
if next_node_name:
|
821
|
-
# Terminal nodes do not have next node name
|
822
|
-
next_node = current_branch.get_node_by_name(next_node_name)
|
823
|
-
|
824
|
-
if next_node.node_type == defaults.FAIL:
|
825
|
-
self.execute_node(next_node, map_variable=map_variable)
|
826
|
-
|
827
|
-
step_log = self._context.run_log_store.get_step_log(node._get_step_log_name(map_variable), self._context.run_id)
|
828
|
-
if step_log.status == defaults.FAIL:
|
829
|
-
raise Exception(f"Step {node.name} failed")
|
830
|
-
|
831
|
-
def fan_out(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
832
|
-
super().fan_out(node, map_variable)
|
833
|
-
|
834
|
-
# If its a map node, write the list values to "/tmp/output.txt"
|
835
|
-
if node.node_type == "map":
|
836
|
-
node = cast(MapNode, node)
|
837
|
-
iterate_on = self._context.run_log_store.get_parameters(self._context.run_id)[node.iterate_on]
|
838
|
-
|
839
|
-
with open("/tmp/output.txt", mode="w", encoding="utf-8") as myfile:
|
840
|
-
json.dump(iterate_on.get_value(), myfile, indent=4)
|
841
|
-
|
842
|
-
def sanitize_name(self, name):
|
843
|
-
return name.replace(" ", "-").replace(".", "-").replace("_", "-")
|
844
|
-
|
845
|
-
def get_clean_name(self, node: BaseNode):
|
846
|
-
# Cache names for the node
|
847
|
-
if node.internal_name not in self._clean_names:
|
848
|
-
sanitized = self.sanitize_name(node.name)
|
849
|
-
tag = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
850
|
-
self._clean_names[node.internal_name] = f"{sanitized}-{node.node_type}-{tag}"
|
851
|
-
|
852
|
-
return self._clean_names[node.internal_name]
|
853
|
-
|
854
|
-
def compose_map_variable(self, list_of_iter_values: Optional[List] = None) -> TypeMapVariable:
|
855
|
-
map_variable = OrderedDict()
|
856
|
-
|
857
|
-
# If we are inside a map node, compose a map_variable
|
858
|
-
# The values of "iterate_as" are sent over as inputs to the container template
|
859
|
-
if list_of_iter_values:
|
860
|
-
for var in list_of_iter_values:
|
861
|
-
map_variable[var] = "{{inputs.parameters." + str(var) + "}}"
|
862
|
-
|
863
|
-
return map_variable # type: ignore
|
864
|
-
|
865
|
-
def create_container_template(
|
866
|
-
self,
|
867
|
-
working_on: BaseNode,
|
868
|
-
command: str,
|
869
|
-
inputs: Optional[List] = None,
|
870
|
-
outputs: Optional[List] = None,
|
871
|
-
overwrite_name: str = "",
|
872
|
-
):
|
873
|
-
effective_node_config = self._resolve_executor_config(working_on)
|
874
|
-
|
875
|
-
override: Override = Override(**effective_node_config)
|
876
|
-
|
877
|
-
container = Container(
|
878
|
-
command=command,
|
879
|
-
image=override.image,
|
880
|
-
volume_mounts=self._container_volumes,
|
881
|
-
image_pull_policy=override.image_pull_policy,
|
882
|
-
resources=override.resources,
|
883
|
-
secrets_from_k8s=self.secrets_from_k8s,
|
884
|
-
)
|
885
|
-
|
886
|
-
if working_on.name == self._context.dag.start_at and self.expose_parameters_as_inputs:
|
887
|
-
for key, value in self._get_parameters().items():
|
888
|
-
value = value.get_value() # type: ignore
|
889
|
-
# Get the value from work flow parameters for dynamic behavior
|
890
|
-
if isinstance(value, int) or isinstance(value, float) or isinstance(value, str):
|
891
|
-
env_var = EnvVar(
|
892
|
-
name=defaults.PARAMETER_PREFIX + key,
|
893
|
-
value="{{workflow.parameters." + key + "}}",
|
894
|
-
)
|
895
|
-
container.env_vars.append(env_var)
|
896
|
-
|
897
|
-
clean_name = self.get_clean_name(working_on)
|
898
|
-
if overwrite_name:
|
899
|
-
clean_name = overwrite_name
|
900
|
-
|
901
|
-
container_template = ContainerTemplate(
|
902
|
-
name=clean_name,
|
903
|
-
active_deadline_seconds=(
|
904
|
-
override.max_step_duration_in_seconds
|
905
|
-
if self.max_step_duration_in_seconds != override.max_step_duration_in_seconds
|
906
|
-
else None
|
907
|
-
),
|
908
|
-
container=container,
|
909
|
-
retry_strategy=override.retry_strategy if self.retry_strategy != override.retry_strategy else None,
|
910
|
-
tolerations=override.tolerations if self.tolerations != override.tolerations else None,
|
911
|
-
node_selector=override.node_selector if self.node_selector != override.node_selector else None,
|
912
|
-
)
|
913
|
-
|
914
|
-
# inputs are the "iterate_as" value map variables in the same order as they are observed
|
915
|
-
# We need to expose the map variables in the command of the container
|
916
|
-
if inputs:
|
917
|
-
if not container_template.inputs:
|
918
|
-
container_template.inputs = []
|
919
|
-
container_template.inputs.extend(inputs)
|
920
|
-
|
921
|
-
# The map step fan out would create an output that we should propagate via Argo
|
922
|
-
if outputs:
|
923
|
-
if not container_template.outputs:
|
924
|
-
container_template.outputs = []
|
925
|
-
container_template.outputs.extend(outputs)
|
926
|
-
|
927
|
-
return container_template
|
928
|
-
|
929
|
-
def _create_fan_out_template(self, composite_node, list_of_iter_values: Optional[List] = None):
|
930
|
-
clean_name = self.get_clean_name(composite_node)
|
931
|
-
inputs = []
|
932
|
-
# If we are fanning out already map state, we need to send the map variable inside
|
933
|
-
# The container template also should be accepting an input parameter
|
934
|
-
map_variable = None
|
935
|
-
if list_of_iter_values:
|
936
|
-
map_variable = self.compose_map_variable(list_of_iter_values=list_of_iter_values)
|
937
|
-
|
938
|
-
for val in list_of_iter_values:
|
939
|
-
inputs.append(Parameter(name=val))
|
940
|
-
|
941
|
-
command = utils.get_fan_command(
|
942
|
-
mode="out",
|
943
|
-
node=composite_node,
|
944
|
-
run_id=self._run_id_placeholder,
|
945
|
-
map_variable=map_variable,
|
946
|
-
log_level=self._log_level,
|
947
|
-
)
|
948
|
-
|
949
|
-
outputs = []
|
950
|
-
# If the node is a map node, we have to set the output parameters
|
951
|
-
# Output is always the step's internal name + iterate-on
|
952
|
-
if composite_node.node_type == "map":
|
953
|
-
output_parameter = OutputParameter(name="iterate-on")
|
954
|
-
outputs.append(output_parameter)
|
955
|
-
|
956
|
-
# Create the node now
|
957
|
-
step_config = {"command": command, "type": "task", "next": "dummy"}
|
958
|
-
node = create_node(name=f"{clean_name}-fan-out", step_config=step_config)
|
959
|
-
|
960
|
-
container_template = self.create_container_template(
|
961
|
-
working_on=node,
|
962
|
-
command=command,
|
963
|
-
outputs=outputs,
|
964
|
-
inputs=inputs,
|
965
|
-
overwrite_name=f"{clean_name}-fan-out",
|
966
|
-
)
|
967
|
-
|
968
|
-
self._container_templates.append(container_template)
|
969
|
-
return DagTaskTemplate(name=f"{clean_name}-fan-out", template=f"{clean_name}-fan-out")
|
970
|
-
|
971
|
-
def _create_fan_in_template(self, composite_node, list_of_iter_values: Optional[List] = None):
|
972
|
-
clean_name = self.get_clean_name(composite_node)
|
973
|
-
inputs = []
|
974
|
-
# If we are fanning in already map state, we need to send the map variable inside
|
975
|
-
# The container template also should be accepting an input parameter
|
976
|
-
map_variable = None
|
977
|
-
if list_of_iter_values:
|
978
|
-
map_variable = self.compose_map_variable(list_of_iter_values=list_of_iter_values)
|
979
|
-
|
980
|
-
for val in list_of_iter_values:
|
981
|
-
inputs.append(Parameter(name=val))
|
982
|
-
|
983
|
-
command = utils.get_fan_command(
|
984
|
-
mode="in",
|
985
|
-
node=composite_node,
|
986
|
-
run_id=self._run_id_placeholder,
|
987
|
-
map_variable=map_variable,
|
988
|
-
log_level=self._log_level,
|
989
|
-
)
|
990
|
-
|
991
|
-
step_config = {"command": command, "type": "task", "next": "dummy"}
|
992
|
-
node = create_node(name=f"{clean_name}-fan-in", step_config=step_config)
|
993
|
-
container_template = self.create_container_template(
|
994
|
-
working_on=node,
|
995
|
-
command=command,
|
996
|
-
inputs=inputs,
|
997
|
-
overwrite_name=f"{clean_name}-fan-in",
|
998
|
-
)
|
999
|
-
self._container_templates.append(container_template)
|
1000
|
-
clean_name = self.get_clean_name(composite_node)
|
1001
|
-
return DagTaskTemplate(name=f"{clean_name}-fan-in", template=f"{clean_name}-fan-in")
|
1002
|
-
|
1003
|
-
def _gather_task_templates_of_dag(
|
1004
|
-
self, dag: Graph, dag_name="runnable-dag", list_of_iter_values: Optional[List] = None
|
1005
|
-
):
|
1006
|
-
current_node = dag.start_at
|
1007
|
-
previous_node = None
|
1008
|
-
previous_node_template_name = None
|
1009
|
-
|
1010
|
-
templates: Dict[str, DagTaskTemplate] = {}
|
1011
|
-
|
1012
|
-
if not list_of_iter_values:
|
1013
|
-
list_of_iter_values = []
|
1014
|
-
|
1015
|
-
while True:
|
1016
|
-
working_on = dag.get_node_by_name(current_node)
|
1017
|
-
if previous_node == current_node:
|
1018
|
-
raise Exception("Potentially running in a infinite loop")
|
1019
|
-
|
1020
|
-
render_obj = get_renderer(working_on)(executor=self, node=working_on)
|
1021
|
-
render_obj.render(list_of_iter_values=list_of_iter_values.copy())
|
1022
|
-
|
1023
|
-
clean_name = self.get_clean_name(working_on)
|
1024
|
-
|
1025
|
-
# If a task template for clean name exists, retrieve it (could have been created by on_failure)
|
1026
|
-
template = templates.get(clean_name, DagTaskTemplate(name=clean_name, template=clean_name))
|
1027
|
-
|
1028
|
-
# Link the current node to previous node, if the previous node was successful.
|
1029
|
-
if previous_node:
|
1030
|
-
template.depends.append(f"{previous_node_template_name}.Succeeded")
|
1031
|
-
|
1032
|
-
templates[clean_name] = template
|
1033
|
-
|
1034
|
-
# On failure nodes
|
1035
|
-
if working_on.node_type not in ["success", "fail"] and working_on._get_on_failure_node():
|
1036
|
-
failure_node = dag.get_node_by_name(working_on._get_on_failure_node())
|
1037
|
-
|
1038
|
-
# same logic, if a template exists, retrieve it
|
1039
|
-
# if not, create a new one
|
1040
|
-
render_obj = get_renderer(working_on)(executor=self, node=failure_node)
|
1041
|
-
render_obj.render(list_of_iter_values=list_of_iter_values.copy())
|
1042
|
-
|
1043
|
-
failure_template_name = self.get_clean_name(failure_node)
|
1044
|
-
# If a task template for clean name exists, retrieve it
|
1045
|
-
failure_template = templates.get(
|
1046
|
-
failure_template_name,
|
1047
|
-
DagTaskTemplate(name=failure_template_name, template=failure_template_name),
|
1048
|
-
)
|
1049
|
-
failure_template.depends.append(f"{clean_name}.Failed")
|
1050
|
-
templates[failure_template_name] = failure_template
|
1051
|
-
|
1052
|
-
# If we are in a map node, we need to add the values as arguments
|
1053
|
-
template = templates[clean_name]
|
1054
|
-
if list_of_iter_values:
|
1055
|
-
if not template.arguments:
|
1056
|
-
template.arguments = []
|
1057
|
-
for value in list_of_iter_values:
|
1058
|
-
template.arguments.append(Argument(name=value, value="{{inputs.parameters." + value + "}}"))
|
1059
|
-
|
1060
|
-
# Move ahead to the next node
|
1061
|
-
previous_node = current_node
|
1062
|
-
previous_node_template_name = self.get_clean_name(working_on)
|
1063
|
-
|
1064
|
-
if working_on.node_type in ["success", "fail"]:
|
1065
|
-
break
|
1066
|
-
|
1067
|
-
current_node = working_on._get_next_node()
|
1068
|
-
|
1069
|
-
# Add the iteration values as input to dag template
|
1070
|
-
dag_template = DagTemplate(tasks=list(templates.values()), name=dag_name)
|
1071
|
-
if list_of_iter_values:
|
1072
|
-
if not dag_template.inputs:
|
1073
|
-
dag_template.inputs = []
|
1074
|
-
dag_template.inputs.extend([Parameter(name=val) for val in list_of_iter_values])
|
1075
|
-
|
1076
|
-
# Add the dag template to the list of templates
|
1077
|
-
self._dag_templates.append(dag_template)
|
1078
|
-
|
1079
|
-
def _get_template_defaults(self) -> TemplateDefaults:
|
1080
|
-
user_provided_config = self.model_dump(by_alias=False)
|
1081
|
-
|
1082
|
-
return TemplateDefaults(**user_provided_config)
|
1083
|
-
|
1084
|
-
def execute_graph(self, dag: Graph, map_variable: Optional[dict] = None, **kwargs):
|
1085
|
-
# TODO: Add metadata
|
1086
|
-
arguments = []
|
1087
|
-
# Expose "simple" parameters as workflow arguments for dynamic behavior
|
1088
|
-
if self.expose_parameters_as_inputs:
|
1089
|
-
for key, value in self._get_parameters().items():
|
1090
|
-
value = value.get_value() # type: ignore
|
1091
|
-
if isinstance(value, dict) or isinstance(value, list):
|
1092
|
-
continue
|
1093
|
-
|
1094
|
-
env_var = EnvVar(name=key, value=value) # type: ignore
|
1095
|
-
arguments.append(env_var)
|
1096
|
-
|
1097
|
-
run_id_var = EnvVar(name="run_id", value="{{workflow.uid}}")
|
1098
|
-
log_level_var = EnvVar(name="log_level", value=defaults.LOG_LEVEL)
|
1099
|
-
arguments.append(run_id_var)
|
1100
|
-
arguments.append(log_level_var)
|
1101
|
-
|
1102
|
-
# TODO: Can we do reruns?
|
1103
|
-
|
1104
|
-
for volume in self.spec.volumes:
|
1105
|
-
self._container_volumes.append(ContainerVolume(name=volume.name, mount_path=volume.mount_path))
|
1106
|
-
|
1107
|
-
# Container specifications are globally collected and added at the end.
|
1108
|
-
# Dag specifications are added as part of the dag traversal.
|
1109
|
-
templates: List[Union[DagTemplate, ContainerTemplate]] = []
|
1110
|
-
self._gather_task_templates_of_dag(dag=dag, list_of_iter_values=[])
|
1111
|
-
templates.extend(self._dag_templates)
|
1112
|
-
templates.extend(self._container_templates)
|
1113
|
-
|
1114
|
-
spec = self.spec
|
1115
|
-
spec.templates = templates
|
1116
|
-
spec.arguments = arguments
|
1117
|
-
workflow = Workflow(metadata=self.metadata, spec=spec)
|
1118
|
-
|
1119
|
-
yaml = YAML()
|
1120
|
-
with open(self.output_file, "w") as f:
|
1121
|
-
yaml.indent(mapping=2, sequence=4, offset=2)
|
1122
|
-
|
1123
|
-
yaml.dump(workflow.model_dump(by_alias=True, exclude_none=True), f)
|
1124
|
-
|
1125
|
-
def execute_job(self, node: BaseNode):
|
1126
|
-
"""
|
1127
|
-
Use K8's job instead
|
1128
|
-
"""
|
1129
|
-
raise NotImplementedError("Use K8's job instead")
|
1130
|
-
|
1131
|
-
def send_return_code(self, stage="traversal"):
|
1132
|
-
"""
|
1133
|
-
Convenience function used by pipeline to send return code to the caller of the cli
|
1134
|
-
|
1135
|
-
Raises:
|
1136
|
-
Exception: If the pipeline execution failed
|
1137
|
-
"""
|
1138
|
-
if stage != "traversal": # traversal does no actual execution, so return code is pointless
|
1139
|
-
run_id = self._context.run_id
|
1140
|
-
|
1141
|
-
run_log = self._context.run_log_store.get_run_log_by_id(run_id=run_id, full=False)
|
1142
|
-
if run_log.status == defaults.FAIL:
|
1143
|
-
raise exceptions.ExecutionFailedError(run_id)
|
1144
|
-
|
1145
|
-
|
1146
|
-
class FileSystemRunLogStore(BaseIntegration):
|
1147
|
-
"""
|
1148
|
-
Only local execution mode is possible for Buffered Run Log store
|
1149
|
-
"""
|
1150
|
-
|
1151
|
-
executor_type = "argo"
|
1152
|
-
service_type = "run_log_store" # One of secret, catalog, datastore
|
1153
|
-
service_provider = "file-system" # The actual implementation of the service
|
1154
|
-
|
1155
|
-
def validate(self, **kwargs):
|
1156
|
-
msg = (
|
1157
|
-
"Argo cannot run work with file-system run log store. "
|
1158
|
-
"Unless you have made a mechanism to use volume mounts."
|
1159
|
-
"Using this run log store if the pipeline has concurrent tasks might lead to unexpected results"
|
1160
|
-
)
|
1161
|
-
logger.warning(msg)
|
1162
|
-
|
1163
|
-
|
1164
|
-
class ChunkedFileSystemRunLogStore(BaseIntegration):
|
1165
|
-
"""
|
1166
|
-
Only local execution mode is possible for Buffered Run Log store
|
1167
|
-
"""
|
1168
|
-
|
1169
|
-
executor_type = "argo"
|
1170
|
-
service_type = "run_log_store" # One of secret, catalog, datastore
|
1171
|
-
service_provider = "chunked-fs" # The actual implementation of the service
|
1172
|
-
|
1173
|
-
def validate(self, **kwargs):
|
1174
|
-
msg = (
|
1175
|
-
"Argo cannot run work with chunked file-system run log store. "
|
1176
|
-
"Unless you have made a mechanism to use volume mounts"
|
1177
|
-
)
|
1178
|
-
logger.warning(msg)
|
1179
|
-
|
1180
|
-
|
1181
|
-
class FileSystemCatalog(BaseIntegration):
|
1182
|
-
"""
|
1183
|
-
Only local execution mode is possible for Buffered Run Log store
|
1184
|
-
"""
|
1185
|
-
|
1186
|
-
executor_type = "argo"
|
1187
|
-
service_type = "catalog" # One of secret, catalog, datastore
|
1188
|
-
service_provider = "file-system" # The actual implementation of the service
|
1189
|
-
|
1190
|
-
def validate(self, **kwargs):
|
1191
|
-
msg = (
|
1192
|
-
"Argo cannot run work with file-system run log store. Unless you have made a mechanism to use volume mounts"
|
1193
|
-
)
|
1194
|
-
logger.warning(msg)
|