動態

詳情 返回 返回

ArgoWorkflow教程(八)---基於 LifecycleHook 實現流水線通知提醒 - 動態 詳情

argoworkflow-8-workflow-notify.png

本篇介紹一下 ArgoWorkflow 中的 ExitHandler 和 LifecycleHook 功能,可以根據流水線每一步的不同狀態,執行不同操作,一般用於發送通知。

<!--more-->

1. 概述

本篇介紹一下 ArgoWorkflow 中的 ExitHandler 和 LifecycleHook 功能,可以根據流水線每一步的不同狀態,執行不同操作,一般用於發送通知。

比如當某個步驟,或者某個 Workflow 執行失敗時,發送郵件通知。

在 ArgoWorkflow 不同版本中中有兩種實現方式:

  • 1)v2.7 版本開始提供了 exit handler 功能,可以指定一個在流水線運行完成後執行的模板。同時這個模板中還可以使用 when 字段來做條件配置,以實現比根據當前流水線運行結果來執行不同流程。

    • 已廢棄,v3.3 版本後不推薦使用
  • 2)v.3.3 版本新增 LifecycleHook,exit handler 功能則不推薦使用了,LifecycleHook 提供了更細粒度以及更多功能,exit handler 可以看做是一個簡單的 LifecycleHook。

2. ExitHandler

雖然官方已經不推薦使用該功能了,但是還是簡單介紹一下。

ArgoWorkflow 提供了 spec.onExit 字段,可以指定一個 template,當 workflow 執行後(不論成功或者失敗)就會運行 onExit 指定的 template。

類似於 Tekton 中的 finally 字段

同時這個 template 中可以使用 when 字段來做條件配置。比如根據當前流水線運行結果來執行不同流程。

比如下面這個 Demo,完整 Workflow 內容如下:

# 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 'spec.onExit'.
# Some common use cases of exit handlers are:
# - sending notifications of workflow status (e.g. e-mail/slack)
# - posting the pass/fail status to a webhook result (e.g. github build result)
# - cleaning up workflow artifacts
# - resubmitting or submitting another workflow
apiVersion: argoproj.io/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 {{workflow.status}}.
    # {{workflow.status}} will be one of: Succeeded, Failed, Error
    - name: exit-handler
      steps:
        - - name: notify
            template: send-email
          - name: celebrate
            template: celebrate
            when: "{{workflow.status}} == Succeeded"
          - name: cry
            template: cry
            when: "{{workflow.status}} != Succeeded"
    - name: send-email
      container:
        image: alpine:latest
        command: [sh, -c]
        # Tip: {{workflow.failures}} is a JSON list. If you're using bash to read it, we recommend using jq to manipulate
        # it. For example:
        #
        # echo "{{workflow.failures}}" | 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: {{workflow.name}} {{workflow.status}} {{workflow.duration}}. Failed steps {{workflow.failures}}"]
    - name: celebrate
      container:
        image: alpine:latest
        command: [sh, -c]
        args: ["echo hooray!"]
    - name: cry
      container:
        image: alpine:latest
        command: [sh, -c]
        args: ["echo boohoo!"]

首先是通過 spec.onExit 字段配置了一個 template

spec:
  entrypoint: intentional-fail
  onExit: exit-handler

這個 template 內容如下:

    - name: exit-handler
      steps:
        - - name: notify
            template: send-email
          - name: celebrate
            template: celebrate
            when: "{{workflow.status}} == Succeeded"
          - name: cry
            template: cry
            when: "{{workflow.status}} != Succeeded"

內部包含 3 個步驟,每個步驟又是一個 template:

  • 1)發送郵件,無論成功或者失敗
  • 2)若成功則執行 celebrate
  • 3)若失敗則執行 cry

該 Workflow 不論執行結果如何,都會發送郵件,郵件內容包含了任務的執行信息,若是執行成功則會額外打印執行成功,若是執行失敗則會打印執行失敗。

為了簡單,這裏所有操作都使用 echo 命令進行模擬

由於在主 template 中最後執行的是 exit 1 命令,因此會判斷為執行失敗,會發送郵件並打印失敗信息,Pod 列表如下:

