Location>code7788 >text

ArgoWorkflow Tutorial (VIII) ---- based on LifecycleHook pipeline notification alerts

Popularity:980 ℃/2024-10-29 13:33:00

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}}"
        ]

Email

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.