Introduction aux mécanismes de notification dans ArgoWorkflow
Cet article explore les fonctionnalités ExitHandler et LifecycleHook dans ArgoWorkflow, permettant d'exécuter des actions basées sur l'état du pipeline, typiquement pour envoyer des notifications. Par exemple, lors de l'échec d'une étape ou d'un workflow, une alerte par email peut être déclenchée.
Dans les différentes versions d'ArgoWorkflow, deux approches existent :
- À partir de la version v2.7, la fonctionnalité exit handler permet de spécifier un template à exécuter après la fin du workflow. Ce template peut utiliser le champ
whenpour des conditions basées sur le résultat du workflow. Cette approche est obsolète et n'est plus recommandée après la version v3.3. - Depuis la version v3.3, LifecycleHook est introduit, offrant une granularité plus fine et davantage de fonctionnalités. ExitHandler peut être considéré comme un LifecycleHook simple.
ExitHandler : une approche historique
Bien que déconseillé, ExitHandler mérite une explication. Dans ArgoWorkflow, le champ spec.onExit désigne un template exécuté à la fin du worklfow, quelle que soit son issue. Ce template peut contenir des conditions via when pour dfifrencier les scénarios de succès et d'échec.
Voici un exemple modifié de workflow avec un ExitHandler :
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: workflow-demo-exit-
spec:
entrypoint: simulate-failure
onExit: post-processing
templates:
- name: simulate-failure
container:
image: busybox:latest
command: [sh, -c]
args: ["echo Simulation d'une défaillance; exit 1"]
- name: post-processing
steps:
- - name: envoyer-notification
template: notification-email
- name: celebrer-succes
template: message-succes
when: "{{workflow.status}} == Succeeded"
- name: echouer-notification
template: message-echec
when: "{{workflow.status}} != Succeeded"
- name: notification-email
container:
image: busybox:latest
command: [sh, -c]
args: ["echo Notification envoyée : {{workflow.name}} {{workflow.status}} {{workflow.duration}}. Échecs : {{workflow.failures}}"]
- name: message-succes
container:
image: busybox:latest
command: [sh, -c]
args: ["echo Réussite confirmée"]
- name: message-echec
container:
image: busybox:latest
command: [sh, -c]
args: ["echo Échec détecté"]
Dans cet exemple, le template post-processing est défini via onExit. Il envoie toujours une notification, puis déclenche une action conditionnelle. Comme le workflow principal échoue (exit code 1), le message d'échec est affiché. Les pods résultants incluent ceux pour la notification et le message d'échec, avec des journaux appropriés.
Cette méthode satisfait les besoins de base pour les notifications, comme l'envoi d'emails ou l'intégration via webhooks. Cependant, ExitHandler est limité au niveau workflow, exécuté uniquement à la fin globale. LifecycleHook dans la version v3.3 répond à ce manque de granularité.
LifecycleHook : une solution flexible
LifecycleHook est une évolution d'ExitHandler, offrant plus de flexibilité. Il peut être configuré à deux niveaux : workflow et template.
Niveau workflow
Similaire à ExitHandler, mais avec plus d'options. Voici un exemple modifié :
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: workflow-lifecycle-demo-
spec:
entrypoint: processus-principal
hooks:
sortie: # Gestionnaire de sortie
template: requete-http
en-cours:
expression: workflow.status == "Running"
template: requete-http
templates:
- name: processus-principal
steps:
- - name: etape-initiale
template: afficher-message
- name: afficher-message
container:
image: alpine:3.6
command: [sh, -c]
args: ["echo Message de test"]
- name: requete-http
http:
url: "https://example.com/api/status"
Ici, le champ hooks remplace onExit, permettant plusieurs hooks avec des expressions conditionnelles. Le template requete-http est de type HTTP, simulant une notification webhook. Comme le workflow réussit, les deux hooks s'exécutent.
Niveau template
Cette granularité permet de surveiller des étapes spécifiques. Exemple modifié :
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: workflow-hooks-template-
spec:
entrypoint: sequence-etapes
templates:
- name: sequence-etapes
steps:
- - name: phase-preparation
hooks:
en-cours:
expression: steps["phase-preparation"].status == "Running"
template: alerte-status
succes:
expression: steps["phase-preparation"].status == "Succeeded"
template: alerte-status
template: executer-commande
- - name: phase-execution
hooks:
en-cours:
expression: steps.phase-execution.status == "Running"
template: alerte-status
succes:
expression: steps.phase-execution.status == "Succeeded"
template: alerte-status
template: executer-commande
- name: executer-commande
container:
image: alpine:3.6
command: [sh, -c]
args: ["echo Exécution en cours"]
- name: alerte-status
http:
url: "https://example.com/api/alerte"
Les hooks sont placés au niveau des étapes, permettant des notifications ciblées. Pour remplacer ExitHandler avec LifecycleHook, nommez simplement le hook sortie (ou exit en code), comme indiqué dans la documentation d'ArgoWorkflow.
Modèles de notification courants
Les notifications peuvent prendre diverses formes, comme les webhooks ou les emails. Voici des exemples adaptés.
Webhook
Une méthode universelle utilisant une requête HTTP. Exemple de template modifié :
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: template-notif-webhook
spec:
templates:
- name: webhook-notification
inputs:
parameters:
- name: CONDITIONS_EXECUTION # Exemples : Pending,Running,Succeeded,Failed,Error
value: "Succeeded,Failed,Error"
- name: POINT_FINAL_WEBHOOK
- name: VERSION_CURL
default: "8.4.0"
container:
image: curlimages/curl:{{inputs.parameters.VERSION_CURL}}
command: [sh, -cx]
args: [
"curl -X POST -H \"Content-Type: application/json\" -d '{
\"message\": \"{{workflow.name}} {{workflow.status}}\",
\"workflow_details\": {
\"nom\": \"{{workflow.name}}\",
\"espace\": \"{{workflow.namespace}}\",
\"identifiant\": \"{{workflow.uid}}\",
\"horodatage\": \"{{workflow.creationTimestamp}}\",
\"statut\": \"{{workflow.status}}\"
}
}' {{inputs.parameters.POINT_FINAL_WEBHOOK}}"
]
Pour les notifications par email, utilisez un script Python modifié :
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: template-notif-email
spec:
templates:
- name: envoyer-email
inputs:
parameters:
- name: CONDITIONS_EXECUTION
value: "Succeeded,Failed,Error"
- name: SECRET_CREDENTIALS
- name: DESTINATAIRE # Adresse email
- name: VERSION_PYTHON
default: "3.8-alpine"
script:
image: python:{{inputs.parameters.VERSION_PYTHON}}
command: [python]
env:
- name: DESTINATAIRE
value: '{{inputs.parameters.DESTINATAIRE}}'
- name: SERVEUR_SMTP
valueFrom:
secretKeyRef:
name: '{{inputs.parameters.SECRET_CREDENTIALS}}'
key: serveur
- name: PORT_SMTP
valueFrom:
secretKeyRef:
name: '{{inputs.parameters.SECRET_CREDENTIALS}}'
key: port
- name: EXPEDITEUR
valueFrom:
secretKeyRef:
name: '{{inputs.parameters.SECRET_CREDENTIALS}}'
key: expediteur
- name: UTILISATEUR
valueFrom:
secretKeyRef:
name: '{{inputs.parameters.SECRET_CREDENTIALS}}'
key: utilisateur
- name: MOT_DE_PASSE
valueFrom:
secretKeyRef:
name: '{{inputs.parameters.SECRET_CREDENTIALS}}'
key: motdepasse
- name: TLS_ACTIF
valueFrom:
secretKeyRef:
name: '{{inputs.parameters.SECRET_CREDENTIALS}}'
key: tls
source: |
import smtplib
import ssl
import os
from email.mime.text import MIMEText
serveur_smtp = os.getenv('SERVEUR_SMTP')
port_smtp = os.getenv('PORT_SMTP')
email_expediteur = os.getenv('EXPEDITEUR')
emails_destinataires = os.getenv('DESTINATAIRE')
utilisateur = os.getenv('UTILISATEUR')
mot_de_passe = os.getenv('MOT_DE_PASSE')
tls_actif = os.getenv('TLS_ACTIF')
corps_message = f"Informations workflow :\nNom: {{workflow.name}}\nStatut: {{workflow.status}}"
message = MIMEText(corps_message, 'plain', 'utf-8')
message['From'] = email_expediteur
message['To'] = emails_destinataires
message['Subject'] = f"Notification : {{workflow.name}} {{workflow.status}}"
if tls_actif == 'True':
contexte = ssl.create_default_context()
serveur = smtplib.SMTP_SSL(serveur_smtp, port_smtp, context=contexte)
else:
serveur = smtplib.SMTP(serveur_smtp, port_smtp)
if mot_de_passe != '':
serveur.login(utilisateur, mot_de_passe)
for destinataire in emails_destinataires.split(';'):
if destinataire.strip():
serveur.sendmail(email_expediteur, destinataire.strip(), message.as_string())
serveur.quit()