This article introduces the ExitHandler and LifecycleHook functions in ArgoWorkflow, which can perform different actions according to the different states of each step of the pipeline, and are generally used to send notifications.
1. General
This article introduces the ExitHandler and LifecycleHook functions in ArgoWorkflow, which can perform different actions according to the different states of each step of the pipeline, and are generally used to send notifications.
For example, send an email notification when a step, or a Workflow execution fails.
There are two implementations in different versions of ArgoWorkflow:
- 1) v2.7 provides an exit handler function, which can specify a template to be executed after the pipeline has finished running. At the same time, this template can also be configured with when field to realize the execution of different processes than the current pipeline result.
- Deprecated, not recommended after v3.3.
- 2) v.3.3 version of the new LifecycleHook, exit handler function is not recommended to use, LifecycleHook provides more granularity and more functionality, exit handler can be seen as a simple LifecycleHook.
2. ExitHandler
Although it is no longer officially recommended to use this feature, it is briefly described.
ArgoWorkflow provides fields to specify a template, and when the workflow executes (either successfully or unsuccessfully) it will run the template specified by onExit.
Similar to the finally field in Tekton
At the same time, this template can use the when field to do conditional configuration. For example, different processes can be executed based on the current pipeline result.
For example, the following demo, the complete workflow content is as follows:
# An exit handler is a template reference that executes at the end of the workflow
# irrespective of the success, failure, or error of the primary workflow. To specify
# an exit handler, reference the name of a template in ''.
# Some common use cases of exit handlers are:
# - sending notifications of workflow status (. e-mail/slack)
# - posting the pass/fail status to a webhook result (. github build result)
# - cleaning up workflow artifacts
# - resubmitting or submitting another workflow
apiVersion: /v1alpha1
kind: Workflow
metadata:
generateName: exit-handlers-
spec:
entrypoint: intentional-fail
onExit: exit-handler
templates:
# primary workflow template
- name: intentional-fail
container:
image: alpine:latest
command: [sh, -c]
args: ["echo intentional failure; exit 1"]
# exit handler related templates
# After the completion of the entrypoint template, the status of the
# workflow is made available in the global variable {{}}.
# {{}} will be one of: Succeeded, Failed, Error
- name: exit-handler
steps:
- - name: notify
template: send-email
- name: celebrate
template: celebrate
when: "{{}} == Succeeded"
- name: cry
template: cry
when: "{{}} != Succeeded"
- name: send-email
container:
image: alpine:latest
command: [sh, -c]
# Tip: {{}} is a JSON list. If you're using bash to read it, we recommend using jq to manipulate
# it. For example:
#
# echo "{{}}" | jq -r '.[] | "Failed Step: \(.displayName)\tMessage: \(.message)"'
#
# Will print a list of all the failed steps and their messages. For more info look up the jq docs.
# Note: jq is not installed by default on the "alpine:latest" image, however it can be installed with "apk add jq"
args: ["echo send e-mail: {{}} {{}} {{}}. Failed steps {{}}"]
- name: celebrate
container:
image: alpine:latest
command: [sh, -c]
args: ["echo hooray!"]
- name: cry
container:
image: alpine:latest
command: [sh, -c]
args: ["echo boohoo!"]
First, a template is configured with the field
spec:
entrypoint: intentional-fail
onExit: exit-handler
The content of this template is as follows:
- name: exit-handler
steps:
- - name: notify
template: send-email
- name: celebrate
template: celebrate
when: "{{}} == Succeeded"
- name: cry
template: cry
when: "{{}} != Succeeded"
Internally there are 3 steps, each of which is a template:
- 1) Send an email, whether it succeeds or fails
- 2) If successful, execute celebrate
- 3) If it fails, execute cry
The Workflow sends an email regardless of the execution result, the content of the email contains information about the execution of the task, if the execution is successful, it will additionally print the execution success, if the execution fails, it will print the execution failure.
For simplicity, all operations are simulated using the echo command.
Since the last thing executed in the main template is theexit 1
command, so it will be judged as an execution failure, and an email will be sent and a failure message will be printed, and the list of Pods is as follows:
[root@argo-1 lifecyclehook]# k get po
NAME READY STATUS RESTARTS AGE
exit-handlers-44ltf 0/2 Error 0 2m45s
exit-handlers-44ltf-cry-1621717811 0/2 Completed 0 2m15s
exit-handlers-44ltf-send-email-2605424148 0/2 Completed 0 2m15s
Individual Pod Logs
[root@argo-1 lifecyclehook]# k logs -f exit-handlers-44ltf-cry-1621717811
boohoo!
time="2024-05-25T11:34:39.472Z" level=info msg="sub-process exited" argo=true error="<nil>"
[root@argo-1 lifecyclehook]# k logs -f exit-handlers-44ltf-send-email-2605424148
send e-mail: exit-handlers-44ltf Failed 30.435347. Failed steps [{"displayName":"exit-handlers-44ltf","message":"Error (exit code 1)","templateName":"intentional-fail","phase":"Failed","podName":"exit-handlers-44ltf","finishedAt":"2024-05-25T11:34:16Z"}]
time="2024-05-25T11:34:44.424Z" level=info msg="sub-process exited" argo=true error="<nil>"
[root@argo-1 lifecyclehook]# k logs -f exit-handlers-44ltf
intentional failure
time="2024-05-25T11:34:15.856Z" level=info msg="sub-process exited" argo=true error="<nil>"
Error: exit status 1
At this point, the exitHandler function can meet our basic notification needs, such as sending the results by email, or interfacing with an external system, Webhook, or more complex needs can also be realized.
However, there is a problem that the exitHandler is at the workflow level, and will only be executed when the entire workflow has finished.
For more granularity, such as at the template level, this is not possible, but the LifecycleHook provided in v3.3 enables more granular notifications.
3. LifecycleHook
LifecycleHook can be seen as a more flexible exit hander, the official description is as follows:
Put differently, an exit handler is like a workflow-level
LifecycleHook
with an expression of== "Succeeded"
or== "Failed"
or== "Error"
.
There are two levels of LifecycleHook:
- Workflow level
- template level
Workflow level
The LifecycleHook and exitHandler at the workflow level are basically similar.
The following is a Workflow level LifecycleHook Demo, the complete Workflow content is as follows:
apiVersion: /v1alpha1
kind: Workflow
metadata:
generateName: lifecycle-hook-
spec:
entrypoint: main
hooks:
exit: # Exit handler
template: http
running:
expression: == "Running"
template: http
templates:
- name: main
steps:
- - name: step1
template: heads
- name: heads
container:
image: alpine:3.6
command: [sh, -c]
args: ["echo \"it was heads\""]
- name: http
http:
# url: /api/v1/employees
url: "/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/"
The first step is to configure the hook
spec:
entrypoint: main
hooks:
exit: # Exit handler
template: http
running:
expression: == "Running"
template: http
As you can see, the original onExit has been replaced by the hooks field, and the hooks field supports specifying multiple hooks, each of which can be set with different conditions via an expression, and will be executed only when the conditions are met.
The template here is a built-in template of type http.
- name: http
http:
# url: /api/v1/employees
url: "/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/"
The main template for this workflow is simple, it just prints a sentence using the echo command, so it will be executed successfully, then both hooks in the hooks will be executed.
Both hooks correspond to the same template, so they are executed twice.
template level
The template level hooks provide more granular configuration, for example, if a user is concerned about the status of a particular step in a workflow, he or she can set a hook for that template individually.
Here's a demo of the template-level hooks, and the full Workflow is below:
apiVersion: /v1alpha1
kind: Workflow
metadata:
generateName: lifecycle-hook-tmpl-level-
spec:
entrypoint: main
templates:
- name: main
steps:
- - name: step-1
hooks:
running: # Name of hook does not matter
# Expr will not support `-` on variable name. Variable should wrap with `[]`
expression: steps["step-1"].status == "Running"
template: http
success:
expression: steps["step-1"].status == "Succeeded"
template: http
template: echo
- - name: step2
hooks:
running:
expression: steps. == "Running"
template: http
success:
expression: steps. == "Succeeded"
template: http
template: echo
- name: echo
container:
image: alpine:3.6
command: [sh, -c]
args: ["echo \"it was heads\""]
- name: http
http:
# url: /api/v1/employees
url: "/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/"
The content is similar to the Workflow-level demo, except that the hooks field is in a different place.
spec:
entrypoint: main
templates:
- name: main
steps:
- - name: step-1
hooks:
# ...
template: echo
- - name: step2
hooks:
# ...
template: echo
We have configured hooks for different steps in this section, which is more flexible than the exiHandler.
How to replace exitHandler
LifecycleHook is a perfect replacement for Exit Handler.is to name the Hook as exitAlthough the name of the hook doesn't matter, it is treated specially if it is an exit.
The original official text is below:
You must not name a
LifecycleHook
exit
or it becomes an exit handler; otherwise the hook name has no relevance.
The exit is written directly into the code as follows:
const (
ExitLifecycleEvent = "exit"
)
func (lchs LifecycleHooks) GetExitHook() *LifecycleHook {
hook, ok := lchs[ExitLifecycleEvent]
if ok {
return &hook
}
return nil
}
func (lchs LifecycleHooks) HasExitHook() bool {
return () != nil
}
Then we can replace the exit handler by simply naming the LifecycleHook exit, like this:
apiVersion: /v1alpha1
kind: Workflow
metadata:
generateName: lifecycle-hook-
spec:
entrypoint: main
hooks:
exit: # if named exit, it'a an Exit handler
template: http
templates:
- name: main
steps:
- - name: step1
template: heads
- name: http
http:
# url: /api/v1/employees
url: "/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/"
4. Common notification templates
Notifications generally support webhook, email, slack, and WeChat notifications.
In ArgoWorkflow it is enough to prepare the corresponding template.
Webhook
This should be the most general way of doing things, and what exactly you do when you receive a message can be flexibly adjusted in the webhook service.
For ArgoWorkflow templates this isfulfillment curl command.So all you need is a container that contains the curl utility
apiVersion: /v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: step-notify-webhook
spec:
templates:
- name: webhook
inputs:
parameters:
- name: POSITIONS # Specify when to run,Multiple separated by comma,for example:Pending,Running,Succeeded,Failed,Error
value: "Succeeded,Failed,Error"
- name: WEBHOOK_ENDPOINT
- name: CURL_VERSION
default: "8.4.0"
container:
image: curlimages/curl:{{.CURL_VERSION}}
command: [sh, -cx]
args: [
"curl -X POST -H \"Content-type: application/json\" -d '{
\"message\": \"{{}} {{}}\",
\"workflow\": {
\"name\": \"{{}}\",
\"namespace\": \"{{}}\",
\"uid\": \"{{}}\",
\"creationTimestamp\": \"{{}}\",
\"status\": \"{{}}\"
}
}'
{{.WEBHOOK_ENDPOINT}}"
]
For the email method, here's a simple demo of sending an email using Python.
# use golangcd-lint for lint
apiVersion: /v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: step-notify-email
spec:
templates:
- name: email
inputs:
parameters:
- name: POSITIONS # Specify when to run,Multiple separated by comma,for example:Pending,Running,Succeeded,Failed,Error
value: "Succeeded,Failed,Error"
- name: CREDENTIALS_SECRET
- name: TO # Recipient Email
- name: PYTHON_VERSION
default: "3.8-alpine"
script:
image: /library/python:{{.PYTHON_VERSION}}
command: [ python ]
env:
- name: TO
value: '{{}}'
- name: HOST
valueFrom:
secretKeyRef:
name: '{{.CREDENTIALS_SECRET}}'
key: host
- name: PORT
valueFrom:
secretKeyRef:
name: '{{.CREDENTIALS_SECRET}}'
key: port
- name: FROM
valueFrom:
secretKeyRef:
name: '{{.CREDENTIALS_SECRET}}'
key: from
- name: USERNAME
valueFrom:
secretKeyRef:
name: '{{.CREDENTIALS_SECRET}}'
key: username
- name: PASSWORD
valueFrom:
secretKeyRef:
name: '{{.CREDENTIALS_SECRET}}'
key: password
- name: TLS
valueFrom:
secretKeyRef:
name: '{{.CREDENTIALS_SECRET}}'
key: tls
source: |
import smtplib
import ssl
import os
from import Header
from import MIMEText
smtp_server = ('HOST')
port = ('PORT')
sender_email = ('FROM')
receiver_emails = ('TO')
user = ('USERNAME')
password = ('PASSWORD')
tls = ('TLS')
# Body of the email,text format
# Building Email Messages
workflow_info = f"""\
"workflow": {{
"name": "{{}}",
"namespace": "{{}}",
"uid": "{{}}",
"creationTimestamp": "{{}}",
"status": "{{}}"
}}
"""
msg = MIMEText(workflow_info, 'plain', 'utf-8')
# header information
msg['From'] = Header(sender_email) # sender
msg['To'] = Header(receiver_emails) # recipient
subject = '{{}} {{}}'
msg['Subject'] = Header(subject, 'utf-8') # Email Subject
if tls == 'True':
context = ssl.create_default_context()
server = smtplib.SMTP_SSL(smtp_server, port, context=context)
else:
server = (smtp_server, port)
if password != '':
(user, password)
for receiver in [item for item in receiver_emails.split(' ') if item]:
(sender_email, receiver, msg.as_string())
()
[ArgoWorkflow Series]Continuously updated, search the public number [Explore Cloud Native]Subscribe to read more articles.
5. Summary
This article analyzes the notification triggering mechanism in Argo, including the old version of exitHandler and the new version of LifecycleHook, and provides several simple notification templates.
Finally, the more flexible LifecycleHook is recommended.