[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

各個 Pod 日誌

[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

至此,這個 exitHandler 功能就可以滿足我們基本的通知需求了,比如將結果以郵件發出,或者對接外部系統 Webhook,更加複雜的需求也可以實現。

不過存在一個問題,就是 exitHandler 是 Workflow 級別的,只能整個 Workflow 執行完成才會執行 exitHandler。

如果想要更細粒度的,比如 template 級別則做不到,v3.3 中提供的 LifecycleHook 則實現了更加細粒度的通知。

3. LifecycleHook

LifecycleHook 可以看做是一個比較靈活的 exit hander,官方描述如下:

Put differently, an exit handler is like a workflow-level LifecycleHook with an expression of workflow.status == "Succeeded" or workflow.status == "Failed" or workflow.status == "Error".

LifecycleHook 有兩種級別:

  • Workflow 級別
  • template 級別

Workflow 級別

Workflow 級別的 LifecycleHook 和 exitHandler 基本類似。

下面就是一個 Workflow 級別的 LifecycleHook Demo,完整 Workflow 內容如下:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: lifecycle-hook-
spec:
  entrypoint: main
  hooks:
    exit: # Exit handler
      template: http
    running:
      expression: workflow.status == "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: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

首先是配置 hook

spec:
  entrypoint: main
  hooks:
    exit: # Exit handler
      template: http
    running:
      expression: workflow.status == "Running"
      template: http

可以看到,原有的 onExit 被 hooks 字段替代了,同時 hooks 字段支持指定多個 hook,每個 hook 中可以通過 expression 設置不同的條件,只有滿足條件時才會執行。

這裏的 template 則是一個內置的 http 類型的 template

    - name: http
      http:
        # url: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

該 Workflow 的主 template 比較簡單,就是使用 echo 命令打印一句話,因此會執行成功,那麼 hooks 中的兩個 hooks 都會執行。

兩個 hook 對應的都是同一個 template,因此會執行兩遍。

template 級別

template 級別的 hooks 則是提供了更細粒度的配置,比如可能用户比較關心 Workflow 中某一個步驟的狀態,可以單獨為該 template 設置 hook。

下面是一個template 級別的 hooks demo,Workflow 完整內容如下:

apiVersion: argoproj.io/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.step2.status == "Running"
                template: http
              success:
                expression: steps.step2.status == "Succeeded"
                template: http
            template: echo

    - name: echo
      container:
        image: alpine:3.6
        command: [sh, -c]
        args: ["echo \"it was heads\""]

    - name: http
      http:
        # url: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

內容和 Workflow 級別的 Demo 差不多,只是 hooks 字段的位置不同

spec:
  entrypoint: main
  templates:
    - name: main
      steps:
        - - name: step-1
            hooks:
              # ...
            template: echo
        - - name: step2
            hooks:
                          # ...
            template: echo

在 spec.templates 中我們分別為不同的步驟配置了 hooks,相比與 exiHandler 則更加靈活。

如何替代 exitHandler

LifecycleHook 可以完美替代 Exit Handler,就是把 Hook 命名為 exit,雖然 hook 的命名無無關緊要,但是如果是 exit 則是會特殊處理。

官方原文如下:

You must not name a LifecycleHook exit or it becomes an exit handler; otherwise the hook name has no relevance.

這個 exit 直接是寫死在代碼裏的,具體如下:

const (
    ExitLifecycleEvent = "exit"
)

func (lchs LifecycleHooks) GetExitHook() *LifecycleHook {
    hook, ok := lchs[ExitLifecycleEvent]
    if ok {
       return &hook
    }
    return nil
}

func (lchs LifecycleHooks) HasExitHook() bool {
    return lchs.GetExitHook() != nil
}

那麼我們只需要將 LifecycleHook 命名為 exit 即可替代 exit handler,就像這樣:

apiVersion: argoproj.io/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: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

4. 常見通知模板

通知一般支持 webhook、email、slack、微信通知等方式。

在 ArgoWorkflow 中則是準備對應的模板即可。

Webhook

這應該是最通用的一種方式,收到消息後具體做什麼事情,可以靈活的在 webhook 服務調整。

對於 ArgoWorkflow 模板就是執行 curl 命令即可,因此只需要一個包含 curl 工具的容器

apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
  name: step-notify-webhook
spec:
  templates:
    - name: webhook
      inputs:
        parameters:
          - name: POSITIONS # 指定什麼時候運行,多個以逗號隔開,例如:Pending,Running,Succeeded,Failed,Error
            value: "Succeeded,Failed,Error"
          - name: WEBHOOK_ENDPOINT
          - name: CURL_VERSION
            default: "8.4.0"

      container:
        image: curlimages/curl:{{inputs.parameters.CURL_VERSION}}
        command: [sh, -cx]
        args: [
          "curl -X POST  -H \"Content-type: application/json\" -d '{
          \"message\": \"{{workflow.name}} {{workflow.status}}\",
          \"workflow\": {
                \"name\": \"{{workflow.name}}\",
                \"namespace\": \"{{workflow.namespace}}\",
                \"uid\": \"{{workflow.uid}}\",
                \"creationTimestamp\": \"{{workflow.creationTimestamp}}\",
                \"status\": \"{{workflow.status}}\"
              }
        }'
        {{inputs.parameters.WEBHOOK_ENDPOINT}}"
        ]

