2026-02-25 09:02:24 +01:00
// Package workflow_builder traduit les informations du graphe d'un Workflow
// (ses composants, ses liens) en un fichier YAML Argo Workflow prêt à être
// soumis à un cluster Kubernetes. Le point d'entrée principal est ArgoBuilder.
2024-08-02 13:34:39 +02:00
package workflow_builder
import (
2026-02-25 09:02:24 +01:00
"encoding/json"
2024-09-25 15:46:39 +02:00
"fmt"
2025-02-14 12:00:29 +01:00
"oc-monitord/conf"
2024-08-19 11:43:40 +02:00
. "oc-monitord/models"
2026-03-25 11:13:12 +01:00
"sync"
2025-06-16 18:22:58 +02:00
2025-04-29 12:23:44 +02:00
"os"
2024-08-02 13:34:39 +02:00
"strings"
2025-04-29 12:23:44 +02:00
"time"
2024-08-02 13:34:39 +02:00
2024-08-19 11:43:40 +02:00
oclib "cloud.o-forge.io/core/oc-lib"
2025-04-08 17:22:43 +02:00
"cloud.o-forge.io/core/oc-lib/logs"
2025-02-05 08:36:26 +01:00
"cloud.o-forge.io/core/oc-lib/models/common/enum"
2026-02-02 14:30:01 +01:00
"cloud.o-forge.io/core/oc-lib/models/peer"
2025-01-13 14:05:21 +01:00
"cloud.o-forge.io/core/oc-lib/models/resources"
2026-01-14 15:17:37 +01:00
"cloud.o-forge.io/core/oc-lib/models/resources/native_tools"
2024-08-29 09:34:10 +02:00
w "cloud.o-forge.io/core/oc-lib/models/workflow"
2025-06-16 18:22:58 +02:00
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
2026-01-14 15:17:37 +01:00
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
2025-04-29 12:23:44 +02:00
"github.com/nwtgck/go-fakelish"
2024-09-03 14:24:03 +02:00
"github.com/rs/zerolog"
2025-04-29 12:23:44 +02:00
"gopkg.in/yaml.v3"
2024-08-02 13:34:39 +02:00
)
2026-02-25 09:02:24 +01:00
// logger est le logger zerolog partagé au sein du package, initialisé à
// chaque appel de CreateDAG pour récupérer la configuration courante.
2024-09-03 14:24:03 +02:00
var logger zerolog . Logger
2026-02-25 09:02:24 +01:00
// ArgoBuilder est le constructeur principal du fichier Argo Workflow.
// Il porte l'état de la construction (workflow source, templates générés,
// services k8s à créer, timeout global, liste des peers distants impliqués).
2024-08-02 13:34:39 +02:00
type ArgoBuilder struct {
2026-02-25 09:02:24 +01:00
// OriginWorkflow est le workflow métier Open Cloud dont on construit la représentation Argo.
2025-04-28 14:01:57 +02:00
OriginWorkflow * w . Workflow
2026-02-25 09:02:24 +01:00
// Workflow est la structure YAML Argo en cours de construction.
Workflow Workflow
// Services liste les services Kubernetes à exposer pour les processings "IsService".
Services [ ] * Service
// Timeout est la durée maximale d'exécution en secondes (activeDeadlineSeconds).
Timeout int
// RemotePeers contient les IDs des peers distants détectés via Admiralty.
RemotePeers [ ] string
2026-03-25 11:13:12 +01:00
// HasLocalCompute indique qu'au moins un processing s'exécute sur le kube local.
// Le kube local doit recevoir son propre ArgoKubeEvent COMPUTE_RESOURCE.
HasLocalCompute bool
// PeerImages associe chaque peer aux images de conteneurs qu'il doit exécuter.
// Clé "" désigne le peer local. Utilisé pour le pre-pull et le release post-exec.
PeerImages map [ string ] [ ] string
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
// Workflow est la structure racine du fichier YAML Argo Workflow.
// Elle correspond exactement au format attendu par le contrôleur Argo.
2024-08-02 13:34:39 +02:00
type Workflow struct {
ApiVersion string ` yaml:"apiVersion" `
Kind string ` yaml:"kind" `
Metadata struct {
2024-08-20 15:23:02 +02:00
Name string ` yaml:"name" `
2024-08-02 13:34:39 +02:00
} ` yaml:"metadata" `
Spec Spec ` yaml:"spec,omitempty" `
}
2026-02-25 09:02:24 +01:00
// getDag retourne le pointeur sur le template "dag" du workflow.
// S'il n'existe pas encore, il est créé et ajouté à la liste des templates.
2024-10-11 13:44:16 +02:00
func ( b * Workflow ) getDag ( ) * Dag {
for _ , t := range b . Spec . Templates {
if t . Name == "dag" {
return t . Dag
}
}
b . Spec . Templates = append ( b . Spec . Templates , Template { Name : "dag" , Dag : & Dag { } } )
return b . Spec . Templates [ len ( b . Spec . Templates ) - 1 ] . Dag
}
2026-03-25 11:13:12 +01:00
// PodSecurityContext mirrors the subset of k8s PodSecurityContext used by Argo.
type PodSecurityContext struct {
RunAsUser * int64 ` yaml:"runAsUser,omitempty" `
RunAsGroup * int64 ` yaml:"runAsGroup,omitempty" `
FSGroup * int64 ` yaml:"fsGroup,omitempty" `
}
2026-02-25 09:02:24 +01:00
// Spec contient la spécification complète du workflow Argo :
// compte de service, point d'entrée, volumes, templates et timeout.
2024-08-02 13:34:39 +02:00
type Spec struct {
2026-02-25 09:02:24 +01:00
ArtifactRepositoryRef
2026-03-25 11:13:12 +01:00
ServiceAccountName string ` yaml:"serviceAccountName,omitempty" `
Entrypoint string ` yaml:"entrypoint" `
Arguments [ ] Parameter ` yaml:"arguments,omitempty" `
2026-01-14 15:17:37 +01:00
Volumes [ ] VolumeClaimTemplate ` yaml:"volumeClaimTemplates,omitempty" `
2026-03-25 11:13:12 +01:00
ExistingVolumes [ ] ExistingVolume ` yaml:"volumes,omitempty" `
Templates [ ] Template ` yaml:"templates" `
Timeout int ` yaml:"activeDeadlineSeconds,omitempty" `
SecurityContext * PodSecurityContext ` yaml:"securityContext,omitempty" `
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
// CreateDAG est le point d'entrée de la construction du DAG Argo.
// Il crée tous les templates (un par processing / native tool / sous-workflow),
// configure les volumes persistants, positionne les métadonnées globales du
// workflow et retourne :
// - le nombre de tâches dans le DAG,
// - les noms des premières tâches (sans dépendances),
// - les noms des dernières tâches (dont personne ne dépend),
// - une éventuelle erreur.
//
// Le paramètre write est conservé pour usage futur (écriture effective du YAML).
// TODO: gérer S3, GCS, Azure selon le type de stockage lié au processing.
2026-01-14 15:17:37 +01:00
func ( b * ArgoBuilder ) CreateDAG ( exec * workflow_execution . WorkflowExecution , namespace string , write bool ) ( int , [ ] string , [ ] string , error ) {
2025-04-08 17:22:43 +02:00
logger = logs . GetLogger ( )
2025-04-30 17:51:24 +02:00
logger . Info ( ) . Msg ( fmt . Sprint ( "Creating DAG " , b . OriginWorkflow . Graph . Items ) )
2026-02-25 09:02:24 +01:00
// Crée un template Argo pour chaque nœud du graphe et collecte les volumes.
2026-03-25 11:13:12 +01:00
firstItems , lastItems , volumes , err := b . createTemplates ( exec , namespace )
if err != nil {
return 0 , firstItems , lastItems , err
}
2026-01-14 15:17:37 +01:00
b . createVolumes ( exec , volumes )
2024-08-29 10:17:31 +02:00
2024-08-22 10:52:49 +02:00
if b . Timeout > 0 {
b . Workflow . Spec . Timeout = b . Timeout
}
2025-07-28 15:52:27 +02:00
b . Workflow . Spec . ServiceAccountName = "sa-" + namespace
2024-08-02 13:34:39 +02:00
b . Workflow . Spec . Entrypoint = "dag"
b . Workflow . ApiVersion = "argoproj.io/v1alpha1"
b . Workflow . Kind = "Workflow"
2024-10-11 13:44:16 +02:00
if ! write {
2025-04-02 11:40:14 +02:00
return len ( b . Workflow . getDag ( ) . Tasks ) , firstItems , lastItems , nil
2024-10-11 13:44:16 +02:00
}
2026-01-14 15:17:37 +01:00
return len ( b . Workflow . getDag ( ) . Tasks ) , firstItems , lastItems , nil
2024-08-02 13:34:39 +02:00
}
2026-01-14 15:17:37 +01:00
2026-02-25 09:02:24 +01:00
// createTemplates parcourt tous les nœuds du graphe (processings, native tools,
// sous-workflows) et génère les templates Argo correspondants.
// Elle gère également le recâblage des dépendances DAG entre sous-workflows
// imbriqués, et l'ajout du pod de service si nécessaire.
// Retourne les premières tâches, les dernières tâches et les volumes à créer.
2026-03-25 11:13:12 +01:00
func ( b * ArgoBuilder ) createTemplates ( exec * workflow_execution . WorkflowExecution , namespace string ) ( [ ] string , [ ] string , [ ] VolumeMount , error ) {
2024-10-11 13:44:16 +02:00
volumes := [ ] VolumeMount { }
firstItems := [ ] string { }
lastItems := [ ] string { }
2026-02-25 09:02:24 +01:00
// --- Processings ---
2025-02-05 08:36:26 +01:00
for _ , item := range b . OriginWorkflow . GetGraphItems ( b . OriginWorkflow . Graph . IsProcessing ) {
2026-01-14 15:17:37 +01:00
index := 0
_ , res := item . GetResource ( )
if d , ok := exec . SelectedInstances [ res . GetID ( ) ] ; ok {
index = d
}
instance := item . Processing . GetSelectedInstance ( & index )
2025-04-30 17:51:24 +02:00
logger . Info ( ) . Msg ( fmt . Sprint ( "Creating template for" , item . Processing . GetName ( ) , instance ) )
2025-02-05 08:36:26 +01:00
if instance == nil || instance . ( * resources . ProcessingInstance ) . Access == nil && instance . ( * resources . ProcessingInstance ) . Access . Container != nil {
logger . Error ( ) . Msg ( "Not enough configuration setup, template can't be created : " + item . Processing . GetName ( ) )
2026-03-25 11:13:12 +01:00
return firstItems , lastItems , volumes , nil
}
// Un même processing peut être bookié sur plusieurs peers : on crée
// un template Argo distinct par peer, déployés en parallèle.
for _ , pb := range getAllPeersForItem ( exec , item . ID ) {
var err error
volumes , firstItems , lastItems , err = b . createArgoTemplates ( exec ,
namespace , item . ID , pb . PeerID , pb . BookingID , item . Processing , volumes , firstItems , lastItems )
if err != nil {
return firstItems , lastItems , volumes , err
}
2024-08-02 13:34:39 +02:00
}
2024-10-11 13:44:16 +02:00
}
2026-02-25 09:02:24 +01:00
// --- Native Tools de type WORKFLOW_EVENT uniquement ---
2026-01-14 15:17:37 +01:00
for _ , item := range b . OriginWorkflow . GetGraphItems ( b . OriginWorkflow . Graph . IsNativeTool ) {
if item . NativeTool . Kind != int ( native_tools . WORKFLOW_EVENT ) {
continue
}
index := 0
_ , res := item . GetResource ( )
if d , ok := exec . SelectedInstances [ res . GetID ( ) ] ; ok {
index = d
}
instance := item . NativeTool . GetSelectedInstance ( & index )
logger . Info ( ) . Msg ( fmt . Sprint ( "Creating template for" , item . NativeTool . GetName ( ) , instance ) )
2026-03-25 11:13:12 +01:00
var err error
volumes , firstItems , lastItems , err = b . createArgoTemplates ( exec ,
namespace , item . ID , "" , item . ID , item . NativeTool , volumes , firstItems , lastItems )
if err != nil {
return firstItems , lastItems , volumes , err
}
2026-01-14 15:17:37 +01:00
}
2026-02-25 09:02:24 +01:00
// --- Sous-workflows : chargement, construction récursive et fusion du DAG ---
2024-10-11 13:44:16 +02:00
firstWfTasks := map [ string ] [ ] string { }
latestWfTasks := map [ string ] [ ] string { }
relatedWfTasks := map [ string ] [ ] string { }
2025-01-13 14:05:21 +01:00
for _ , wf := range b . OriginWorkflow . Workflows {
realWorkflow , code , err := w . NewAccessor ( nil ) . LoadOne ( wf )
2024-10-11 13:44:16 +02:00
if code != 200 {
logger . Error ( ) . Msg ( "Error loading the workflow : " + err . Error ( ) )
continue
2024-09-03 14:24:03 +02:00
}
2024-10-11 13:44:16 +02:00
subBuilder := ArgoBuilder { OriginWorkflow : realWorkflow . ( * w . Workflow ) , Timeout : b . Timeout }
2026-01-14 15:17:37 +01:00
_ , fi , li , err := subBuilder . CreateDAG ( exec , namespace , false )
2024-10-11 13:44:16 +02:00
if err != nil {
logger . Error ( ) . Msg ( "Error creating the subworkflow : " + err . Error ( ) )
continue
}
2025-01-13 14:05:21 +01:00
firstWfTasks [ wf ] = fi
2026-03-25 11:13:12 +01:00
if ok , depsOfIds := subBuilder . isArgoDependancy ( exec , wf ) ; ok { // le sous-workflow est une dépendance d'autre chose
2025-01-13 14:05:21 +01:00
latestWfTasks [ wf ] = li
relatedWfTasks [ wf ] = depsOfIds
2024-10-11 13:44:16 +02:00
}
2026-02-25 09:02:24 +01:00
// Fusion des tâches, templates, volumes et arguments du sous-workflow dans le DAG principal.
2024-10-11 13:44:16 +02:00
subDag := subBuilder . Workflow . getDag ( )
d := b . Workflow . getDag ( )
2026-02-25 09:02:24 +01:00
d . Tasks = append ( d . Tasks , subDag . Tasks ... )
2024-10-11 13:44:16 +02:00
b . Workflow . Spec . Templates = append ( b . Workflow . Spec . Templates , subBuilder . Workflow . Spec . Templates ... )
b . Workflow . Spec . Volumes = append ( b . Workflow . Spec . Volumes , subBuilder . Workflow . Spec . Volumes ... )
b . Workflow . Spec . Arguments = append ( b . Workflow . Spec . Arguments , subBuilder . Workflow . Spec . Arguments ... )
b . Services = append ( b . Services , subBuilder . Services ... )
2024-09-03 14:24:03 +02:00
}
2026-02-25 09:02:24 +01:00
// Recâblage : les tâches qui dépendaient du sous-workflow dépendent désormais
// de sa dernière tâche réelle (latestWfTasks).
2024-10-11 13:44:16 +02:00
for wfID , depsOfIds := range relatedWfTasks {
for _ , dep := range depsOfIds {
for _ , task := range b . Workflow . getDag ( ) . Tasks {
if strings . Contains ( task . Name , dep ) {
index := - 1
for i , depp := range task . Dependencies {
if strings . Contains ( depp , wfID ) {
index = i
break
}
}
if index != - 1 {
task . Dependencies = append ( task . Dependencies [ : index ] , task . Dependencies [ index + 1 : ] ... )
}
task . Dependencies = append ( task . Dependencies , latestWfTasks [ wfID ] ... )
}
}
}
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
// Les premières tâches du sous-workflow héritent des dépendances
// que le sous-workflow avait vis-à-vis du DAG principal.
2024-10-11 13:44:16 +02:00
for wfID , fi := range firstWfTasks {
2026-03-25 11:13:12 +01:00
deps := b . getArgoDependencies ( exec , wfID )
2024-10-11 13:44:16 +02:00
if len ( deps ) > 0 {
for _ , dep := range fi {
for _ , task := range b . Workflow . getDag ( ) . Tasks {
if strings . Contains ( task . Name , dep ) {
task . Dependencies = append ( task . Dependencies , deps ... )
}
}
}
2024-08-02 13:34:39 +02:00
}
}
2026-02-25 09:02:24 +01:00
// Si des services Kubernetes sont nécessaires, on ajoute le pod dédié.
2024-09-03 14:24:03 +02:00
if b . Services != nil {
2024-10-11 13:44:16 +02:00
dag := b . Workflow . getDag ( )
dag . Tasks = append ( dag . Tasks , Task { Name : "workflow-service-pod" , Template : "workflow-service-pod" } )
b . addServiceToArgo ( )
2024-09-03 14:24:03 +02:00
}
2026-03-25 11:13:12 +01:00
return firstItems , lastItems , volumes , nil
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
// createArgoTemplates crée le template Argo pour un nœud du graphe (processing
2026-03-25 11:13:12 +01:00
// ou native tool) sur un peer donné. Il :
2026-02-25 09:02:24 +01:00
// 1. Ajoute la tâche au DAG avec ses dépendances.
// 2. Crée le template de container (ou d'événement pour les native tools).
2026-03-25 11:13:12 +01:00
// 3. Ajoute les annotations Admiralty si peerID désigne un peer distant.
2026-02-25 09:02:24 +01:00
// 4. Crée un service Kubernetes si le processing est déclaré IsService.
// 5. Configure les annotations de stockage (S3, volumes locaux).
2026-01-14 15:17:37 +01:00
func ( b * ArgoBuilder ) createArgoTemplates (
exec * workflow_execution . WorkflowExecution ,
namespace string ,
2026-03-25 11:13:12 +01:00
graphID string ,
peerID string ,
bookingID string ,
2026-02-02 14:30:01 +01:00
obj resources . ResourceInterface ,
2024-10-11 13:44:16 +02:00
volumes [ ] VolumeMount ,
firstItems [ ] string ,
2026-03-25 11:13:12 +01:00
lastItems [ ] string ,
) ( [ ] VolumeMount , [ ] string , [ ] string , error ) {
2026-02-25 09:02:24 +01:00
2026-03-25 11:13:12 +01:00
_ , firstItems , lastItems = b . addTaskToArgo ( exec , b . Workflow . getDag ( ) , graphID , bookingID , obj , firstItems , lastItems )
template := & Template { Name : getArgoName ( obj . GetName ( ) , bookingID ) }
2025-04-30 17:51:24 +02:00
logger . Info ( ) . Msg ( fmt . Sprint ( "Creating template for" , template . Name ) )
2026-03-25 11:13:12 +01:00
2026-02-02 14:30:01 +01:00
if obj . GetType ( ) == tools . PROCESSING_RESOURCE . String ( ) {
template . CreateContainer ( exec , obj . ( * resources . ProcessingResource ) , b . Workflow . getDag ( ) )
} else if obj . GetType ( ) == tools . NATIVE_TOOL . String ( ) {
template . CreateEventContainer ( exec , obj . ( * resources . NativeTool ) , b . Workflow . getDag ( ) )
2026-01-14 15:17:37 +01:00
}
2026-03-25 11:13:12 +01:00
// Enregistre l'image pour le pre-pull sur le peer cible.
// peerID == "" désigne le peer local (clé "" dans PeerImages).
b . addPeerImage ( peerID , template . Container . Image )
2026-01-14 15:17:37 +01:00
2026-03-25 11:13:12 +01:00
// Vérifie si le peer est distant (Admiralty).
isReparted , remotePeer := b . isPeerReparted ( peerID )
2025-03-07 16:19:26 +01:00
if isReparted {
2026-03-25 11:13:12 +01:00
logger . Debug ( ) . Msg ( "Reparted processing, on " + remotePeer . GetID ( ) )
b . RemotePeers = append ( b . RemotePeers , remotePeer . GetID ( ) )
template . AddAdmiraltyAnnotations ( remotePeer . GetID ( ) )
} else {
// Processing local : le kube local doit aussi être configuré.
b . HasLocalCompute = true
2025-03-07 16:19:26 +01:00
}
2026-02-25 09:02:24 +01:00
// Si le processing expose un service Kubernetes, on l'enregistre et on
// applique le label "app" pour que le Service puisse le sélectionner.
2026-02-02 14:30:01 +01:00
if obj . GetType ( ) == tools . PROCESSING_RESOURCE . String ( ) && obj . ( * resources . ProcessingResource ) . IsService {
2026-03-25 11:13:12 +01:00
b . CreateService ( exec , graphID , obj )
2024-10-11 13:44:16 +02:00
template . Metadata . Labels = make ( map [ string ] string )
2026-02-25 09:02:24 +01:00
template . Metadata . Labels [ "app" ] = "oc-service-" + obj . GetName ( )
2024-10-11 13:44:16 +02:00
}
2025-06-16 18:22:58 +02:00
2026-03-25 11:13:12 +01:00
var err error
volumes , err = b . addStorageAnnotations ( exec , graphID , template , namespace , volumes , isReparted )
if err != nil {
return volumes , firstItems , lastItems , err
}
2025-06-20 11:28:12 +02:00
b . Workflow . Spec . Templates = append ( b . Workflow . Spec . Templates , * template )
2026-03-25 11:13:12 +01:00
return volumes , firstItems , lastItems , nil
2025-06-20 11:28:12 +02:00
}
2026-02-25 09:02:24 +01:00
// addStorageAnnotations parcourt tous les nœuds de stockage liés au processing
// identifié par id. Pour chaque lien de stockage :
// - Construit le nom de l'artefact Argo (lecture ou écriture).
// - Pour les stockages S3 : appelle waitForConsiders (STORAGE_RESOURCE) pour
// attendre la validation PB_CONSIDERS avant de configurer les annotations S3.
// - Pour les volumes locaux : ajoute un VolumeMount dans le container.
2026-03-25 11:13:12 +01:00
// Si isReparted est true (step Admiralty), le volume local est marqué comme
// réparti afin que createVolumes ne génère pas de PVC local-path incompatible
// avec les virtual kubelets.
func ( b * ArgoBuilder ) addStorageAnnotations ( exec * workflow_execution . WorkflowExecution , id string , template * Template , namespace string , volumes [ ] VolumeMount , isReparted bool ) ( [ ] VolumeMount , error ) {
2026-02-25 09:02:24 +01:00
// Récupère tous les nœuds de stockage connectés au processing courant.
related := b . OriginWorkflow . GetByRelatedProcessing ( id , b . OriginWorkflow . Graph . IsStorage )
2025-08-11 12:01:34 +02:00
2025-02-05 08:36:26 +01:00
for _ , r := range related {
storage := r . Node . ( * resources . StorageResource )
for _ , linkToStorage := range r . Links {
for _ , rw := range linkToStorage . StorageLinkInfos {
2025-06-16 18:22:58 +02:00
var art Artifact
2026-02-25 09:02:24 +01:00
// Le nom de l'artefact doit être alphanumérique + '-' ou '_'.
artifactBaseName := strings . Join ( strings . Split ( storage . GetName ( ) , " " ) , "-" ) + "-" + strings . Replace ( rw . FileName , "." , "-" , - 1 )
2025-02-05 08:36:26 +01:00
if rw . Write {
2026-02-25 09:02:24 +01:00
// Écriture vers S3 : Path = chemin du fichier dans le pod.
art = Artifact { Path : template . ReplacePerEnv ( rw . Source , linkToStorage . Env ) }
2025-06-16 18:22:58 +02:00
art . Name = artifactBaseName + "-input-write"
2025-02-05 08:36:26 +01:00
} else {
2026-02-25 09:02:24 +01:00
// Lecture depuis S3 : Path = destination dans le pod.
art = Artifact { Path : template . ReplacePerEnv ( rw . Destination + "/" + rw . FileName , linkToStorage . Env ) }
2025-06-16 18:22:58 +02:00
art . Name = artifactBaseName + "-input-read"
2025-02-05 08:36:26 +01:00
}
2025-06-20 11:28:12 +02:00
2025-02-05 08:36:26 +01:00
if storage . StorageType == enum . S3 {
2026-02-25 09:02:24 +01:00
// Pour chaque ressource de compute liée à ce stockage S3,
// on notifie via NATS et on attend la validation PB_CONSIDERS
// avec DataType = STORAGE_RESOURCE avant de continuer.
2026-03-25 11:13:12 +01:00
// Les goroutines tournent en parallèle ; un timeout sur l'une
// d'elles est une erreur fatale qui stoppe la suite du build.
relatedProcessing := b . getStorageRelatedProcessing ( storage . GetID ( ) )
var wg sync . WaitGroup
errCh := make ( chan error , len ( relatedProcessing ) )
for _ , r := range relatedProcessing {
wg . Add ( 1 )
go waitForConsiders ( exec . ExecutionsID , tools . STORAGE_RESOURCE , ArgoKubeEvent {
2026-02-25 09:02:24 +01:00
ExecutionsID : exec . ExecutionsID ,
DestPeerID : r . GetID ( ) ,
Type : tools . STORAGE_RESOURCE ,
SourcePeerID : storage . GetCreatorID ( ) ,
OriginID : conf . GetConfig ( ) . PeerID ,
2026-03-25 11:13:12 +01:00
} , & wg , errCh )
}
wg . Wait ( )
close ( errCh )
for err := range errCh {
if err != nil {
return volumes , err
}
2026-02-25 09:02:24 +01:00
}
// Configure la référence au dépôt d'artefacts S3 dans le Spec.
b . addS3annotations ( storage , namespace )
2025-02-05 08:36:26 +01:00
}
2025-06-20 11:28:12 +02:00
2025-02-05 08:36:26 +01:00
if rw . Write {
2025-08-11 12:01:34 +02:00
template . Outputs . Artifacts = append ( template . Outputs . Artifacts , art )
2025-02-05 08:36:26 +01:00
} else {
2025-08-11 12:01:34 +02:00
template . Inputs . Artifacts = append ( template . Inputs . Artifacts , art )
2025-02-05 08:36:26 +01:00
}
}
}
2026-02-25 09:02:24 +01:00
2026-03-25 11:13:12 +01:00
// Si l'instance de stockage est locale, on pré-provisionne le PVC via
// oc-datacenter (même pattern que MinIO) puis on monte un volume existant.
2025-02-05 08:36:26 +01:00
index := 0
2026-01-14 15:17:37 +01:00
if s , ok := exec . SelectedInstances [ storage . GetID ( ) ] ; ok {
index = s
2025-01-13 14:05:21 +01:00
}
2025-02-05 08:36:26 +01:00
s := storage . Instances [ index ]
2025-01-13 14:05:21 +01:00
if s . Local {
2026-03-25 11:13:12 +01:00
var pvcWg sync . WaitGroup
pvcErrCh := make ( chan error , 1 )
pvcWg . Add ( 1 )
go waitForConsiders ( exec . ExecutionsID , tools . STORAGE_RESOURCE , ArgoKubeEvent {
ExecutionsID : exec . ExecutionsID ,
Type : tools . STORAGE_RESOURCE ,
SourcePeerID : conf . GetConfig ( ) . PeerID ,
DestPeerID : conf . GetConfig ( ) . PeerID ,
OriginID : conf . GetConfig ( ) . PeerID ,
MinioID : storage . GetID ( ) ,
Local : true ,
StorageName : storage . GetName ( ) ,
} , & pvcWg , pvcErrCh )
pvcWg . Wait ( )
close ( pvcErrCh )
for err := range pvcErrCh {
if err != nil {
return volumes , err
}
}
2024-10-11 13:44:16 +02:00
volumes = template . Container . AddVolumeMount ( VolumeMount {
2026-03-25 11:13:12 +01:00
Name : strings . ReplaceAll ( strings . ToLower ( storage . GetName ( ) ) , " " , "-" ) ,
MountPath : s . Source ,
Storage : storage ,
IsReparted : isReparted ,
2024-10-11 13:44:16 +02:00
} , volumes )
2024-08-02 13:34:39 +02:00
}
}
2026-03-25 11:13:12 +01:00
return volumes , nil
2026-01-14 15:17:37 +01:00
}
2026-02-25 09:02:24 +01:00
// getStorageRelatedProcessing retourne la liste des ressources de compute
// connectées (via un processing intermédiaire) au stockage identifié par storageId.
// Ces ressources sont utilisées pour construire les ArgoKubeEvent destinés
// à la validation NATS.
func ( b * ArgoBuilder ) getStorageRelatedProcessing ( storageId string ) ( res [ ] resources . ResourceInterface ) {
var storageLinks [ ] graph . GraphLink
// On ne conserve que les liens impliquant ce stockage.
for _ , link := range b . OriginWorkflow . Graph . Links {
if link . Destination . ID == storageId || link . Source . ID == storageId {
storageLinks = append ( storageLinks , link )
}
2026-01-14 15:17:37 +01:00
}
2026-02-25 09:02:24 +01:00
for _ , link := range storageLinks {
var resourceId string
// L'opposé du lien est soit la source soit la destination selon la direction.
if link . Source . ID != storageId {
resourceId = link . Source . ID
} else {
resourceId = link . Destination . ID
2026-01-14 15:17:37 +01:00
}
2026-02-25 09:02:24 +01:00
// Si l'opposé est un processing, on récupère ses ressources de compute.
if b . OriginWorkflow . Graph . IsProcessing ( b . OriginWorkflow . Graph . Items [ resourceId ] ) {
res = append ( res , b . getComputeProcessing ( resourceId ) ... )
2025-07-10 17:31:38 +02:00
}
}
2026-01-14 15:17:37 +01:00
2026-02-25 09:02:24 +01:00
return
2025-07-10 17:31:38 +02:00
}
2026-02-25 09:02:24 +01:00
// getComputeProcessing retourne toutes les ressources de compute attachées
// au processing identifié par processingId dans le graphe du workflow.
func ( b * ArgoBuilder ) getComputeProcessing ( processingId string ) ( res [ ] resources . ResourceInterface ) {
arr := [ ] resources . ResourceInterface { }
computeRel := b . OriginWorkflow . GetByRelatedProcessing ( processingId , b . OriginWorkflow . Graph . IsCompute )
for _ , rel := range computeRel {
arr = append ( arr , rel . Node )
2025-06-16 18:22:58 +02:00
}
2026-02-25 09:02:24 +01:00
return arr
2025-06-20 11:28:12 +02:00
}
2025-06-16 18:22:58 +02:00
2026-02-25 09:02:24 +01:00
// addS3annotations configure la référence au dépôt d'artefacts S3 dans le Spec
// du workflow Argo. La ConfigMap et la clé sont dérivées de l'ID du stockage.
// Le namespace est conservé en signature pour une évolution future.
func ( b * ArgoBuilder ) addS3annotations ( storage * resources . StorageResource , namespace string ) {
b . Workflow . Spec . ArtifactRepositoryRef = ArtifactRepositoryRef {
ConfigMap : storage . GetID ( ) + "-artifact-repository" ,
Key : storage . GetID ( ) + "-s3-local" ,
}
2025-06-16 18:22:58 +02:00
}
2026-02-25 09:02:24 +01:00
// addTaskToArgo ajoute une tâche au DAG Argo pour le nœud graphItemID.
// Elle résout les dépendances DAG, propage les paramètres d'environnement,
// d'entrée et de sortie de l'instance sélectionnée, et met à jour les listes
// firstItems / lastItems utilisées pour le recâblage des sous-workflows.
2026-03-25 11:13:12 +01:00
// bookingID est le nom unique de cette instance (peut varier par peer).
func ( b * ArgoBuilder ) addTaskToArgo ( exec * workflow_execution . WorkflowExecution , dag * Dag , graphItemID string , bookingID string , processing resources . ResourceInterface ,
2024-10-11 13:44:16 +02:00
firstItems [ ] string , lastItems [ ] string ) ( * Dag , [ ] string , [ ] string ) {
2026-02-25 09:02:24 +01:00
2026-03-25 11:13:12 +01:00
unique_name := getArgoName ( processing . GetName ( ) , bookingID )
2026-01-14 15:17:37 +01:00
step := Task { Name : unique_name , Template : unique_name }
2026-02-25 09:02:24 +01:00
2026-01-14 15:17:37 +01:00
index := 0
if d , ok := exec . SelectedInstances [ processing . GetID ( ) ] ; ok {
index = d
}
instance := processing . GetSelectedInstance ( & index )
2025-02-05 08:36:26 +01:00
if instance != nil {
2026-02-25 09:02:24 +01:00
// Propagation des variables d'environnement, entrées et sorties
// de l'instance vers les paramètres de la tâche Argo.
2025-02-05 08:36:26 +01:00
for _ , value := range instance . ( * resources . ProcessingInstance ) . Env {
2024-10-11 13:44:16 +02:00
step . Arguments . Parameters = append ( step . Arguments . Parameters , Parameter {
2025-02-05 08:36:26 +01:00
Name : value . Name ,
Value : value . Value ,
} )
}
for _ , value := range instance . ( * resources . ProcessingInstance ) . Inputs {
step . Arguments . Parameters = append ( step . Arguments . Parameters , Parameter {
Name : value . Name ,
Value : value . Value ,
} )
}
for _ , value := range instance . ( * resources . ProcessingInstance ) . Outputs {
step . Arguments . Parameters = append ( step . Arguments . Parameters , Parameter {
Name : value . Name ,
Value : value . Value ,
2024-10-11 13:44:16 +02:00
} )
}
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
2026-03-25 11:13:12 +01:00
step . Dependencies = b . getArgoDependencies ( exec , graphItemID )
2026-02-25 09:02:24 +01:00
// Détermine si ce nœud est une première ou une dernière tâche du DAG.
2024-10-11 13:44:16 +02:00
name := ""
if b . OriginWorkflow . Graph . Items [ graphItemID ] . Processing != nil {
name = b . OriginWorkflow . Graph . Items [ graphItemID ] . Processing . GetName ( )
2024-08-02 13:34:39 +02:00
}
2024-10-11 13:44:16 +02:00
if b . OriginWorkflow . Graph . Items [ graphItemID ] . Workflow != nil {
name = b . OriginWorkflow . Graph . Items [ graphItemID ] . Workflow . GetName ( )
2024-08-02 13:34:39 +02:00
}
2024-10-11 13:44:16 +02:00
if len ( step . Dependencies ) == 0 && name != "" {
2026-03-25 11:13:12 +01:00
firstItems = append ( firstItems , getArgoName ( name , bookingID ) )
2024-08-02 13:34:39 +02:00
}
2026-03-25 11:13:12 +01:00
if ok , _ := b . isArgoDependancy ( exec , graphItemID ) ; ! ok && name != "" {
lastItems = append ( lastItems , getArgoName ( name , bookingID ) )
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
2024-10-11 13:44:16 +02:00
dag . Tasks = append ( dag . Tasks , step )
return dag , firstItems , lastItems
}
2024-08-02 13:34:39 +02:00
2026-03-25 11:13:12 +01:00
// createVolumes référence les PVCs pré-provisionnés par oc-datacenter comme
// volumes existants (ExistingVolumes) dans le Spec Argo.
// Le nom du PVC est calculé de manière déterministe : <storageName>-<executionsID>,
// identique à ClaimName() dans oc-datacenter/infrastructure/storage/pvc_setter.go.
2026-02-25 09:02:24 +01:00
func ( b * ArgoBuilder ) createVolumes ( exec * workflow_execution . WorkflowExecution , volumes [ ] VolumeMount ) {
2026-03-25 11:13:12 +01:00
seen := make ( map [ string ] struct { } )
2024-10-11 13:44:16 +02:00
for _ , volume := range volumes {
2026-03-25 11:13:12 +01:00
name := strings . ReplaceAll ( strings . ToLower ( volume . Name ) , " " , "-" )
if _ , ok := seen [ name ] ; ok {
continue
}
seen [ name ] = struct { } { }
claimName := name + "-" + exec . ExecutionsID
ev := ExistingVolume { }
ev . Name = name
ev . PersistentVolumeClaim . ClaimName = claimName
b . Workflow . Spec . ExistingVolumes = append ( b . Workflow . Spec . ExistingVolumes , ev )
}
// hostPath PVs are created as root:root 0755. Ensure pods can read/write
// by running as root when local volumes are present.
if len ( b . Workflow . Spec . ExistingVolumes ) > 0 && b . Workflow . Spec . SecurityContext == nil {
zero := int64 ( 0 )
b . Workflow . Spec . SecurityContext = & PodSecurityContext {
RunAsUser : & zero ,
RunAsGroup : & zero ,
FSGroup : & zero ,
2025-01-13 14:05:21 +01:00
}
2024-08-02 13:34:39 +02:00
}
2024-10-11 13:44:16 +02:00
}
2024-08-02 13:34:39 +02:00
2026-02-25 09:02:24 +01:00
// isArgoDependancy vérifie si le nœud identifié par id est une dépendance
// d'au moins un autre nœud du DAG (i.e. s'il existe un lien sortant vers
// un processing ou un workflow).
// Retourne true + la liste des noms Argo des nœuds qui en dépendent.
2026-03-25 11:13:12 +01:00
func ( b * ArgoBuilder ) isArgoDependancy ( exec * workflow_execution . WorkflowExecution , id string ) ( bool , [ ] string ) {
2024-10-11 13:44:16 +02:00
dependancyOfIDs := [ ] string { }
isDeps := false
for _ , link := range b . OriginWorkflow . Graph . Links {
2025-02-06 08:55:28 +01:00
if _ , ok := b . OriginWorkflow . Graph . Items [ link . Destination . ID ] ; ! ok {
2025-04-30 17:51:24 +02:00
logger . Info ( ) . Msg ( fmt . Sprint ( "Could not find the source of the link" , link . Destination . ID ) )
2025-02-06 08:55:28 +01:00
continue
}
2024-10-11 13:44:16 +02:00
source := b . OriginWorkflow . Graph . Items [ link . Destination . ID ] . Processing
if id == link . Source . ID && source != nil {
isDeps = true
2026-03-25 11:13:12 +01:00
for _ , pb := range getAllPeersForItem ( exec , link . Destination . ID ) {
dependancyOfIDs = append ( dependancyOfIDs , getArgoName ( source . GetName ( ) , pb . BookingID ) )
}
2024-10-11 13:44:16 +02:00
}
wourceWF := b . OriginWorkflow . Graph . Items [ link . Destination . ID ] . Workflow
if id == link . Source . ID && wourceWF != nil {
isDeps = true
2026-03-25 11:13:12 +01:00
for _ , pb := range getAllPeersForItem ( exec , link . Destination . ID ) {
dependancyOfIDs = append ( dependancyOfIDs , getArgoName ( wourceWF . GetName ( ) , pb . BookingID ) )
}
2024-10-11 13:44:16 +02:00
}
}
return isDeps , dependancyOfIDs
2024-08-02 13:34:39 +02:00
}
2026-02-25 09:02:24 +01:00
// getArgoDependencies retourne la liste des noms de tâches Argo dont dépend
// le nœud identifié par id (liens entrants depuis des processings).
2026-03-25 11:13:12 +01:00
// Si le processing source est bookié sur N peers, toutes ses instances sont
// retournées comme dépendances (la tâche courante attend toutes les instances).
func ( b * ArgoBuilder ) getArgoDependencies ( exec * workflow_execution . WorkflowExecution , id string ) ( dependencies [ ] string ) {
2024-10-11 13:44:16 +02:00
for _ , link := range b . OriginWorkflow . Graph . Links {
2025-02-05 08:36:26 +01:00
if _ , ok := b . OriginWorkflow . Graph . Items [ link . Source . ID ] ; ! ok {
2025-04-30 17:51:24 +02:00
logger . Info ( ) . Msg ( fmt . Sprint ( "Could not find the source of the link" , link . Source . ID ) )
2025-02-05 08:36:26 +01:00
continue
}
2024-10-11 13:44:16 +02:00
source := b . OriginWorkflow . Graph . Items [ link . Source . ID ] . Processing
if id == link . Destination . ID && source != nil {
2026-03-25 11:13:12 +01:00
for _ , pb := range getAllPeersForItem ( exec , link . Source . ID ) {
dependencies = append ( dependencies , getArgoName ( source . GetName ( ) , pb . BookingID ) )
}
2024-10-11 13:44:16 +02:00
}
}
2024-08-02 13:34:39 +02:00
return
}
2026-02-25 09:02:24 +01:00
// getArgoName construit le nom unique d'une tâche / template Argo à partir
// du nom humain de la ressource et de son ID dans le graphe.
// Les espaces sont remplacés par des tirets et tout est mis en minuscules.
2024-08-02 13:34:39 +02:00
func getArgoName ( raw_name string , component_id string ) ( formatedName string ) {
formatedName = strings . ReplaceAll ( raw_name , " " , "-" )
formatedName += "-" + component_id
formatedName = strings . ToLower ( formatedName )
return
}
2025-02-28 14:15:59 +01:00
2026-03-25 11:13:12 +01:00
// peerBooking associe un peerID à son bookingID pour un item du graphe.
type peerBooking struct {
PeerID string
BookingID string
}
// getAllPeersForItem retourne tous les (peerID, bookingID) enregistrés dans
// PeerBookByGraph pour un item donné. Si aucun booking n'est trouvé (item
// non encore planifié ou sous-workflow), retourne une entrée locale de
// fallback avec BookingID = graphItemID.
func getAllPeersForItem ( exec * workflow_execution . WorkflowExecution , graphItemID string ) [ ] peerBooking {
var result [ ] peerBooking
for peerID , byGraph := range exec . PeerBookByGraph {
if bookings , ok := byGraph [ graphItemID ] ; ok && len ( bookings ) > 0 {
result = append ( result , peerBooking { PeerID : peerID , BookingID : bookings [ 0 ] } )
}
}
if len ( result ) == 0 {
result = [ ] peerBooking { { PeerID : "" , BookingID : graphItemID } }
}
return result
}
// isPeerReparted vérifie si le peerID désigne un peer distant (Relation != 1).
// Un peerID vide signifie exécution locale : retourne false sans appel réseau.
func ( b * ArgoBuilder ) isPeerReparted ( peerID string ) ( bool , * peer . Peer ) {
if peerID == "" {
return false , nil
2025-03-07 16:19:26 +01:00
}
2025-04-28 14:01:57 +02:00
req := oclib . NewRequest ( oclib . LibDataEnum ( oclib . PEER ) , "" , "" , nil , nil )
2025-02-28 14:15:59 +01:00
if req == nil {
fmt . Println ( "TODO : handle error when trying to create a request on the Peer Collection" )
2026-02-02 14:30:01 +01:00
return false , nil
2025-04-28 14:01:57 +02:00
}
2025-02-28 14:15:59 +01:00
2026-03-25 11:13:12 +01:00
res := req . LoadOne ( peerID )
2025-02-28 14:15:59 +01:00
if res . Err != "" {
2026-03-25 11:13:12 +01:00
fmt . Print ( "TODO : handle error when requesting PeerID: " + res . Err )
2026-02-02 14:30:01 +01:00
return false , nil
2025-02-28 14:15:59 +01:00
}
2025-04-28 14:01:57 +02:00
2026-03-25 11:13:12 +01:00
p := res . ToPeer ( )
2026-02-25 09:02:24 +01:00
// Relation == 1 signifie "moi-même" : le processing est local.
2026-03-25 11:13:12 +01:00
isNotReparted := p . Relation == 1
logger . Info ( ) . Msg ( fmt . Sprint ( "Result IsMySelf for " , p . UUID , " : " , isNotReparted ) )
return ! isNotReparted , p
2026-02-25 09:02:24 +01:00
}
2025-02-28 14:15:59 +01:00
2026-03-25 11:13:12 +01:00
// waitForConsiders publie un ArgoKubeEvent sur NATS puis attend la confirmation
// PB_CONSIDERS via le cache global (globalConsidersCache), sans ouvrir de
// connexion NATS supplémentaire. Le listener centralisé (StartConsidersListener)
// dispatche le message vers le bon canal.
2026-02-25 09:02:24 +01:00
// Un timeout de 5 minutes est appliqué pour éviter un blocage indéfini.
2026-03-25 11:13:12 +01:00
func waitForConsiders ( executionsId string , dataType tools . DataType , event ArgoKubeEvent , wg * sync . WaitGroup , errCh chan <- error ) {
defer wg . Done ( )
2026-02-25 09:02:24 +01:00
// Sérialise l'événement et le publie sur ARGO_KUBE_EVENT.
b , err := json . Marshal ( event )
if err != nil {
logger . Error ( ) . Msg ( "Cannot marshal ArgoKubeEvent: " + err . Error ( ) )
2026-03-25 11:13:12 +01:00
errCh <- err
2026-02-25 09:02:24 +01:00
return
}
tools . NewNATSCaller ( ) . SetNATSPub ( tools . ARGO_KUBE_EVENT , tools . NATSResponse {
FromApp : "oc-monitord" ,
Datatype : dataType ,
User : "root" ,
2026-03-25 11:13:12 +01:00
Method : int ( tools . ARGO_KUBE_EVENT ) ,
2026-02-25 09:02:24 +01:00
Payload : b ,
} )
2026-03-25 11:13:12 +01:00
// Enregistrement dans le cache et attente de la confirmation.
// Pour COMPUTE_RESOURCE, SourcePeerID différencie le peer compute (local ou distant).
// Pour STORAGE_RESOURCE, SourcePeerID est le peer hébergeant le stockage.
key := considersKey ( executionsId , dataType , event . SourcePeerID )
ch , unregister := globalConsidersCache . register ( key )
defer unregister ( )
select {
case <- ch :
logger . Info ( ) . Msg ( fmt . Sprintf ( "PB_CONSIDERS received for executions_id=%s datatype=%s source_peer=%s dest_peer=%s" , executionsId , dataType . String ( ) , event . SourcePeerID , event . DestPeerID ) )
errCh <- nil
case <- time . After ( 5 * time . Minute ) :
err := fmt . Errorf ( "timeout waiting for PB_CONSIDERS executions_id=%s datatype=%s" , executionsId , dataType . String ( ) )
logger . Error ( ) . Msg ( err . Error ( ) )
errCh <- err
2026-02-25 09:02:24 +01:00
}
}
// ArgoKubeEvent est la structure publiée sur NATS lors de la demande de
// provisionnement d'une ressource distante (Admiralty ou stockage S3).
// Le champ OriginID identifie le peer initiateur : c'est vers lui que la
// réponse PB_CONSIDERS sera routée par le système de propagation.
type ArgoKubeEvent struct {
// ExecutionsID est l'identifiant de l'exécution de workflow en cours.
ExecutionsID string ` json:"executions_id" `
// DestPeerID est le peer de destination (compute ou peer S3 cible).
DestPeerID string ` json:"dest_peer_id" `
// Type indique la nature de la ressource : COMPUTE_RESOURCE ou STORAGE_RESOURCE.
Type tools . DataType ` json:"data_type" `
// SourcePeerID est le peer source de la ressource demandée.
SourcePeerID string ` json:"source_peer_id" `
// OriginID est le peer qui a initié la demande de provisionnement ;
// la réponse PB_CONSIDERS lui sera renvoyée.
OriginID string ` json:"origin_id" `
2026-03-25 11:13:12 +01:00
// MinioID est l'ID de la ressource storage (Minio ou local PVC).
MinioID string ` json:"minio_id,omitempty" `
// Local signale un storage Local=true (PVC pré-provisionné par oc-datacenter).
Local bool ` json:"local,omitempty" `
// StorageName est le nom normalisé du storage, utilisé pour calculer le claimName.
StorageName string ` json:"storage_name,omitempty" `
// Images est la liste des images de conteneurs à pre-pull sur le peer cible
// avant le démarrage du workflow. Vide pour les events STORAGE_RESOURCE.
Images [ ] string ` json:"images,omitempty" `
2025-04-28 14:01:57 +02:00
}
2025-02-28 14:15:59 +01:00
2026-03-25 11:13:12 +01:00
// addPeerImage enregistre une image à pre-pull pour un peer donné.
// Clé "" désigne le peer local. Les doublons sont ignorés.
func ( b * ArgoBuilder ) addPeerImage ( peerID , image string ) {
if image == "" {
return
}
if b . PeerImages == nil {
b . PeerImages = make ( map [ string ] [ ] string )
}
for _ , existing := range b . PeerImages [ peerID ] {
if existing == image {
return
}
}
b . PeerImages [ peerID ] = append ( b . PeerImages [ peerID ] , image )
}
2026-02-25 09:02:24 +01:00
// CompleteBuild finalise la construction du workflow Argo après la génération
// du DAG. Elle effectue dans l'ordre :
// 1. Pour chaque peer distant (Admiralty) : publie un ArgoKubeEvent de type
// COMPUTE_RESOURCE et attend la validation PB_CONSIDERS via waitForConsiders.
// 2. Met à jour les annotations Admiralty des templates avec le nom de cluster
// construit à partir du peerId et de l'executionsId.
// 3. Sérialise le workflow en YAML et l'écrit dans ./argo_workflows/.
//
// Retourne le chemin du fichier YAML généré.
2025-04-02 11:40:14 +02:00
func ( b * ArgoBuilder ) CompleteBuild ( executionsId string ) ( string , error ) {
2026-02-25 09:02:24 +01:00
logger . Info ( ) . Msg ( "DEV :: Completing build" )
2026-03-25 11:13:12 +01:00
// --- Étape 1 : validation kube pour tous les peers (local + distants) ---
// Les goroutines tournent en parallèle ; un timeout est une erreur fatale.
// Déduplique RemotePeers : plusieurs processings peuvent pointer vers le même
// peer distant, on ne doit envoyer qu'un seul ArgoKubeEvent par peer.
seen := make ( map [ string ] struct { } )
uniqueRemotePeers := b . RemotePeers [ : 0 ]
for _ , p := range b . RemotePeers {
if _ , ok := seen [ p ] ; ! ok {
seen [ p ] = struct { } { }
uniqueRemotePeers = append ( uniqueRemotePeers , p )
}
}
b . RemotePeers = uniqueRemotePeers
total := len ( b . RemotePeers )
if b . HasLocalCompute {
total ++
}
var wg sync . WaitGroup
errCh := make ( chan error , total )
// Le kube local doit aussi être configuré s'il porte au moins un processing.
if b . HasLocalCompute {
if localPeer , err := oclib . GetMySelf ( ) ; err == nil {
logger . Info ( ) . Msg ( "DEV :: Launching local kube setup for " + localPeer . GetID ( ) )
wg . Add ( 1 )
go waitForConsiders ( executionsId , tools . COMPUTE_RESOURCE , ArgoKubeEvent {
ExecutionsID : executionsId ,
Type : tools . COMPUTE_RESOURCE ,
DestPeerID : localPeer . GetID ( ) ,
SourcePeerID : localPeer . GetID ( ) ,
OriginID : localPeer . GetID ( ) ,
Images : b . PeerImages [ "" ] , // images à pre-pull sur le cluster local
} , & wg , errCh )
}
}
// Peers distants via Admiralty.
2025-07-28 15:52:27 +02:00
for _ , peer := range b . RemotePeers {
logger . Info ( ) . Msg ( fmt . Sprint ( "DEV :: Launching Admiralty Setup for " , peer ) )
2026-03-25 11:13:12 +01:00
if self , err := oclib . GetMySelf ( ) ; err == nil {
wg . Add ( 1 )
go waitForConsiders ( executionsId , tools . COMPUTE_RESOURCE , ArgoKubeEvent {
ExecutionsID : executionsId ,
Type : tools . COMPUTE_RESOURCE ,
DestPeerID : self . GetID ( ) ,
SourcePeerID : peer ,
OriginID : self . GetID ( ) ,
Images : b . PeerImages [ peer ] , // images à pre-pull sur le cluster distant (via kubeconfig Admiralty)
} , & wg , errCh )
}
}
wg . Wait ( )
close ( errCh )
for err := range errCh {
if err != nil {
return "" , err
2025-07-28 15:52:27 +02:00
}
}
2025-04-29 12:23:44 +02:00
2026-03-25 11:13:12 +01:00
// --- Étape 2 : génération et écriture du fichier YAML ---
2025-04-29 12:23:44 +02:00
random_name := fakelish . GenerateFakeWord ( 5 , 8 ) + "-" + fakelish . GenerateFakeWord ( 5 , 8 )
b . Workflow . Metadata . Name = "oc-monitor-" + random_name
logger = oclib . GetLogger ( )
yamlified , err := yaml . Marshal ( b . Workflow )
if err != nil {
logger . Error ( ) . Msg ( "Could not transform object to yaml file" )
2026-01-14 15:17:37 +01:00
return "" , err
2025-04-29 12:23:44 +02:00
}
2026-02-25 09:02:24 +01:00
// Nom de fichier horodaté au format DD_MM_YYYY_hhmmss.
2026-03-25 11:13:12 +01:00
current_timestamp := time . Now ( ) . UTC ( ) . Format ( "02_01_2006_150405" )
2025-04-29 12:23:44 +02:00
file_name := random_name + "_" + current_timestamp + ".yml"
workflows_dir := "./argo_workflows/"
err = os . WriteFile ( workflows_dir + file_name , [ ] byte ( yamlified ) , 0660 )
if err != nil {
logger . Error ( ) . Msg ( "Could not write the yaml file" )
return "" , err
}
return workflows_dir + file_name , nil
2025-04-28 14:01:57 +02:00
}