metaflow 2.15.17__py2.py3-none-any.whl → 2.15.19__py2.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.
- metaflow/_vendor/imghdr/__init__.py +180 -0
- metaflow/cli.py +12 -0
- metaflow/cmd/develop/stub_generator.py +19 -2
- metaflow/metaflow_config.py +0 -2
- metaflow/plugins/__init__.py +3 -0
- metaflow/plugins/airflow/airflow.py +6 -0
- metaflow/plugins/argo/argo_workflows.py +316 -287
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/aws/aws_utils.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions.py +6 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_datastore.py +8 -36
- metaflow/plugins/cards/card_decorator.py +57 -1
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
- metaflow/plugins/cards/card_modules/test_cards.py +16 -0
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/pypi/conda_environment.py +8 -4
- metaflow/plugins/pypi/micromamba.py +9 -1
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +9 -173
- metaflow/plugins/secrets/secrets_func.py +60 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/runner/metaflow_runner.py +16 -1
- metaflow/runtime.py +45 -0
- metaflow/version.py +1 -1
- {metaflow-2.15.17.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Tiltfile +27 -2
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/METADATA +2 -2
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/RECORD +39 -30
- {metaflow-2.15.17.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.17.data → metaflow-2.15.19.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/WHEEL +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.17.dist-info → metaflow-2.15.19.dist-info}/top_level.txt +0 -0
@@ -65,6 +65,7 @@ from metaflow.util import (
|
|
65
65
|
)
|
66
66
|
|
67
67
|
from .argo_client import ArgoClient
|
68
|
+
from .exit_hooks import ExitHookHack, HttpExitHook, ContainerHook
|
68
69
|
from metaflow.util import resolve_identity
|
69
70
|
|
70
71
|
|
@@ -795,6 +796,7 @@ class ArgoWorkflows(object):
|
|
795
796
|
|
796
797
|
dag_annotation = {"metaflow/dag": json.dumps(graph_info)}
|
797
798
|
|
799
|
+
lifecycle_hooks = self._lifecycle_hooks()
|
798
800
|
return (
|
799
801
|
WorkflowTemplate()
|
800
802
|
.metadata(
|
@@ -903,97 +905,20 @@ class ArgoWorkflows(object):
|
|
903
905
|
if self.enable_error_msg_capture
|
904
906
|
else None
|
905
907
|
)
|
906
|
-
# Set
|
908
|
+
# Set lifecycle hooks if notifications are enabled
|
907
909
|
.hooks(
|
908
910
|
{
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
"notify-slack-on-success": LifecycleHook()
|
913
|
-
.expression("workflow.status == 'Succeeded'")
|
914
|
-
.template("notify-slack-on-success"),
|
915
|
-
}
|
916
|
-
if self.notify_on_success and self.notify_slack_webhook_url
|
917
|
-
else {}
|
918
|
-
),
|
919
|
-
**(
|
920
|
-
{
|
921
|
-
# workflow status maps to Completed
|
922
|
-
"notify-pager-duty-on-success": LifecycleHook()
|
923
|
-
.expression("workflow.status == 'Succeeded'")
|
924
|
-
.template("notify-pager-duty-on-success"),
|
925
|
-
}
|
926
|
-
if self.notify_on_success
|
927
|
-
and self.notify_pager_duty_integration_key
|
928
|
-
else {}
|
929
|
-
),
|
930
|
-
**(
|
931
|
-
{
|
932
|
-
# workflow status maps to Completed
|
933
|
-
"notify-incident-io-on-success": LifecycleHook()
|
934
|
-
.expression("workflow.status == 'Succeeded'")
|
935
|
-
.template("notify-incident-io-on-success"),
|
936
|
-
}
|
937
|
-
if self.notify_on_success
|
938
|
-
and self.notify_incident_io_api_key
|
939
|
-
else {}
|
940
|
-
),
|
941
|
-
**(
|
942
|
-
{
|
943
|
-
# workflow status maps to Failed or Error
|
944
|
-
"notify-slack-on-failure": LifecycleHook()
|
945
|
-
.expression("workflow.status == 'Failed'")
|
946
|
-
.template("notify-slack-on-error"),
|
947
|
-
"notify-slack-on-error": LifecycleHook()
|
948
|
-
.expression("workflow.status == 'Error'")
|
949
|
-
.template("notify-slack-on-error"),
|
950
|
-
}
|
951
|
-
if self.notify_on_error and self.notify_slack_webhook_url
|
952
|
-
else {}
|
953
|
-
),
|
954
|
-
**(
|
955
|
-
{
|
956
|
-
# workflow status maps to Failed or Error
|
957
|
-
"notify-pager-duty-on-failure": LifecycleHook()
|
958
|
-
.expression("workflow.status == 'Failed'")
|
959
|
-
.template("notify-pager-duty-on-error"),
|
960
|
-
"notify-pager-duty-on-error": LifecycleHook()
|
961
|
-
.expression("workflow.status == 'Error'")
|
962
|
-
.template("notify-pager-duty-on-error"),
|
963
|
-
}
|
964
|
-
if self.notify_on_error
|
965
|
-
and self.notify_pager_duty_integration_key
|
966
|
-
else {}
|
967
|
-
),
|
968
|
-
**(
|
969
|
-
{
|
970
|
-
# workflow status maps to Failed or Error
|
971
|
-
"notify-incident-io-on-failure": LifecycleHook()
|
972
|
-
.expression("workflow.status == 'Failed'")
|
973
|
-
.template("notify-incident-io-on-error"),
|
974
|
-
"notify-incident-io-on-error": LifecycleHook()
|
975
|
-
.expression("workflow.status == 'Error'")
|
976
|
-
.template("notify-incident-io-on-error"),
|
977
|
-
}
|
978
|
-
if self.notify_on_error and self.notify_incident_io_api_key
|
979
|
-
else {}
|
980
|
-
),
|
981
|
-
# Warning: terrible hack to workaround a bug in Argo Workflow
|
982
|
-
# where the hooks listed above do not execute unless
|
983
|
-
# there is an explicit exit hook. as and when this
|
984
|
-
# bug is patched, we should remove this effectively
|
985
|
-
# no-op hook.
|
986
|
-
**(
|
987
|
-
{"exit": LifecycleHook().template("exit-hook-hack")}
|
988
|
-
if self.notify_on_error or self.notify_on_success
|
989
|
-
else {}
|
990
|
-
),
|
911
|
+
lifecycle.name: lifecycle
|
912
|
+
for hook in lifecycle_hooks
|
913
|
+
for lifecycle in hook.lifecycle_hooks
|
991
914
|
}
|
992
915
|
)
|
993
916
|
# Top-level DAG template(s)
|
994
917
|
.templates(self._dag_templates())
|
995
918
|
# Container templates
|
996
919
|
.templates(self._container_templates())
|
920
|
+
# Lifecycle hook template(s)
|
921
|
+
.templates([hook.template for hook in lifecycle_hooks])
|
997
922
|
# Exit hook template(s)
|
998
923
|
.templates(self._exit_hook_templates())
|
999
924
|
# Sidecar templates (Daemon Containers)
|
@@ -2336,40 +2261,186 @@ class ArgoWorkflows(object):
|
|
2336
2261
|
templates.append(self._heartbeat_daemon_template())
|
2337
2262
|
return templates
|
2338
2263
|
|
2339
|
-
# Return
|
2340
|
-
def
|
2341
|
-
|
2264
|
+
# Return lifecycle hooks for workflow execution notifications.
|
2265
|
+
def _lifecycle_hooks(self):
|
2266
|
+
hooks = []
|
2342
2267
|
if self.notify_on_error:
|
2343
|
-
|
2344
|
-
|
2345
|
-
|
2268
|
+
hooks.append(self._slack_error_template())
|
2269
|
+
hooks.append(self._pager_duty_alert_template())
|
2270
|
+
hooks.append(self._incident_io_alert_template())
|
2346
2271
|
if self.notify_on_success:
|
2347
|
-
|
2348
|
-
|
2349
|
-
|
2272
|
+
hooks.append(self._slack_success_template())
|
2273
|
+
hooks.append(self._pager_duty_change_template())
|
2274
|
+
hooks.append(self._incident_io_change_template())
|
2275
|
+
|
2276
|
+
exit_hook_decos = self.flow._flow_decorators.get("exit_hook", [])
|
2277
|
+
|
2278
|
+
for deco in exit_hook_decos:
|
2279
|
+
hooks.extend(self._lifecycle_hook_from_deco(deco))
|
2350
2280
|
|
2351
2281
|
# Clean up None values from templates.
|
2352
|
-
|
2353
|
-
|
2354
|
-
if
|
2355
|
-
|
2356
|
-
|
2357
|
-
|
2358
|
-
# remove this effectively no-op template.
|
2359
|
-
# Note: We use the Http template because changing this to an actual no-op container had the side-effect of
|
2360
|
-
# leaving LifecycleHooks in a pending state even when they have finished execution.
|
2361
|
-
templates.append(
|
2362
|
-
Template("exit-hook-hack").http(
|
2363
|
-
Http("GET")
|
2364
|
-
.url(
|
2282
|
+
hooks = list(filter(None, hooks))
|
2283
|
+
|
2284
|
+
if hooks:
|
2285
|
+
hooks.append(
|
2286
|
+
ExitHookHack(
|
2287
|
+
url=(
|
2365
2288
|
self.notify_slack_webhook_url
|
2366
2289
|
or "https://events.pagerduty.com/v2/enqueue"
|
2367
2290
|
)
|
2368
|
-
.success_condition("true == true")
|
2369
2291
|
)
|
2370
2292
|
)
|
2293
|
+
return hooks
|
2294
|
+
|
2295
|
+
def _lifecycle_hook_from_deco(self, deco):
|
2296
|
+
from kubernetes import client as kubernetes_sdk
|
2297
|
+
|
2298
|
+
start_step = [step for step in self.graph if step.name == "start"][0]
|
2299
|
+
# We want to grab the base image used by the start step, as this is known to be pullable from within the cluster,
|
2300
|
+
# and it might contain the required libraries, allowing us to start up faster.
|
2301
|
+
resources = dict(
|
2302
|
+
[deco for deco in start_step.decorators if deco.name == "kubernetes"][
|
2303
|
+
0
|
2304
|
+
].attributes
|
2305
|
+
)
|
2306
|
+
|
2307
|
+
run_id_template = "argo-{{workflow.name}}"
|
2308
|
+
metaflow_version = self.environment.get_environment_info()
|
2309
|
+
metaflow_version["flow_name"] = self.graph.name
|
2310
|
+
metaflow_version["production_token"] = self.production_token
|
2311
|
+
env = {
|
2312
|
+
# These values are needed by Metaflow to set it's internal
|
2313
|
+
# state appropriately.
|
2314
|
+
"METAFLOW_CODE_URL": self.code_package_url,
|
2315
|
+
"METAFLOW_CODE_SHA": self.code_package_sha,
|
2316
|
+
"METAFLOW_CODE_DS": self.flow_datastore.TYPE,
|
2317
|
+
"METAFLOW_SERVICE_URL": SERVICE_INTERNAL_URL,
|
2318
|
+
"METAFLOW_SERVICE_HEADERS": json.dumps(SERVICE_HEADERS),
|
2319
|
+
"METAFLOW_USER": "argo-workflows",
|
2320
|
+
"METAFLOW_DEFAULT_DATASTORE": self.flow_datastore.TYPE,
|
2321
|
+
"METAFLOW_DEFAULT_METADATA": DEFAULT_METADATA,
|
2322
|
+
"METAFLOW_OWNER": self.username,
|
2323
|
+
}
|
2324
|
+
# pass on the Run pathspec for script
|
2325
|
+
env["RUN_PATHSPEC"] = f"{self.graph.name}/{run_id_template}"
|
2326
|
+
|
2327
|
+
# support Metaflow sandboxes
|
2328
|
+
env["METAFLOW_INIT_SCRIPT"] = KUBERNETES_SANDBOX_INIT_SCRIPT
|
2329
|
+
|
2330
|
+
env["METAFLOW_WORKFLOW_NAME"] = "{{workflow.name}}"
|
2331
|
+
env["METAFLOW_WORKFLOW_NAMESPACE"] = "{{workflow.namespace}}"
|
2332
|
+
env = {
|
2333
|
+
k: v
|
2334
|
+
for k, v in env.items()
|
2335
|
+
if v is not None
|
2336
|
+
and k not in set(ARGO_WORKFLOWS_ENV_VARS_TO_SKIP.split(","))
|
2337
|
+
}
|
2338
|
+
|
2339
|
+
def _cmd(fn_name):
|
2340
|
+
mflog_expr = export_mflog_env_vars(
|
2341
|
+
datastore_type=self.flow_datastore.TYPE,
|
2342
|
+
stdout_path="$PWD/.logs/mflog_stdout",
|
2343
|
+
stderr_path="$PWD/.logs/mflog_stderr",
|
2344
|
+
flow_name=self.flow.name,
|
2345
|
+
run_id=run_id_template,
|
2346
|
+
step_name=f"_hook_{fn_name}",
|
2347
|
+
task_id="1",
|
2348
|
+
retry_count="0",
|
2349
|
+
)
|
2350
|
+
cmds = " && ".join(
|
2351
|
+
[
|
2352
|
+
# For supporting sandboxes, ensure that a custom script is executed
|
2353
|
+
# before anything else is executed. The script is passed in as an
|
2354
|
+
# env var.
|
2355
|
+
'${METAFLOW_INIT_SCRIPT:+eval \\"${METAFLOW_INIT_SCRIPT}\\"}',
|
2356
|
+
"mkdir -p $PWD/.logs",
|
2357
|
+
mflog_expr,
|
2358
|
+
]
|
2359
|
+
+ self.environment.get_package_commands(
|
2360
|
+
self.code_package_url, self.flow_datastore.TYPE
|
2361
|
+
)[:-1]
|
2362
|
+
# Replace the line 'Task in starting'
|
2363
|
+
+ [f"mflog 'Lifecycle hook {fn_name} is starting.'"]
|
2364
|
+
+ [
|
2365
|
+
f"python -m metaflow.plugins.exit_hook.exit_hook_script {metaflow_version['script']} {fn_name} $RUN_PATHSPEC"
|
2366
|
+
]
|
2367
|
+
)
|
2368
|
+
|
2369
|
+
cmds = shlex.split('bash -c "%s"' % cmds)
|
2370
|
+
return cmds
|
2371
|
+
|
2372
|
+
def _container(cmds):
|
2373
|
+
return to_camelcase(
|
2374
|
+
kubernetes_sdk.V1Container(
|
2375
|
+
name="main",
|
2376
|
+
command=cmds,
|
2377
|
+
image=deco.attributes["options"].get("image", None)
|
2378
|
+
or resources["image"],
|
2379
|
+
env=[
|
2380
|
+
kubernetes_sdk.V1EnvVar(name=k, value=str(v))
|
2381
|
+
for k, v in env.items()
|
2382
|
+
],
|
2383
|
+
env_from=[
|
2384
|
+
kubernetes_sdk.V1EnvFromSource(
|
2385
|
+
secret_ref=kubernetes_sdk.V1SecretEnvSource(
|
2386
|
+
name=str(k),
|
2387
|
+
# optional=True
|
2388
|
+
)
|
2389
|
+
)
|
2390
|
+
for k in list(
|
2391
|
+
[]
|
2392
|
+
if not resources.get("secrets")
|
2393
|
+
else (
|
2394
|
+
[resources.get("secrets")]
|
2395
|
+
if isinstance(resources.get("secrets"), str)
|
2396
|
+
else resources.get("secrets")
|
2397
|
+
)
|
2398
|
+
)
|
2399
|
+
+ KUBERNETES_SECRETS.split(",")
|
2400
|
+
+ ARGO_WORKFLOWS_KUBERNETES_SECRETS.split(",")
|
2401
|
+
if k
|
2402
|
+
],
|
2403
|
+
resources=kubernetes_sdk.V1ResourceRequirements(
|
2404
|
+
# NOTE: base resources for this are kept to a minimum to save on running costs.
|
2405
|
+
requests={
|
2406
|
+
"cpu": "200m",
|
2407
|
+
"memory": "100Mi",
|
2408
|
+
},
|
2409
|
+
limits={
|
2410
|
+
"cpu": "200m",
|
2411
|
+
"memory": "500Mi",
|
2412
|
+
},
|
2413
|
+
),
|
2414
|
+
).to_dict()
|
2415
|
+
)
|
2416
|
+
|
2417
|
+
# create lifecycle hooks from deco
|
2418
|
+
hooks = []
|
2419
|
+
for success_fn_name in deco.success_hooks:
|
2420
|
+
hook = ContainerHook(
|
2421
|
+
name=f"success-{success_fn_name.replace('_', '-')}",
|
2422
|
+
container=_container(cmds=_cmd(success_fn_name)),
|
2423
|
+
service_account_name=resources["service_account"],
|
2424
|
+
on_success=True,
|
2425
|
+
)
|
2426
|
+
hooks.append(hook)
|
2427
|
+
|
2428
|
+
for error_fn_name in deco.error_hooks:
|
2429
|
+
hook = ContainerHook(
|
2430
|
+
name=f"error-{error_fn_name.replace('_', '-')}",
|
2431
|
+
service_account_name=resources["service_account"],
|
2432
|
+
container=_container(cmds=_cmd(error_fn_name)),
|
2433
|
+
on_error=True,
|
2434
|
+
)
|
2435
|
+
hooks.append(hook)
|
2436
|
+
|
2437
|
+
return hooks
|
2438
|
+
|
2439
|
+
def _exit_hook_templates(self):
|
2440
|
+
templates = []
|
2371
2441
|
if self.enable_error_msg_capture:
|
2372
2442
|
templates.extend(self._error_msg_capture_hook_templates())
|
2443
|
+
|
2373
2444
|
return templates
|
2374
2445
|
|
2375
2446
|
def _error_msg_capture_hook_templates(self):
|
@@ -2518,30 +2589,30 @@ class ArgoWorkflows(object):
|
|
2518
2589
|
# https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgx-send-an-alert-event
|
2519
2590
|
if self.notify_pager_duty_integration_key is None:
|
2520
2591
|
return None
|
2521
|
-
return
|
2522
|
-
|
2523
|
-
|
2524
|
-
.
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
|
2530
|
-
|
2531
|
-
|
2532
|
-
|
2533
|
-
|
2534
|
-
|
2535
|
-
|
2536
|
-
|
2537
|
-
|
2538
|
-
|
2539
|
-
},
|
2592
|
+
return HttpExitHook(
|
2593
|
+
name="notify-pager-duty-on-error",
|
2594
|
+
method="POST",
|
2595
|
+
url="https://events.pagerduty.com/v2/enqueue",
|
2596
|
+
headers={"Content-Type": "application/json"},
|
2597
|
+
body=json.dumps(
|
2598
|
+
{
|
2599
|
+
"event_action": "trigger",
|
2600
|
+
"routing_key": self.notify_pager_duty_integration_key,
|
2601
|
+
# "dedup_key": self.flow.name, # TODO: Do we need deduplication?
|
2602
|
+
"payload": {
|
2603
|
+
"source": "{{workflow.name}}",
|
2604
|
+
"severity": "info",
|
2605
|
+
"summary": "Metaflow run %s/argo-{{workflow.name}} failed!"
|
2606
|
+
% self.flow.name,
|
2607
|
+
"custom_details": {
|
2608
|
+
"Flow": self.flow.name,
|
2609
|
+
"Run ID": "argo-{{workflow.name}}",
|
2540
2610
|
},
|
2541
|
-
|
2542
|
-
|
2543
|
-
|
2544
|
-
)
|
2611
|
+
},
|
2612
|
+
"links": self._pager_duty_notification_links(),
|
2613
|
+
}
|
2614
|
+
),
|
2615
|
+
on_error=True,
|
2545
2616
|
)
|
2546
2617
|
|
2547
2618
|
def _incident_io_alert_template(self):
|
@@ -2552,50 +2623,52 @@ class ArgoWorkflows(object):
|
|
2552
2623
|
"Creating alerts for errors requires a alert source config ID."
|
2553
2624
|
)
|
2554
2625
|
ui_links = self._incident_io_ui_urls_for_run()
|
2555
|
-
return
|
2556
|
-
|
2557
|
-
|
2626
|
+
return HttpExitHook(
|
2627
|
+
name="notify-incident-io-on-error",
|
2628
|
+
method="POST",
|
2629
|
+
url=(
|
2558
2630
|
"https://api.incident.io/v2/alert_events/http/%s"
|
2559
2631
|
% self.incident_io_alert_source_config_id
|
2560
|
-
)
|
2561
|
-
|
2562
|
-
|
2563
|
-
|
2564
|
-
|
2565
|
-
|
2566
|
-
|
2567
|
-
|
2568
|
-
|
2569
|
-
|
2570
|
-
|
2571
|
-
|
2572
|
-
|
2573
|
-
|
2574
|
-
|
2575
|
-
|
2576
|
-
|
2577
|
-
),
|
2578
|
-
"source_url": (
|
2579
|
-
"%s/%s/%s"
|
2580
|
-
% (
|
2581
|
-
UI_URL.rstrip("/"),
|
2582
|
-
self.flow.name,
|
2583
|
-
"argo-{{workflow.name}}",
|
2584
|
-
)
|
2585
|
-
if UI_URL
|
2586
|
-
else None
|
2632
|
+
),
|
2633
|
+
headers={
|
2634
|
+
"Content-Type": "application/json",
|
2635
|
+
"Authorization": "Bearer %s" % self.notify_incident_io_api_key,
|
2636
|
+
},
|
2637
|
+
body=json.dumps(
|
2638
|
+
{
|
2639
|
+
"idempotency_key": "argo-{{workflow.name}}", # use run id to deduplicate alerts.
|
2640
|
+
"status": "firing",
|
2641
|
+
"title": "Flow %s has failed." % self.flow.name,
|
2642
|
+
"description": "Metaflow run {run_pathspec} failed!{urls}".format(
|
2643
|
+
run_pathspec="%s/argo-{{workflow.name}}" % self.flow.name,
|
2644
|
+
urls=(
|
2645
|
+
"\n\nSee details for the run at:\n\n"
|
2646
|
+
+ "\n\n".join(ui_links)
|
2647
|
+
if ui_links
|
2648
|
+
else ""
|
2587
2649
|
),
|
2588
|
-
|
2589
|
-
|
2590
|
-
|
2591
|
-
|
2592
|
-
|
2593
|
-
|
2594
|
-
},
|
2650
|
+
),
|
2651
|
+
"source_url": (
|
2652
|
+
"%s/%s/%s"
|
2653
|
+
% (
|
2654
|
+
UI_URL.rstrip("/"),
|
2655
|
+
self.flow.name,
|
2656
|
+
"argo-{{workflow.name}}",
|
2657
|
+
)
|
2658
|
+
if UI_URL
|
2659
|
+
else None
|
2660
|
+
),
|
2661
|
+
"metadata": {
|
2662
|
+
**(self.incident_io_metadata or {}),
|
2663
|
+
**{
|
2664
|
+
"run_status": "failed",
|
2665
|
+
"flow_name": self.flow.name,
|
2666
|
+
"run_id": "argo-{{workflow.name}}",
|
2595
2667
|
},
|
2596
|
-
}
|
2597
|
-
|
2598
|
-
)
|
2668
|
+
},
|
2669
|
+
}
|
2670
|
+
),
|
2671
|
+
on_error=True,
|
2599
2672
|
)
|
2600
2673
|
|
2601
2674
|
def _incident_io_change_template(self):
|
@@ -2606,50 +2679,52 @@ class ArgoWorkflows(object):
|
|
2606
2679
|
"Creating alerts for successes requires an alert source config ID."
|
2607
2680
|
)
|
2608
2681
|
ui_links = self._incident_io_ui_urls_for_run()
|
2609
|
-
return
|
2610
|
-
|
2611
|
-
|
2682
|
+
return HttpExitHook(
|
2683
|
+
name="notify-incident-io-on-success",
|
2684
|
+
method="POST",
|
2685
|
+
url=(
|
2612
2686
|
"https://api.incident.io/v2/alert_events/http/%s"
|
2613
2687
|
% self.incident_io_alert_source_config_id
|
2614
|
-
)
|
2615
|
-
|
2616
|
-
|
2617
|
-
|
2618
|
-
|
2619
|
-
|
2620
|
-
|
2621
|
-
|
2622
|
-
|
2623
|
-
|
2624
|
-
|
2625
|
-
|
2626
|
-
|
2627
|
-
|
2628
|
-
|
2629
|
-
|
2630
|
-
|
2631
|
-
),
|
2632
|
-
"source_url": (
|
2633
|
-
"%s/%s/%s"
|
2634
|
-
% (
|
2635
|
-
UI_URL.rstrip("/"),
|
2636
|
-
self.flow.name,
|
2637
|
-
"argo-{{workflow.name}}",
|
2638
|
-
)
|
2639
|
-
if UI_URL
|
2640
|
-
else None
|
2688
|
+
),
|
2689
|
+
headers={
|
2690
|
+
"Content-Type": "application/json",
|
2691
|
+
"Authorization": "Bearer %s" % self.notify_incident_io_api_key,
|
2692
|
+
},
|
2693
|
+
body=json.dumps(
|
2694
|
+
{
|
2695
|
+
"idempotency_key": "argo-{{workflow.name}}", # use run id to deduplicate alerts.
|
2696
|
+
"status": "firing",
|
2697
|
+
"title": "Flow %s has succeeded." % self.flow.name,
|
2698
|
+
"description": "Metaflow run {run_pathspec} succeeded!{urls}".format(
|
2699
|
+
run_pathspec="%s/argo-{{workflow.name}}" % self.flow.name,
|
2700
|
+
urls=(
|
2701
|
+
"\n\nSee details for the run at:\n\n"
|
2702
|
+
+ "\n\n".join(ui_links)
|
2703
|
+
if ui_links
|
2704
|
+
else ""
|
2641
2705
|
),
|
2642
|
-
|
2643
|
-
|
2644
|
-
|
2645
|
-
|
2646
|
-
|
2647
|
-
|
2648
|
-
},
|
2706
|
+
),
|
2707
|
+
"source_url": (
|
2708
|
+
"%s/%s/%s"
|
2709
|
+
% (
|
2710
|
+
UI_URL.rstrip("/"),
|
2711
|
+
self.flow.name,
|
2712
|
+
"argo-{{workflow.name}}",
|
2713
|
+
)
|
2714
|
+
if UI_URL
|
2715
|
+
else None
|
2716
|
+
),
|
2717
|
+
"metadata": {
|
2718
|
+
**(self.incident_io_metadata or {}),
|
2719
|
+
**{
|
2720
|
+
"run_status": "succeeded",
|
2721
|
+
"flow_name": self.flow.name,
|
2722
|
+
"run_id": "argo-{{workflow.name}}",
|
2649
2723
|
},
|
2650
|
-
}
|
2651
|
-
|
2652
|
-
)
|
2724
|
+
},
|
2725
|
+
}
|
2726
|
+
),
|
2727
|
+
on_success=True,
|
2653
2728
|
)
|
2654
2729
|
|
2655
2730
|
def _incident_io_ui_urls_for_run(self):
|
@@ -2674,27 +2749,27 @@ class ArgoWorkflows(object):
|
|
2674
2749
|
# https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgy-send-a-change-event
|
2675
2750
|
if self.notify_pager_duty_integration_key is None:
|
2676
2751
|
return None
|
2677
|
-
return
|
2678
|
-
|
2679
|
-
|
2680
|
-
.
|
2681
|
-
|
2682
|
-
|
2683
|
-
|
2684
|
-
|
2685
|
-
|
2686
|
-
|
2687
|
-
|
2688
|
-
|
2689
|
-
|
2690
|
-
|
2691
|
-
|
2692
|
-
},
|
2752
|
+
return HttpExitHook(
|
2753
|
+
name="notify-pager-duty-on-success",
|
2754
|
+
method="POST",
|
2755
|
+
url="https://events.pagerduty.com/v2/change/enqueue",
|
2756
|
+
headers={"Content-Type": "application/json"},
|
2757
|
+
body=json.dumps(
|
2758
|
+
{
|
2759
|
+
"routing_key": self.notify_pager_duty_integration_key,
|
2760
|
+
"payload": {
|
2761
|
+
"summary": "Metaflow run %s/argo-{{workflow.name}} Succeeded"
|
2762
|
+
% self.flow.name,
|
2763
|
+
"source": "{{workflow.name}}",
|
2764
|
+
"custom_details": {
|
2765
|
+
"Flow": self.flow.name,
|
2766
|
+
"Run ID": "argo-{{workflow.name}}",
|
2693
2767
|
},
|
2694
|
-
|
2695
|
-
|
2696
|
-
|
2697
|
-
)
|
2768
|
+
},
|
2769
|
+
"links": self._pager_duty_notification_links(),
|
2770
|
+
}
|
2771
|
+
),
|
2772
|
+
on_success=True,
|
2698
2773
|
)
|
2699
2774
|
|
2700
2775
|
def _pager_duty_notification_links(self):
|
@@ -2798,8 +2873,12 @@ class ArgoWorkflows(object):
|
|
2798
2873
|
blocks = self._get_slack_blocks(message)
|
2799
2874
|
payload = {"text": message, "blocks": blocks}
|
2800
2875
|
|
2801
|
-
return
|
2802
|
-
|
2876
|
+
return HttpExitHook(
|
2877
|
+
name="notify-slack-on-error",
|
2878
|
+
method="POST",
|
2879
|
+
url=self.notify_slack_webhook_url,
|
2880
|
+
body=json.dumps(payload),
|
2881
|
+
on_error=True,
|
2803
2882
|
)
|
2804
2883
|
|
2805
2884
|
def _slack_success_template(self):
|
@@ -2814,8 +2893,12 @@ class ArgoWorkflows(object):
|
|
2814
2893
|
blocks = self._get_slack_blocks(message)
|
2815
2894
|
payload = {"text": message, "blocks": blocks}
|
2816
2895
|
|
2817
|
-
return
|
2818
|
-
|
2896
|
+
return HttpExitHook(
|
2897
|
+
name="notify-slack-on-success",
|
2898
|
+
method="POST",
|
2899
|
+
url=self.notify_slack_webhook_url,
|
2900
|
+
body=json.dumps(payload),
|
2901
|
+
on_success=True,
|
2819
2902
|
)
|
2820
2903
|
|
2821
2904
|
def _heartbeat_daemon_template(self):
|
@@ -4186,57 +4269,3 @@ class TriggerParameter(object):
|
|
4186
4269
|
|
4187
4270
|
def __str__(self):
|
4188
4271
|
return json.dumps(self.payload, indent=4)
|
4189
|
-
|
4190
|
-
|
4191
|
-
class Http(object):
|
4192
|
-
# https://argoproj.github.io/argo-workflows/fields/#http
|
4193
|
-
|
4194
|
-
def __init__(self, method):
|
4195
|
-
tree = lambda: defaultdict(tree)
|
4196
|
-
self.payload = tree()
|
4197
|
-
self.payload["method"] = method
|
4198
|
-
self.payload["headers"] = []
|
4199
|
-
|
4200
|
-
def header(self, header, value):
|
4201
|
-
self.payload["headers"].append({"name": header, "value": value})
|
4202
|
-
return self
|
4203
|
-
|
4204
|
-
def body(self, body):
|
4205
|
-
self.payload["body"] = str(body)
|
4206
|
-
return self
|
4207
|
-
|
4208
|
-
def url(self, url):
|
4209
|
-
self.payload["url"] = url
|
4210
|
-
return self
|
4211
|
-
|
4212
|
-
def success_condition(self, success_condition):
|
4213
|
-
self.payload["successCondition"] = success_condition
|
4214
|
-
return self
|
4215
|
-
|
4216
|
-
def to_json(self):
|
4217
|
-
return self.payload
|
4218
|
-
|
4219
|
-
def __str__(self):
|
4220
|
-
return json.dumps(self.payload, indent=4)
|
4221
|
-
|
4222
|
-
|
4223
|
-
class LifecycleHook(object):
|
4224
|
-
# https://argoproj.github.io/argo-workflows/fields/#lifecyclehook
|
4225
|
-
|
4226
|
-
def __init__(self):
|
4227
|
-
tree = lambda: defaultdict(tree)
|
4228
|
-
self.payload = tree()
|
4229
|
-
|
4230
|
-
def expression(self, expression):
|
4231
|
-
self.payload["expression"] = str(expression)
|
4232
|
-
return self
|
4233
|
-
|
4234
|
-
def template(self, template):
|
4235
|
-
self.payload["template"] = template
|
4236
|
-
return self
|
4237
|
-
|
4238
|
-
def to_json(self):
|
4239
|
-
return self.payload
|
4240
|
-
|
4241
|
-
def __str__(self):
|
4242
|
-
return json.dumps(self.payload, indent=4)
|