Email

對於郵件方式,這裏簡單提供一個使用 Python 發送郵件的 Demo。

# use golangcd-lint for lint
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
  name: step-notify-email
spec:
  templates:
    - name: email
      inputs:
        parameters:
          - name: POSITIONS # 指定什麼時候運行,多個以逗號隔開,例如:Pending,Running,Succeeded,Failed,Error
            value: "Succeeded,Failed,Error"
          - name: CREDENTIALS_SECRET
          - name: TO # 收件人郵箱
          - name: PYTHON_VERSION
            default: "3.8-alpine"
      script:
        image: docker.io/library/python:{{inputs.parameters.PYTHON_VERSION}}
        command: [ python ]
        env:
          - name: TO
            value: '{{inputs.parameters.TO}}'
          - name: HOST
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: host
          - name: PORT
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: port
          - name: FROM
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: from
          - name: USERNAME
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: username
          - name: PASSWORD
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: password
          - name: TLS
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: tls
        source: |
          import smtplib
          import ssl
          import os
          from email.header import Header
          from email.mime.text import MIMEText

          smtp_server = os.getenv('HOST')
          port = os.getenv('PORT')
          sender_email = os.getenv('FROM')
          receiver_emails = os.getenv('TO')
          user = os.getenv('USERNAME')
          password = os.getenv('PASSWORD')
          tls = os.getenv('TLS')

          # 郵件正文,文本格式
          # 構建郵件消息
          workflow_info = f"""\
            "workflow": {{
              "name": "{{workflow.name}}",
              "namespace": "{{workflow.namespace}}",
              "uid": "{{workflow.uid}}",
              "creationTimestamp": "{{workflow.creationTimestamp}}",
              "status": "{{workflow.status}}"
            }}
          """
          msg = MIMEText(workflow_info, 'plain', 'utf-8')
          # 郵件頭信息
          msg['From'] = Header(sender_email)  # 發送者
          msg['To'] = Header(receiver_emails)  # 接收者
          subject = '{{workflow.name}} {{workflow.status}}'
          msg['Subject'] = Header(subject, 'utf-8')  # 郵件主題
          if tls == 'True':
            context = ssl.create_default_context()
            server = smtplib.SMTP_SSL(smtp_server, port, context=context)
          else:
            server = smtplib.SMTP(smtp_server, port)

          if password != '':
            server.login(user, password)

          for receiver in [item for item in receiver_emails.split(' ') if item]:
            server.sendmail(sender_email, receiver, msg.as_string())

            server.quit()

【ArgoWorkflow 系列】持續更新中,搜索公眾號【探索雲原生】訂閲,閲讀更多文章。


5. 小結

本文主要分析了 Argo 中的通知觸發機制,包括舊版的 exitHandler 以及新版的 LifecycleHook,並提供了幾個簡單的通知模板。

最後則是推薦使用更加靈活的 LifecycleHook。

user avatar manongsir 頭像 crossoverjie 頭像 yuzhoustayhungry 頭像 duiniwukenaihe_60e4196de52b7 頭像 meiyoufujideyidongdianyuan 頭像 openfuyao 頭像 ansurfen 頭像 zyuxuaner 頭像 xiaolanbenlan 頭像 lixingning 頭像 changhao_flag 頭像 tekin_cn 頭像
點贊 12 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.