Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 411effb000 |
@@ -22,5 +22,3 @@
|
||||
go.work
|
||||
|
||||
argo_workflows/*
|
||||
env.env
|
||||
oc-monitord
|
||||
@@ -12,16 +12,16 @@ clean:
|
||||
rm -rf oc-monitord
|
||||
|
||||
docker:
|
||||
DOCKER_BUILDKIT=1 docker build -t oc-monitord -f Dockerfile .
|
||||
docker tag oc-monitord opencloudregistry/oc-monitord:latest
|
||||
DOCKER_BUILDKIT=1 docker build -t oc/oc-monitord:0.0.1 -f Dockerfile .
|
||||
docker tag oc/oc-monitord:0.0.1 oc/oc-monitord:latest
|
||||
docker tag oc/oc-monitord:0.0.1 oc-monitord:latest
|
||||
|
||||
publish-kind:
|
||||
kind load docker-image opencloudregistry/oc-monitord:latest --name $(CLUSTER_NAME)
|
||||
kind load docker-image oc/oc-monitord:0.0.1 --name opencloud
|
||||
|
||||
publish-registry:
|
||||
docker push opencloudregistry/oc-monitord:latest
|
||||
@echo "TODO"
|
||||
|
||||
all: docker publish-kind
|
||||
|
||||
ci: docker publish-registry
|
||||
all: docker publish-kind publish-registry
|
||||
|
||||
.PHONY: build run clean docker publish-kind publish-registry
|
||||
|
||||
+5
-29
@@ -1,13 +1,12 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/config"
|
||||
)
|
||||
import "sync"
|
||||
|
||||
type Config struct {
|
||||
MongoURL string
|
||||
Database string
|
||||
LokiURL string
|
||||
NatsURL string
|
||||
ExecutionID string
|
||||
PeerID string
|
||||
Timeout int
|
||||
@@ -20,29 +19,6 @@ type Config struct {
|
||||
KubeCert string
|
||||
KubeData string
|
||||
ArgoHost string // when executed in a container will replace addresses with "localhost" in their url
|
||||
// OCNamespace est le namespace Kubernetes où tournent les composants OpenCloud (NATS, etc.).
|
||||
// Utilisé pour construire le FQDN NATS accessible depuis n'importe quel namespace.
|
||||
// Valeur par défaut : "opencloud".
|
||||
NatsUrl string
|
||||
OCNamespace string
|
||||
// ScheduledTime is the wall-clock time at which the Argo workflow must be submitted.
|
||||
// oc-monitord completes pre-pull + infra setup first, then waits until this time.
|
||||
// Zero value means "submit immediately after prep".
|
||||
ScheduledTime time.Time
|
||||
}
|
||||
|
||||
// NATSPodURL retourne l'URL NATS utilisable depuis un pod dans n'importe quel namespace.
|
||||
// Les pods Argo tournent dans le namespace executions_id, pas dans OCNamespace,
|
||||
// donc le FQDN complet est nécessaire pour atteindre le service NATS.
|
||||
func (c *Config) NATSPodURL() string {
|
||||
if config.GetConfig().NATSUrl == "" {
|
||||
ns := c.OCNamespace
|
||||
if ns == "" {
|
||||
ns = "opencloud"
|
||||
}
|
||||
return "nats." + ns + ".svc.cluster.local:4222"
|
||||
}
|
||||
return config.GetConfig().NATSUrl
|
||||
}
|
||||
|
||||
var instance *Config
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"oc-catalog": "https://oc-catalog:8087"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"oc-catalog": "https://localhost:8087"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
KUBERNETES_SERVICE_HOST=192.168.1.169
|
||||
KUBE_CA="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTnpReU56STVNVEF3SGhjTk1qWXdNekl6TVRNek5URXdXaGNOTXpZd016SXdNVE16TlRFdwpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTnpReU56STVNVEF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSSGpYRDVpbnRIYWZWSk5VaDFlRnIxcXBKdFlkUmc5NStKVENEa0tadTIKYjUxRXlKaG1zanRIY3BDUndGL1VGMzlvdzY4TFBUcjBxaUorUHlhQTBLZUtvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTdWQkNzZVN3ajJ2cmczMFE5UG8vCnV6ZzAvMjR3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUlEOVY2aFlUSS83ZW1hRzU0dDdDWVU3TXFSdDdESUkKNlgvSUwrQ0RLbzlNQWlCdlFEMGJmT0tVWDc4UmRGdUplcEhEdWFUMUExaGkxcWdIUGduM1dZdDBxUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
KUBE_CERT="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJUU5KbFNJQUJPMDR3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOemMwTWpjeU9URXdNQjRYRFRJMk1ETXlNekV6TXpVeE1Gb1hEVEkzTURNeQpNekV6TXpVeE1Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMY3Uwb2pUbVg4RFhTQkYKSHZwZDZNVEoyTHdXc1lRTmdZVURXRDhTVERIUWlCczlMZ0x5ZTdOMEFvZk85RkNZVW1HamhiaVd3WFVHR3dGTgpUdlRMU2lXalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUlJhRW9wQzc5NGJyTHlnR0g5SVhvbDZTSmlFREFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQWhaRUlrSWV3Y1loL1NmTFVCVjE5MW1CYTNRK0J5S2J5eTVlQmpwL3kzeWtDSUIxWTJicTVOZTNLUUU4RAprNnNzeFJrbjJmN0VoWWVRQU1pUlJ2MjIweDNLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTnpReU56STVNVEF3SGhjTk1qWXdNekl6TVRNek5URXdXaGNOTXpZd016SXdNVE16TlRFdwpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTnpReU56STVNVEF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTcTdVTC85MEc1ZmVTaE95NjI3eGFZWlM5dHhFdWFoWFQ3Vk5wZkpQSnMKaEdXd2UxOXdtbXZzdlp6dlNPUWFRSzJaMmttN0hSb1IrNlA1YjIyamczbHVvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVVXaEtLUXUvZUc2eThvQmgvU0Y2Ckpla2lZaEF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUk3cGxHczFtV20ySDErbjRobDBNTk13RmZzd0o5ZXIKTzRGVkM0QzhwRG44QWlCN3NZMVFwd2M5VkRUeGNZaGxuZzZNUzRXai85K0lHWjJxcy94UStrMjdTQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
KUBE_DATA="LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUROZDRnWXd6aVRhK1hwNnFtNVc3SHFzc1JJNkREaUJTbUV2ZHoxZzk3VGxvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFdHk3U2lOT1pmd05kSUVVZStsM294TW5ZdkJheGhBMkJoUU5ZUHhKTU1kQ0lHejB1QXZKNwpzM1FDaDg3MFVKaFNZYU9GdUpiQmRRWWJBVTFPOU10S0pRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
||||
KUBE_CA="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTWpNeE1USXdNell3SGhjTk1qUXdPREE0TVRBeE16VTJXaGNOTXpRd09EQTJNVEF4TXpVMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTWpNeE1USXdNell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTVlk3ZHZhNEdYTVdkMy9jMlhLN3JLYjlnWXgyNSthaEE0NmkyNVBkSFAKRktQL2UxSVMyWVF0dzNYZW1TTUQxaStZdzJSaVppNUQrSVZUamNtNHdhcnFvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWtlUVJpNFJiODduME5yRnZaWjZHClc2SU55NnN3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnRXA5ck04WmdNclRZSHYxZjNzOW5DZXZZeWVVa3lZUk4KWjUzazdoaytJS1FDSVFDbk05TnVGKzlTakIzNDFacGZ5ays2NEpWdkpSM3BhcmVaejdMd2lhNm9kdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
KUBE_CERT="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJWUxWNkFPQkdrU1F3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekl6TVRFeU1ETTJNQjRYRFRJME1EZ3dPREV3TVRNMU5sb1hEVEkxTURndwpPREV3TVRNMU5sb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJGQ2Q1MFdPeWdlQ2syQzcKV2FrOWY4MVAvSkJieVRIajRWOXBsTEo0ck5HeHFtSjJOb2xROFYxdUx5RjBtOTQ2Nkc0RmRDQ2dqaXFVSk92Swp3NVRPNnd5alNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVFJkOFI5cXVWK2pjeUVmL0ovT1hQSzMyS09XekFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQTArbThqTDBJVldvUTZ0dnB4cFo4NVlMalF1SmpwdXM0aDdnSXRxS3NmUVVDSUI2M2ZNdzFBMm5OVWU1TgpIUGZOcEQwSEtwcVN0Wnk4djIyVzliYlJUNklZCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTWpNeE1USXdNell3SGhjTk1qUXdPREE0TVRBeE16VTJXaGNOTXpRd09EQTJNVEF4TXpVMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTWpNeE1USXdNell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRc3hXWk9pbnIrcVp4TmFEQjVGMGsvTDF5cE01VHAxOFRaeU92ektJazQKRTFsZWVqUm9STW0zNmhPeVljbnN3d3JoNnhSUnBpMW5RdGhyMzg0S0Z6MlBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBYZkVmYXJsZm8zTWhIL3lmemx6Cnl0OWlqbHN3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQUxJL2dNYnNMT3MvUUpJa3U2WHVpRVMwTEE2cEJHMXgKcnBlTnpGdlZOekZsQWlFQW1wdjBubjZqN3M0MVI0QzFNMEpSL0djNE53MHdldlFmZWdEVGF1R2p3cFk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
KUBE_DATA="LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU5ZS1BFb1dhd1NKUzJlRW5oWmlYMk5VZlY1ZlhKV2krSVNnV09TNFE5VTlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUozblJZN0tCNEtUWUx0WnFUMS96VS84a0Z2Sk1lUGhYMm1Vc25pczBiR3FZblkyaVZEeApYVzR2SVhTYjNqcm9iZ1YwSUtDT0twUWs2OHJEbE03ckRBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
||||
@@ -1,34 +1,30 @@
|
||||
module oc-monitord
|
||||
|
||||
go 1.25.0
|
||||
go 1.23.1
|
||||
|
||||
toolchain go1.23.3
|
||||
|
||||
require (
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260527135023-cef23b5f307b
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20250313155727-88c88cac5bc9
|
||||
github.com/akamensky/argparse v1.4.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/goraz/onion v0.1.3
|
||||
github.com/nwtgck/go-fakelish v0.1.3
|
||||
github.com/rs/zerolog v1.34.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beego/beego/v2 v2.3.8 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/beego/beego/v2 v2.3.7 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/goraz/onion v0.1.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/libp2p/go-libp2p/core v0.43.0-rc2 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/grpc v1.63.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -37,10 +33,10 @@ require (
|
||||
github.com/biter777/countries v1.7.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
@@ -48,7 +44,9 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -59,16 +57,18 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/nats.go v1.44.0
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nats.go v1.41.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.10 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -76,24 +76,25 @@ require (
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/api v0.35.1
|
||||
k8s.io/apimachinery v0.35.1
|
||||
k8s.io/client-go v0.35.1
|
||||
k8s.io/api v0.32.1
|
||||
k8s.io/apimachinery v0.32.1
|
||||
k8s.io/client-go v0.32.1
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,44 +1,16 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260319065647-5b7edb53a984 h1:6HAlL367LM75T7IokS5H4y7iZg8mrk05uAy/yANKwdc=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260319065647-5b7edb53a984/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260319071818-28b5b7d39ffe h1:CHiWQAX7j/bMfbytCWGL2mUgSWYoDY4+bFQbCHEfypk=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260319071818-28b5b7d39ffe/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260320093030-a62fbc6c7a03 h1:GyfeEHGlyQIFtuzmwsJZ9b64dr9D7zvi6RCo1e/E5wc=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260320093030-a62fbc6c7a03/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260320103359-c34b8c67038b h1:VdLBRXb0wSsR9lzkoEGvhScRe4cNJy/QoGTkyG302uQ=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260320103359-c34b8c67038b/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260320151407-88d2e526283b h1:QEdy0FxwWcXYHVLcC06tRmhFl6T/pr2M7l2Auni/sSU=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260320151407-88d2e526283b/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260326110203-87cf2cb12af0 h1:pQf9k+GSzNGEmrUa00jn9Zcqfp9X4N1Z5ie7InvUf3g=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260326110203-87cf2cb12af0/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260402123119-a2f6f3c252ac h1:nCr9cWzPNdEuwjG/KDOYslKw4kHE8hJXzGI81jDNf/A=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260402123119-a2f6f3c252ac/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260402124551-4f0714cb1182 h1:1SQm0TfFIpn+3fJpFgxibx0V8uAqaf4DpjDL28+bkqs=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260402124551-4f0714cb1182/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260402125506-54985bbc4543 h1:gP+DrjkHZJ4I1xUkR/4DbfW1mVdHoAwmmkte9TEiPwM=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260402125506-54985bbc4543/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260410075751-d7b2ef6ae120 h1:CMOOpmpgkD63Gq7ukmXG6r+WlJxvpSgDRmalpWPhaIg=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260410075751-d7b2ef6ae120/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260414104622-dc0041999d22 h1:lum7G12vCKYKQWXTOYtl2Qh9hLRlzrcOPO3pozUBL40=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260414104622-dc0041999d22/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260423072402-9c2663601a2b h1:h1Rwra0Ljp8bhj7L5t9NEtP51lbg7RFySY1XMTprEXE=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260423072402-9c2663601a2b/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260427091650-f048b420d74d h1:jzgwgbZDASalQJSYbPF/L2L2RSP2OAbqhMB4YUXK27M=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260427091650-f048b420d74d/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260527135023-cef23b5f307b h1:TWhmHeurbBmdyevREh4+mHWOBehO2AK587RCIjCfvOc=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20260527135023-cef23b5f307b/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20250313155727-88c88cac5bc9 h1:mSFFPwil5Ih+RPBvn88MBerQMtsoHnOuyCZQaf91a34=
|
||||
cloud.o-forge.io/core/oc-lib v0.0.0-20250313155727-88c88cac5bc9/go.mod h1:2roQbUpv3a6mTIr5oU1ux31WbN8YucyyQvCQ0FqwbcE=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
|
||||
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/argoproj/argo-workflows/v3 v3.6.4 h1:5+Cc1UwaQE5ka3w7R3hxZ1TK3M6VjDEXA5WSQ/IXrxY=
|
||||
github.com/argoproj/argo-workflows/v3 v3.6.4/go.mod h1:2f5zB8CkbNCCO1od+kd1dWkVokqcuyvu+tc+Jwx1MZg=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc=
|
||||
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg=
|
||||
github.com/beego/beego/v2 v2.3.7 h1:z4btKtjU/rfp5BiYHkGD2QPjK9i1E9GH+I7vfhn6Agk=
|
||||
github.com/beego/beego/v2 v2.3.7/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
|
||||
@@ -59,27 +31,23 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||
@@ -92,8 +60,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@@ -108,15 +76,18 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -131,8 +102,6 @@ github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -144,8 +113,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -154,10 +121,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-libp2p/core v0.43.0-rc2 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
|
||||
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
@@ -169,8 +132,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -180,61 +141,47 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc=
|
||||
github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo=
|
||||
github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nats.go v1.41.0 h1:PzxEva7fflkd+n87OtQTXqCTyLfIIMFJBpyccHLE2Ko=
|
||||
github.com/nats-io/nats.go v1.41.0/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
|
||||
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
|
||||
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nwtgck/go-fakelish v0.1.3 h1:bA8/xa9hQmzppexIhBvdmztcd/PJ4SPuAUTBdMKZ8G4=
|
||||
github.com/nwtgck/go-fakelish v0.1.3/go.mod h1:2HC44/OwVWwOa/g3+P2jUM3FEHQ0ya4gyCSU19PPd3Y=
|
||||
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
@@ -249,24 +196,20 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
@@ -285,33 +228,23 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -324,12 +257,12 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -337,8 +270,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -354,20 +287,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -378,8 +311,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -402,13 +335,13 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
|
||||
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -420,25 +353,21 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
|
||||
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
|
||||
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
|
||||
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
|
||||
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
|
||||
k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
|
||||
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
|
||||
k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
|
||||
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
|
||||
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
||||
+28
-386
@@ -11,9 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
octools "cloud.o-forge.io/core/oc-lib/tools"
|
||||
"github.com/rs/zerolog"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
|
||||
)
|
||||
@@ -47,7 +46,7 @@ func NewArgoLogs(name string, namespace string, stepMax int) *ArgoLogs {
|
||||
return &ArgoLogs{
|
||||
Name: "oc-monitor-" + name,
|
||||
Namespace: namespace,
|
||||
CreatedDate: time.Now().UTC().Format("2006-01-02 15:04:05"),
|
||||
CreatedDate: time.Now().Format("2006-01-02 15:04:05"),
|
||||
StepCount: 0,
|
||||
StepMax: stepMax,
|
||||
stop: false,
|
||||
@@ -94,6 +93,7 @@ func (a *ArgoLogs) StartStepRecording(current_watch *ArgoWatch, logger zerolog.L
|
||||
a.Started = time.Now()
|
||||
}
|
||||
|
||||
|
||||
type ArgoPodLog struct {
|
||||
PodName string
|
||||
Step string
|
||||
@@ -108,100 +108,38 @@ func NewArgoPodLog(name string, step string, msg string) ArgoPodLog {
|
||||
}
|
||||
}
|
||||
|
||||
// LogKubernetesArgo watches an Argo workflow and emits NATS lifecycle events.
|
||||
// It no longer writes directly to the database — all state transitions are
|
||||
// delegated to oc-scheduler (WorkflowExecution) and oc-datacenter (Bookings)
|
||||
// via the dedicated NATS channels.
|
||||
//
|
||||
// When the Kubernetes watch channel closes before the workflow reaches a
|
||||
// terminal state (API server timeout, network blip, etc.), the function
|
||||
// fetches the actual workflow state directly. If the workflow is still
|
||||
// running it reconnects the watcher and continues. Only a genuine terminal
|
||||
// failure emits WORKFLOW_DONE_EVENT with FAILURE.
|
||||
//
|
||||
// - wfName : Argo workflow name as returned by CreateArgoWorkflow (used
|
||||
// as the root DAG node key in wf.Status.Nodes)
|
||||
// - execID : WorkflowExecution UUID
|
||||
// - executionsID: run-group ID / Kubernetes namespace of the workflow
|
||||
// - namespace : Kubernetes namespace for pod log streaming
|
||||
// - tool : Kubernetes/Argo client (used to reconnect + fetch state)
|
||||
// - rawWfName : workflow name without the "oc-monitor-" prefix (passed to
|
||||
// GetArgoWatch which prepends it)
|
||||
func LogKubernetesArgo(wfName string, execID string, executionsID string, namespace string, tool tools.Tool, rawWfName string) {
|
||||
func LogKubernetesArgo(wfName string, namespace string, watcher watch.Interface) {
|
||||
var argoWatcher *ArgoWatch
|
||||
var pods []string
|
||||
var node wfv1.NodeStatus
|
||||
|
||||
wfl := utils.GetWFLogger("")
|
||||
wfl.Debug().Msg("Starting to log " + wfName)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// nodePhases tracks the last known phase of each step node so we can detect
|
||||
// phase transitions and emit WORKFLOW_STEP_DONE_EVENT exactly once per step.
|
||||
// Kept across watcher reconnections so we never double-emit.
|
||||
nodePhases := map[string]wfv1.NodePhase{}
|
||||
|
||||
// stepResults captures the final NodeStatus of every completed step so the
|
||||
// WORKFLOW_DONE_EVENT can include a full recap (Steps slice).
|
||||
stepResults := map[string]wfv1.NodeStatus{}
|
||||
|
||||
workflowStartedEmitted := false
|
||||
|
||||
for {
|
||||
watcher, err := tool.GetArgoWatch(executionsID, rawWfName)
|
||||
if err != nil {
|
||||
wfl.Error().Msg("Could not create watcher: " + err.Error())
|
||||
if resolveAndEmitTerminal(wfName, execID, executionsID, tool, nodePhases, stepResults, &wg, wfl) {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
reachedTerminalState := false
|
||||
var node wfv1.NodeStatus
|
||||
|
||||
for event := range watcher.ResultChan() {
|
||||
for event := range (watcher.ResultChan()) {
|
||||
wf, ok := event.Object.(*wfv1.Workflow)
|
||||
if !ok {
|
||||
wfl.Error().Msg("unexpected type")
|
||||
continue
|
||||
}
|
||||
if len(wf.Status.Nodes) == 0 {
|
||||
wfl.Info().Msg("No node status yet")
|
||||
wfl.Debug().Msg("No node status yet") // The first output of the channel doesn't contain Nodes so we skip it
|
||||
continue
|
||||
}
|
||||
|
||||
// ── Emit WORKFLOW_STARTED_EVENT once ────────────────────────────────
|
||||
if !workflowStartedEmitted {
|
||||
realStart := wf.Status.StartedAt.Time
|
||||
if realStart.IsZero() {
|
||||
realStart = time.Now().UTC()
|
||||
}
|
||||
emitLifecycleEvent(octools.WORKFLOW_STARTED_EVENT, octools.WorkflowLifecycleEvent{
|
||||
ExecutionID: execID,
|
||||
ExecutionsID: executionsID,
|
||||
State: enum.STARTED.EnumIndex(),
|
||||
RealStart: &realStart,
|
||||
})
|
||||
workflowStartedEmitted = true
|
||||
}
|
||||
|
||||
conditions := retrieveCondition(wf)
|
||||
|
||||
// Retrieving the Status for the main node, which is named after the workflow
|
||||
if node, ok = wf.Status.Nodes[wfName]; !ok {
|
||||
bytified, _ := json.MarshalIndent(wf.Status.Nodes,"","\t")
|
||||
wfl.Error().Msg("Could not find the " + wfName + " node in \n" + string(bytified))
|
||||
continue
|
||||
wfl.Fatal().Msg("Could not find the " + wfName + " node in \n" + string(bytified))
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
start := node.StartedAt.Time.UTC()
|
||||
var duration time.Duration
|
||||
if !start.IsZero() {
|
||||
duration = now.Sub(start)
|
||||
}
|
||||
now := time.Now()
|
||||
start, _ := time.Parse(time.RFC3339, node.StartedAt.String() )
|
||||
duration := now.Sub(start)
|
||||
|
||||
newWatcher := ArgoWatch{
|
||||
Name: node.Name,
|
||||
@@ -224,63 +162,12 @@ func LogKubernetesArgo(wfName string, execID string, executionsID string, namesp
|
||||
argoWatcher = &newWatcher
|
||||
}
|
||||
|
||||
// ── Per-step completion detection ────────────────────────────────────
|
||||
for _, stepNode := range wf.Status.Nodes {
|
||||
if stepNode.Name == wfName {
|
||||
continue // skip the main DAG node
|
||||
}
|
||||
prev := nodePhases[stepNode.Name]
|
||||
nodePhases[stepNode.Name] = stepNode.Phase
|
||||
|
||||
if prev == stepNode.Phase {
|
||||
continue // no change
|
||||
}
|
||||
if !stepNode.Phase.Completed() && !stepNode.Phase.FailedOrError() {
|
||||
continue // not terminal yet
|
||||
}
|
||||
if prev.Completed() || prev.FailedOrError() {
|
||||
continue // already processed
|
||||
}
|
||||
|
||||
bookingID := extractBookingID(stepNode.Name)
|
||||
if bookingID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
stepState := enum.SUCCESS
|
||||
if stepNode.Phase.FailedOrError() {
|
||||
if !(strings.Contains(stepNode.Message, "context cancel") || strings.Contains(stepNode.Message, "exit")) {
|
||||
fmt.Println("1 baraka", stepNode.Message)
|
||||
stepState = enum.FAILURE
|
||||
}
|
||||
}
|
||||
realStart := stepNode.StartedAt.Time
|
||||
realEnd := stepNode.FinishedAt.Time
|
||||
if realEnd.IsZero() {
|
||||
realEnd = time.Now().UTC()
|
||||
}
|
||||
if realStart.IsZero() {
|
||||
realStart = realEnd
|
||||
}
|
||||
emitLifecycleEvent(octools.WORKFLOW_STEP_DONE_EVENT, octools.WorkflowLifecycleEvent{
|
||||
ExecutionID: execID,
|
||||
ExecutionsID: executionsID,
|
||||
BookingID: bookingID,
|
||||
State: stepState.EnumIndex(),
|
||||
RealStart: &realStart,
|
||||
RealEnd: &realEnd,
|
||||
})
|
||||
// Store for the final recap emitted with WORKFLOW_DONE_EVENT.
|
||||
stepResults[bookingID] = stepNode
|
||||
}
|
||||
|
||||
// ── Pod log streaming ────────────────────────────────────────────────
|
||||
// I don't think we need to use WaitGroup here, because the loop itself
|
||||
// acts as blocking process for the main thread, because Argo watch never closes the channel
|
||||
for _, pod := range wf.Status.Nodes{
|
||||
if pod.Type != wfv1.NodeTypePod {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(pods,pod.Name){
|
||||
pl := wfl.With().Str("pod", pod.Name).Logger()
|
||||
if wfName == pod.Name { pods = append(pods, pod.Name); continue } // One of the node is the Workflow, the others are the pods so don't try to log on the wf name
|
||||
pl.Info().Msg("Found a new pod to log : " + pod.Name)
|
||||
wg.Add(1)
|
||||
go logKubernetesPods(namespace, wfName, pod.Name, pl, &wg)
|
||||
@@ -288,246 +175,19 @@ func LogKubernetesArgo(wfName string, execID string, executionsID string, namesp
|
||||
}
|
||||
}
|
||||
|
||||
// ── Workflow terminal phase ──────────────────────────────────────────
|
||||
if node.Phase.Completed() || node.Phase.FailedOrError() {
|
||||
// Stop listening to the chan when the Workflow is completed or something bad happened
|
||||
if node.Phase.Completed() {
|
||||
wfl.Info().Msg(wfName + " workflow completed")
|
||||
} else {
|
||||
wfl.Error().Msg(wfName + " has failed, please refer to the logs")
|
||||
wfl.Error().Msg(node.Message)
|
||||
}
|
||||
wfl.Info().Msg(wfName + " worflow completed")
|
||||
wg.Wait()
|
||||
wfl.Info().Msg(wfName + " exiting")
|
||||
|
||||
finalState := enum.SUCCESS
|
||||
break
|
||||
}
|
||||
if node.Phase.FailedOrError() {
|
||||
if !(strings.Contains(node.Message, "context cancel") || strings.Contains(node.Message, "exit")) {
|
||||
fmt.Println("2 baraka", node.Message)
|
||||
finalState = enum.FAILURE
|
||||
}
|
||||
}
|
||||
realStart := node.StartedAt.Time
|
||||
realEnd := node.FinishedAt.Time
|
||||
if realEnd.IsZero() {
|
||||
realEnd = time.Now().UTC()
|
||||
}
|
||||
if realStart.IsZero() {
|
||||
realStart = realEnd
|
||||
}
|
||||
|
||||
// Build recap from all observed step results.
|
||||
steps := make([]octools.StepMetric, 0, len(stepResults))
|
||||
for bookingID, s := range stepResults {
|
||||
stepState := enum.SUCCESS
|
||||
if s.Phase.FailedOrError() {
|
||||
if !(strings.Contains(s.Message, "context cancel") || strings.Contains(s.Message, "exit")) {
|
||||
fmt.Println("3 baraka", s.Message)
|
||||
stepState = enum.FAILURE
|
||||
}
|
||||
}
|
||||
start := s.StartedAt.Time
|
||||
end := s.FinishedAt.Time
|
||||
if end.IsZero() {
|
||||
end = realEnd
|
||||
}
|
||||
steps = append(steps, octools.StepMetric{
|
||||
BookingID: bookingID,
|
||||
State: stepState.EnumIndex(),
|
||||
RealStart: &start,
|
||||
RealEnd: &end,
|
||||
})
|
||||
}
|
||||
|
||||
emitLifecycleEvent(octools.WORKFLOW_DONE_EVENT, octools.WorkflowLifecycleEvent{
|
||||
ExecutionID: execID,
|
||||
ExecutionsID: executionsID,
|
||||
State: finalState.EnumIndex(),
|
||||
RealStart: &realStart,
|
||||
RealEnd: &realEnd,
|
||||
Steps: steps,
|
||||
})
|
||||
reachedTerminalState = true
|
||||
wfl.Error().Msg(wfName + "has failed, please refer to the logs")
|
||||
wfl.Error().Msg(node.Message)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
watcher.Stop()
|
||||
|
||||
if reachedTerminalState {
|
||||
return
|
||||
}
|
||||
|
||||
// ── Watcher closed before terminal state ─────────────────────────────
|
||||
// The Kubernetes watch API closes the channel on server-side timeouts,
|
||||
// API server restarts, or network blips. Do NOT assume failure: fetch
|
||||
// the actual workflow state and reconnect if still running.
|
||||
wfl.Warn().Msg(wfName + " watcher closed before workflow reached terminal state — checking actual state")
|
||||
|
||||
if resolveAndEmitTerminal(wfName, execID, executionsID, tool, nodePhases, stepResults, &wg, wfl) {
|
||||
return
|
||||
}
|
||||
|
||||
wfl.Info().Msg(wfName + " workflow still running, reconnecting watcher")
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveAndEmitTerminal fetches the live workflow state via the API. If the
|
||||
// workflow has reached a terminal phase it emits any missing STEP_DONE events
|
||||
// followed by WORKFLOW_DONE_EVENT and returns true. Returns false when the
|
||||
// workflow is still running or the state cannot be determined (caller should
|
||||
// reconnect the watcher).
|
||||
func resolveAndEmitTerminal(
|
||||
wfName string, execID string, executionsID string,
|
||||
tool tools.Tool,
|
||||
nodePhases map[string]wfv1.NodePhase,
|
||||
stepResults map[string]wfv1.NodeStatus,
|
||||
wg *sync.WaitGroup,
|
||||
wfl zerolog.Logger,
|
||||
) bool {
|
||||
wf, err := tool.GetArgoWorkflow(executionsID, wfName)
|
||||
if err != nil {
|
||||
wfl.Warn().Msg("Could not fetch workflow state: " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
rootNode, ok := wf.Status.Nodes[wfName]
|
||||
if !ok {
|
||||
wfl.Warn().Msg("Root node " + wfName + " not found in live workflow status")
|
||||
return false
|
||||
}
|
||||
|
||||
if !rootNode.Phase.Completed() && !rootNode.Phase.FailedOrError() {
|
||||
wfl.Info().Msgf("%s still running (phase=%s), will reconnect watcher", wfName, rootNode.Phase)
|
||||
return false
|
||||
}
|
||||
|
||||
// Emit any STEP_DONE events that were missed while the watcher was down.
|
||||
for _, stepNode := range wf.Status.Nodes {
|
||||
if stepNode.Name == wfName {
|
||||
continue
|
||||
}
|
||||
prev := nodePhases[stepNode.Name]
|
||||
if prev.Completed() || prev.FailedOrError() {
|
||||
continue // already emitted
|
||||
}
|
||||
if !stepNode.Phase.Completed() && !stepNode.Phase.FailedOrError() {
|
||||
continue
|
||||
}
|
||||
bookingID := extractBookingID(stepNode.Name)
|
||||
if bookingID == "" {
|
||||
continue
|
||||
}
|
||||
stepState := enum.SUCCESS
|
||||
if stepNode.Phase.FailedOrError() {
|
||||
if !(strings.Contains(stepNode.Message, "context cancel") || strings.Contains(stepNode.Message, "exit")) {
|
||||
fmt.Println("4 baraka", stepNode.Message)
|
||||
stepState = enum.FAILURE
|
||||
}
|
||||
}
|
||||
realStart := stepNode.StartedAt.Time
|
||||
realEnd := stepNode.FinishedAt.Time
|
||||
if realEnd.IsZero() {
|
||||
realEnd = time.Now().UTC()
|
||||
}
|
||||
if realStart.IsZero() {
|
||||
realStart = realEnd
|
||||
}
|
||||
fmt.Println("STEP DONE !!!!! ", stepNode.Name, stepState)
|
||||
emitLifecycleEvent(octools.WORKFLOW_STEP_DONE_EVENT, octools.WorkflowLifecycleEvent{
|
||||
ExecutionID: execID,
|
||||
ExecutionsID: executionsID,
|
||||
BookingID: bookingID,
|
||||
State: stepState.EnumIndex(),
|
||||
RealStart: &realStart,
|
||||
RealEnd: &realEnd,
|
||||
})
|
||||
stepResults[bookingID] = stepNode
|
||||
nodePhases[stepNode.Name] = stepNode.Phase
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
finalState := enum.SUCCESS
|
||||
if rootNode.Phase.FailedOrError() {
|
||||
if !(strings.Contains(rootNode.Message, "context cancel") || strings.Contains(rootNode.Message, "exit")) {
|
||||
fmt.Println("5 baraka", rootNode.Message)
|
||||
finalState = enum.FAILURE
|
||||
}
|
||||
}
|
||||
realStart := rootNode.StartedAt.Time
|
||||
realEnd := rootNode.FinishedAt.Time
|
||||
if realEnd.IsZero() {
|
||||
realEnd = time.Now().UTC()
|
||||
}
|
||||
if realStart.IsZero() {
|
||||
realStart = realEnd
|
||||
}
|
||||
|
||||
steps := make([]octools.StepMetric, 0, len(stepResults))
|
||||
for bookingID, s := range stepResults {
|
||||
stepState := enum.SUCCESS
|
||||
if s.Phase.FailedOrError() {
|
||||
if !(strings.Contains(s.Message, "context cancel") || strings.Contains(s.Message, "exit")) {
|
||||
fmt.Println("6 baraka", s.Message)
|
||||
stepState = enum.FAILURE
|
||||
}
|
||||
}
|
||||
start := s.StartedAt.Time
|
||||
end := s.FinishedAt.Time
|
||||
if end.IsZero() {
|
||||
end = realEnd
|
||||
}
|
||||
steps = append(steps, octools.StepMetric{
|
||||
BookingID: bookingID,
|
||||
State: stepState.EnumIndex(),
|
||||
RealStart: &start,
|
||||
RealEnd: &end,
|
||||
})
|
||||
}
|
||||
|
||||
emitLifecycleEvent(octools.WORKFLOW_DONE_EVENT, octools.WorkflowLifecycleEvent{
|
||||
ExecutionID: execID,
|
||||
ExecutionsID: executionsID,
|
||||
State: finalState.EnumIndex(),
|
||||
RealStart: &realStart,
|
||||
RealEnd: &realEnd,
|
||||
Steps: steps,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// emitLifecycleEvent publishes a WorkflowLifecycleEvent on the given NATS channel.
|
||||
func emitLifecycleEvent(method octools.NATSMethod, evt octools.WorkflowLifecycleEvent) {
|
||||
payload, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
octools.NewNATSCaller().SetNATSPub(method, octools.NATSResponse{
|
||||
FromApp: "oc-monitord",
|
||||
Method: int(method),
|
||||
Payload: payload,
|
||||
})
|
||||
}
|
||||
|
||||
// extractBookingID extracts the bookingID (UUID, 36 chars) from an Argo node
|
||||
// display name. Argo step nodes are named "{wfName}.{taskName}" where taskName
|
||||
// is "{resource-name}-{bookingID}" as generated by getArgoName in argo_builder.
|
||||
func extractBookingID(nodeName string) string {
|
||||
parts := strings.SplitN(nodeName, ".", 2)
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
taskName := parts[1]
|
||||
if len(taskName) < 36 {
|
||||
return ""
|
||||
}
|
||||
candidate := taskName[len(taskName)-36:]
|
||||
// Validate UUID shape: 8-4-4-4-12 with dashes at positions 8,13,18,23.
|
||||
if candidate[8] == '-' && candidate[13] == '-' && candidate[18] == '-' && candidate[23] == '-' {
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func retrieveCondition(wf *wfv1.Workflow) (c Conditions) {
|
||||
@@ -539,18 +199,16 @@ func retrieveCondition(wf *wfv1.Workflow) (c Conditions) {
|
||||
c.Completed = cond.Status == "True"
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// logKubernetesPods streams pod logs to the structured logger.
|
||||
// Function needed to be executed as a go thread
|
||||
func logKubernetesPods(executionId string, wfName string,podName string, logger zerolog.Logger, wg *sync.WaitGroup){
|
||||
defer wg.Done()
|
||||
|
||||
s := strings.SplitN(podName, ".", 2)
|
||||
if len(s) < 2 {
|
||||
logger.Error().Str("pod", podName).Msg("Unexpected pod name format, expected wfName.stepName")
|
||||
return
|
||||
}
|
||||
s := strings.Split(podName, ".")
|
||||
name := s[0] + "-" + s[1]
|
||||
step := s[1]
|
||||
|
||||
@@ -567,27 +225,11 @@ func logKubernetesPods(executionId string, wfName string, podName string, logger
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
podLog := NewArgoPodLog(name, step, line)
|
||||
jsonified, err := json.Marshal(podLog)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to marshal pod log")
|
||||
continue
|
||||
}
|
||||
// Propagate the log level from the container output so errors
|
||||
// are visible above Debug-only sinks.
|
||||
switch {
|
||||
case strings.Contains(line, "level=error") || strings.Contains(line, " ERR "):
|
||||
logger.Error().Msg(string(jsonified))
|
||||
case strings.Contains(line, "level=warning") || strings.Contains(line, " WRN "):
|
||||
logger.Warn().Msg(string(jsonified))
|
||||
default:
|
||||
log := scanner.Text()
|
||||
podLog := NewArgoPodLog(name,step,log)
|
||||
jsonified, _ := json.Marshal(podLog)
|
||||
logger.Info().Msg(string(jsonified))
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logger.Error().Err(err).Str("pod", podName).Msg("Pod log scanner error")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"oc-monitord/conf"
|
||||
@@ -15,9 +19,8 @@ import (
|
||||
|
||||
oclib "cloud.o-forge.io/core/oc-lib"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/config"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/booking"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
@@ -27,6 +30,7 @@ import (
|
||||
|
||||
"github.com/akamensky/argparse"
|
||||
"github.com/google/uuid"
|
||||
"github.com/goraz/onion"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -42,28 +46,35 @@ var wf_logger zerolog.Logger
|
||||
var parser argparse.Parser
|
||||
var workflowName string
|
||||
|
||||
func main() {
|
||||
parser = *argparse.NewParser("oc-monitord", "Launch the execution of a workflow given as a parameter and sends the produced logs to a loki database")
|
||||
loadConfig(&parser)
|
||||
const defaultConfigFile = "/etc/oc/ocmonitord_conf.json"
|
||||
const localConfigFile = "./conf/local_ocmonitord_conf.json"
|
||||
|
||||
func main() {
|
||||
|
||||
os.Setenv("test_service", "true") // Only for service demo, delete before merging on main
|
||||
parser = *argparse.NewParser("oc-monitord", "Launch the execution of a workflow given as a parameter and sends the produced logs to a loki database")
|
||||
loadConfig(false, &parser)
|
||||
oclib.InitDaemon("oc-monitord")
|
||||
|
||||
// Lance l'abonné NATS centralisé pour les confirmations PB_CONSIDERS.
|
||||
workflow_builder.StartConsidersListener()
|
||||
oclib.SetConfig(
|
||||
conf.GetConfig().MongoURL,
|
||||
conf.GetConfig().Database,
|
||||
conf.GetConfig().NatsURL,
|
||||
conf.GetConfig().LokiURL,
|
||||
conf.GetConfig().Logs,
|
||||
)
|
||||
|
||||
logger = u.GetLogger()
|
||||
|
||||
logger.Debug().Msg("Loki URL : " + config.GetConfig().LokiUrl)
|
||||
logger.Info().Msg("Workflow executed : " + conf.GetConfig().ExecutionID)
|
||||
logger.Debug().Msg("Loki URL : " + conf.GetConfig().LokiURL)
|
||||
logger.Debug().Msg("Workflow executed : " + conf.GetConfig().ExecutionID)
|
||||
exec := u.GetExecution(conf.GetConfig().ExecutionID)
|
||||
if exec == nil {
|
||||
logger.Fatal().Msg("Could not retrieve workflow ID from execution ID " + conf.GetConfig().ExecutionID + " on peer " + conf.GetConfig().PeerID)
|
||||
u.EmitExecStateUpdate(conf.GetConfig().ExecutionID, enum.FAILURE)
|
||||
return
|
||||
}
|
||||
conf.GetConfig().WorkflowID = exec.WorkflowID
|
||||
|
||||
logger.Info().Msg("Starting construction of yaml argo for workflow :" + exec.WorkflowID)
|
||||
logger.Debug().Msg("Starting construction of yaml argo for workflow :" + exec.WorkflowID)
|
||||
|
||||
if _, err := os.Stat("./argo_workflows/"); os.IsNotExist(err) {
|
||||
os.Mkdir("./argo_workflows/", 0755)
|
||||
@@ -73,55 +84,44 @@ func main() {
|
||||
// // create argo
|
||||
new_wf := workflow_builder.WorflowDB{}
|
||||
|
||||
err := new_wf.LoadFrom(conf.GetConfig().WorkflowID)
|
||||
err := new_wf.LoadFrom(conf.GetConfig().WorkflowID, conf.GetConfig().PeerID)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not retrieve workflow " + conf.GetConfig().WorkflowID + " from oc-catalog API")
|
||||
}
|
||||
fmt.Println("ExportToArgo")
|
||||
builder, _, err := new_wf.ExportToArgo(exec, conf.GetConfig().Timeout) // Removed stepMax so far, I don't know if we need it anymore
|
||||
fmt.Println("ExportToArgo", err)
|
||||
|
||||
builder, _, err := new_wf.ExportToArgo(exec.ExecutionsID, conf.GetConfig().Timeout) // Removed stepMax so far, I don't know if we need it anymore
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not create the Argo file for " + conf.GetConfig().WorkflowID)
|
||||
logger.Error().Msg(err.Error())
|
||||
u.EmitExecStateUpdate(exec.GetID(), enum.FAILURE)
|
||||
return
|
||||
}
|
||||
fmt.Println("CompleteBuild")
|
||||
|
||||
argoFilePath, err := builder.CompleteBuild(exec.ExecutionsID)
|
||||
fmt.Println("CompleteBuild", err)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Error when completing the build of the workflow: " + err.Error())
|
||||
u.EmitExecStateUpdate(exec.GetID(), enum.FAILURE)
|
||||
return
|
||||
}
|
||||
|
||||
workflowName = getContainerName(argoFilePath)
|
||||
fmt.Println("getContainerName", workflowName, conf.GetConfig().KubeHost)
|
||||
|
||||
wf_logger := u.GetWFLogger(workflowName)
|
||||
wf_logger.Debug().Msg("Testing argo name")
|
||||
|
||||
if conf.GetConfig().KubeHost == "" {
|
||||
// Not in a k8s environment, get conf from parameters
|
||||
panic("can't exec with no kube for argo deployment")
|
||||
logger.Info().Msg("Executes outside of k8s")
|
||||
executeOutside(argoFilePath, builder.Workflow)
|
||||
} else {
|
||||
// Executed in a k8s environment
|
||||
logger.Info().Msg("Executes inside a k8s")
|
||||
// Wait until the scheduled start time if prep finished early.
|
||||
if st := conf.GetConfig().ScheduledTime; !st.IsZero() && time.Now().Before(st) {
|
||||
wait := time.Until(st)
|
||||
logger.Info().Msgf("Prep done early, waiting %s until scheduled start %s", wait.Round(time.Second), st.Format(time.RFC3339))
|
||||
u.EmitExecStateUpdate(exec.GetID(), enum.IN_PREPARATION)
|
||||
time.Sleep(wait)
|
||||
} else if st := conf.GetConfig().ScheduledTime; !st.IsZero() && time.Now().After(st) {
|
||||
logger.Warn().Msgf("Prep finished %s late vs scheduled start %s", time.Since(st).Round(time.Second), st.Format(time.RFC3339))
|
||||
}
|
||||
fmt.Println("EXEC")
|
||||
executeInside(exec.ExecutionsID, exec.GetID(), argoFilePath)
|
||||
// executeInside(exec.GetID(), "argo", argo_file_path, stepMax) // commenting to use conf.ExecutionID instead of exec.GetID()
|
||||
executeInside(conf.GetConfig().ExecutionID, exec.ExecutionsID, argoFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
// So far we only log the output from
|
||||
func executeInside(ns string, execID string, argo_file_path string) {
|
||||
func executeInside(execID string, ns string, argo_file_path string) {
|
||||
t, err := tools2.NewService(conf.GetConfig().Mode)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not create KubernetesTool : " + err.Error())
|
||||
logger.Error().Msg("Could not create KubernetesTool")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,19 +134,95 @@ func executeInside(ns string, execID string, argo_file_path string) {
|
||||
logger.Info().Msg(fmt.Sprint("Data :" + conf.GetConfig().KubeData))
|
||||
return
|
||||
} else {
|
||||
l.LogKubernetesArgo(name, execID, ns, ns, t, workflowName)
|
||||
watcher, err := t.GetArgoWatch(ns, workflowName)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not retrieve Watcher : " + err.Error())
|
||||
}
|
||||
|
||||
l.LogKubernetesArgo(name, ns, watcher)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not log workflow : " + err.Error())
|
||||
}
|
||||
|
||||
logger.Info().Msg("Finished, exiting...")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func loadConfig(parser *argparse.Parser) {
|
||||
func executeOutside(argo_file_path string, workflow workflow_builder.Workflow) {
|
||||
var stdoutSubmit, stderrSubmit io.ReadCloser
|
||||
var stdoutLogs, stderrLogs io.ReadCloser
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
|
||||
logger.Debug().Msg("executing :" + "argo submit --watch " + argo_file_path + " --serviceaccount sa-" + conf.GetConfig().ExecutionID + " -n " + conf.GetConfig().ExecutionID )
|
||||
|
||||
cmdSubmit := exec.Command("argo", "submit", "--watch", argo_file_path, "--serviceaccount", "sa-"+conf.GetConfig().ExecutionID, "-n", conf.GetConfig().ExecutionID)
|
||||
if stdoutSubmit, err = cmdSubmit.StdoutPipe(); err != nil {
|
||||
wf_logger.Error().Msg("Could not retrieve stdoutpipe " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cmdLogs := exec.Command("argo", "logs", "oc-monitor-"+workflowName, "-n", conf.GetConfig().ExecutionID, "--follow","--no-color")
|
||||
if stdoutLogs, err = cmdLogs.StdoutPipe(); err != nil {
|
||||
wf_logger.Error().Msg("Could not retrieve stdoutpipe for 'argo logs'" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var steps []string
|
||||
for _, template := range workflow.Spec.Templates {
|
||||
steps = append(steps, template.Name)
|
||||
}
|
||||
|
||||
go l.LogLocalWorkflow(workflowName, stdoutSubmit, &wg)
|
||||
go l.LogLocalPod(workflowName, stdoutLogs, steps, &wg)
|
||||
|
||||
logger.Info().Msg("Starting argo submit")
|
||||
if err := cmdSubmit.Start(); err != nil {
|
||||
wf_logger.Error().Msg("Could not start argo submit")
|
||||
wf_logger.Error().Msg(err.Error() + bufio.NewScanner(stderrSubmit).Text())
|
||||
updateStatus("fatal", "")
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
logger.Info().Msg("Running argo logs")
|
||||
if err := cmdLogs.Run(); err != nil {
|
||||
wf_logger.Error().Msg("Could not run '" + strings.Join(cmdLogs.Args, " ") + "'")
|
||||
|
||||
wf_logger.Fatal().Msg(err.Error() + bufio.NewScanner(stderrLogs).Text())
|
||||
|
||||
}
|
||||
|
||||
logger.Info().Msg("Waiting argo submit")
|
||||
if err := cmdSubmit.Wait(); err != nil {
|
||||
wf_logger.Error().Msg("Could not execute argo submit")
|
||||
wf_logger.Error().Msg(err.Error() + bufio.NewScanner(stderrSubmit).Text())
|
||||
updateStatus("fatal", "")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
|
||||
func loadConfig(is_k8s bool, parser *argparse.Parser) {
|
||||
var o *onion.Onion
|
||||
o = initOnion(o)
|
||||
setConf(is_k8s, o, parser)
|
||||
|
||||
// if !IsValidUUID(conf.GetConfig().ExecutionID) {
|
||||
// logger.Fatal().Msg("Provided ID is not an UUID")
|
||||
// }
|
||||
}
|
||||
|
||||
func setConf(is_k8s bool, o *onion.Onion, parser *argparse.Parser) {
|
||||
url := parser.String("u", "url", &argparse.Options{Required: true, Default: "http://127.0.0.1:3100", Help: "Url to the Loki database logs will be sent to"})
|
||||
mode := parser.String("M", "mode", &argparse.Options{Required: false, Default: "", Help: "Mode of the execution"})
|
||||
ocNamespace := parser.String("n", "namespace", &argparse.Options{Required: false, Default: "opencloud", Help: "Kubernetes namespace where OpenCloud components (NATS) run"})
|
||||
execution := parser.String("e", "execution", &argparse.Options{Required: true, Help: "Execution ID of the workflow to request from oc-catalog API"})
|
||||
peer := parser.String("p", "peer", &argparse.Options{Required: false, Default: "", Help: "Peer ID of the workflow to request from oc-catalog API"})
|
||||
mongo := parser.String("m", "mongo", &argparse.Options{Required: true, Default: "mongodb://127.0.0.1:27017", Help: "URL to reach the MongoDB"})
|
||||
db := parser.String("d", "database", &argparse.Options{Required: true, Default: "DC_myDC", Help: "Name of the database to query in MongoDB"})
|
||||
timeout := parser.Int("t", "timeout", &argparse.Options{Required: false, Default: -1, Help: "Timeout for the execution of the workflow"})
|
||||
scheduledUnix := parser.Int("s", "scheduled-time", &argparse.Options{Required: false, Default: 0, Help: "Unix timestamp of the scheduled start; oc-monitord will wait until this time before submitting the Argo workflow"})
|
||||
|
||||
ca := parser.String("c", "ca", &argparse.Options{Required: false, Default: "", Help: "CA file for the Kubernetes cluster"})
|
||||
cert := parser.String("C", "cert", &argparse.Options{Required: false, Default: "", Help: "Cert file for the Kubernetes cluster"})
|
||||
@@ -155,7 +231,6 @@ func loadConfig(parser *argparse.Parser) {
|
||||
host := parser.String("H", "host", &argparse.Options{Required: false, Default: "", Help: "Host for the Kubernetes cluster"})
|
||||
port := parser.String("P", "port", &argparse.Options{Required: false, Default: "6443", Help: "Port for the Kubernetes cluster"})
|
||||
|
||||
natsUrl := parser.String("N", "nats", &argparse.Options{Required: false, Default: "", Help: "Nats URL"})
|
||||
// argoHost := parser.String("h", "argoHost", &argparse.Options{Required: false, Default: "", Help: "Host where Argo is running from"}) // can't use -h because its reserved to help
|
||||
|
||||
err := parser.Parse(os.Args)
|
||||
@@ -164,15 +239,14 @@ func loadConfig(parser *argparse.Parser) {
|
||||
os.Exit(1)
|
||||
}
|
||||
conf.GetConfig().Logs = "debug"
|
||||
conf.GetConfig().LokiURL = *url
|
||||
conf.GetConfig().MongoURL = *mongo
|
||||
conf.GetConfig().Database = *db
|
||||
conf.GetConfig().Timeout = *timeout
|
||||
conf.GetConfig().Mode = *mode
|
||||
conf.GetConfig().ExecutionID = *execution
|
||||
conf.GetConfig().PeerID = *peer
|
||||
conf.GetConfig().OCNamespace = *ocNamespace
|
||||
if *scheduledUnix > 0 {
|
||||
conf.GetConfig().ScheduledTime = time.Unix(int64(*scheduledUnix), 0)
|
||||
}
|
||||
conf.GetConfig().NatsUrl = *natsUrl
|
||||
|
||||
conf.GetConfig().KubeHost = *host
|
||||
conf.GetConfig().KubePort = *port
|
||||
|
||||
@@ -192,6 +266,34 @@ func loadConfig(parser *argparse.Parser) {
|
||||
}
|
||||
}
|
||||
|
||||
func initOnion(o *onion.Onion) *onion.Onion {
|
||||
logger = logs.CreateLogger("oc-monitord")
|
||||
configFile := ""
|
||||
|
||||
l3 := onion.NewEnvLayerPrefix("_", "OCMONITORD")
|
||||
l2, err := onion.NewFileLayer(defaultConfigFile, nil)
|
||||
if err == nil {
|
||||
logger.Info().Msg("Config file found : " + defaultConfigFile)
|
||||
configFile = defaultConfigFile
|
||||
}
|
||||
l1, err := onion.NewFileLayer(localConfigFile, nil)
|
||||
if err == nil {
|
||||
logger.Info().Msg("Local config file found " + localConfigFile + ", overriding default file")
|
||||
configFile = localConfigFile
|
||||
}
|
||||
if configFile == "" {
|
||||
logger.Info().Msg("No config file found, using env")
|
||||
o = onion.New(l3)
|
||||
} else if l1 == nil && l2 == nil {
|
||||
o = onion.New(l1, l2, l3)
|
||||
} else if l1 == nil {
|
||||
o = onion.New(l2, l3)
|
||||
} else if l2 == nil {
|
||||
o = onion.New(l1, l3)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func IsValidUUID(u string) bool {
|
||||
_, err := uuid.Parse(u)
|
||||
return err == nil
|
||||
@@ -206,6 +308,7 @@ func getContainerName(argo_file string) string {
|
||||
return container_name
|
||||
}
|
||||
|
||||
|
||||
func updateStatus(status string, log string) {
|
||||
exec_id := conf.GetConfig().ExecutionID
|
||||
|
||||
@@ -213,7 +316,7 @@ func updateStatus(status string, log string) {
|
||||
wf_exec.ArgoStatusToState(status)
|
||||
exec, _, err := workflow_execution.NewAccessor(&tools.APIRequest{
|
||||
PeerID: conf.GetConfig().PeerID,
|
||||
}).UpdateOne(wf_exec.Serialize(wf_exec), exec_id)
|
||||
}).UpdateOne(wf_exec, exec_id)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not update status for workflow execution " + exec_id + err.Error())
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# Goal
|
||||
|
||||
We want to be able to instantiate a service that allows to store file located on a `processing` pod onto it.
|
||||
|
||||
We have already tested it with a static `Argo` yaml file, a MinIO running on the same kubernetes node, the minio service is reached because it is the only associated to the `serviceAccount`.
|
||||
|
||||
We have established three otpions that need to be available to the user for the feature to be implemented:
|
||||
|
||||
- Use a MinIO running constantly on the node that executes the argo workflow
|
||||
- Use a MinIO
|
||||
- A MinIO is instanciated when a new workflow is launched
|
||||
|
||||
# Requirements
|
||||
|
||||
- Helm : `https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3`
|
||||
- Helm GO client : `$ go get github.com/mittwald/go-helm-client`
|
||||
- MinIO chart : `https://charts.min.io/`
|
||||
|
||||
|
||||
# Ressources
|
||||
|
||||
We need to create several ressources in order for the pods to communicate with the MinIO
|
||||
|
||||
## MinIO Auth Secrets
|
||||
|
||||
## Bucket ConfigMap
|
||||
|
||||
With the name `artifact-repositories` this configMap will be used by default. It contains the URL to the MinIO server and the key to the authentication data held in a `secret` ressource.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# If you want to use this config map by default, name it "artifact-repositories".
|
||||
name: artifact-repositories
|
||||
# annotations:
|
||||
# # v3.0 and after - if you want to use a specific key, put that key into this annotation.
|
||||
# workflows.argoproj.io/default-artifact-repository: oc-s3-artifact-repository
|
||||
data:
|
||||
oc-s3-artifact-repository: |
|
||||
s3:
|
||||
bucket: oc-bucket
|
||||
endpoint: [ retrieve cluster with kubectl get service argo-artifacts -o jsonpath="{.spec.clusterIP}" ]:9000
|
||||
insecure: true
|
||||
accessKeySecret:
|
||||
name: argo-artifact-secret
|
||||
key: access-key
|
||||
secretKeySecret:
|
||||
name: argo-artifact-secret
|
||||
key: secret-key
|
||||
|
||||
```
|
||||
|
||||
|
||||
# Code modifications
|
||||
|
||||
Rajouter un attribut "isDataLink"
|
||||
- true/false
|
||||
|
||||
Rajouter un attribut DataPath ou un truc comme ca
|
||||
|
||||
- liste de map[string]string permet de n'avoir qu'une copie par fichier)
|
||||
- éditable uniquement a travers la méthode addDataPath
|
||||
- clé : path du fichier / value : nom de la copie dans minio
|
||||
|
||||
===> on a besoin du meme attribut pour Processing -> Data et Data -> Processing
|
||||
+24
-209
@@ -1,17 +1,10 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources/native_tools"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type Parameter struct {
|
||||
@@ -21,7 +14,6 @@ type Parameter struct {
|
||||
|
||||
type Container struct {
|
||||
Image string `yaml:"image"`
|
||||
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
|
||||
Command []string `yaml:"command,omitempty,flow"`
|
||||
Args []string `yaml:"args,omitempty,flow"`
|
||||
VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"`
|
||||
@@ -48,9 +40,7 @@ func (c *Container) AddVolumeMount(volumeMount VolumeMount, volumes []VolumeMoun
|
||||
type VolumeMount struct {
|
||||
Name string `yaml:"name"`
|
||||
MountPath string `yaml:"mountPath"`
|
||||
ReadOnly bool `yaml:"readOnly,omitempty"`
|
||||
Storage *resources.StorageResource `yaml:"-"`
|
||||
IsReparted bool `yaml:"-"`
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
@@ -81,18 +71,14 @@ type Key struct {
|
||||
Bucket string `yaml:"bucket"`
|
||||
EndPoint string `yaml:"endpoint"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
AccessKeySecret *Secret `yaml:"accessKeySecret"`
|
||||
SecretKeySecret *Secret `yaml:"secretKeySecret"`
|
||||
AccessKeySecret *Secret `yaml accessKeySecret`
|
||||
SecretKeySecret *Secret `yaml secretKeySecret`
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
Name string `yaml:"name"`
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
type ArtifactRepositoryRef struct {
|
||||
ConfigMap string `yaml:"configMap,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
S3 *Key `yaml:"s3,omitempty"`
|
||||
}
|
||||
|
||||
type InOut struct {
|
||||
@@ -108,230 +94,59 @@ type Template struct {
|
||||
Dag *Dag `yaml:"dag,omitempty"`
|
||||
Metadata TemplateMetadata `yaml:"metadata,omitempty"`
|
||||
Resource ServiceResource `yaml:"resource,omitempty"`
|
||||
NodeSelector map[string]string `yaml:"nodeSelector,omitempty"`
|
||||
}
|
||||
|
||||
func (template *Template) CreateEventContainer(execution *workflow_execution.WorkflowExecution, id string, wf *workflow.Workflow, nt *resources.NativeTool, dag *Dag, natsURL string) {
|
||||
container := Container{Image: "natsio/nats-box", ImagePullPolicy: "IfNotPresent"}
|
||||
container.Command = []string{"sh", "-c"} // all is bash
|
||||
|
||||
var event native_tools.WorkflowEventParams
|
||||
b, err := json.Marshal(nt.Params)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, &event)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if event.WorkflowResourceID != "" {
|
||||
event.Payload = event.Input
|
||||
event.Input = ""
|
||||
if b, err := json.Marshal(event); err == nil {
|
||||
payload, err := json.Marshal(&tools.NATSResponse{
|
||||
FromApp: "oc-monitord",
|
||||
Datatype: tools.NATIVE_TOOL,
|
||||
Method: int(tools.WORKFLOW_EVENT),
|
||||
Payload: b,
|
||||
})
|
||||
if err == nil {
|
||||
cmd := exec.Command(
|
||||
"nats",
|
||||
"pub",
|
||||
"--server", natsURL,
|
||||
tools.WORKFLOW_EVENT.GenerateKey(),
|
||||
string(payload),
|
||||
)
|
||||
if len(wf.Args[id]) > 0 {
|
||||
for _, args := range wf.Args[id] {
|
||||
container.Args = append(container.Args, args)
|
||||
}
|
||||
} else {
|
||||
for _, args := range cmd.Args {
|
||||
container.Args = append(container.Args, args)
|
||||
}
|
||||
}
|
||||
container.Args = []string{
|
||||
strings.Join(container.Args, " "),
|
||||
}
|
||||
template.Container = container
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (template *Template) CreateContainer(exec *workflow_execution.WorkflowExecution, wf *workflow.Workflow, itemID string, processing *resources.ProcessingResource, dag *Dag) {
|
||||
index := 0
|
||||
if d, ok := exec.SelectedInstances[processing.GetID()]; ok {
|
||||
index = d
|
||||
}
|
||||
instance := processing.GetSelectedInstance(&index)
|
||||
func (template *Template) CreateContainer(processing *resources.ProcessingResource, dag *Dag) {
|
||||
instance := processing.GetSelectedInstance()
|
||||
if instance == nil {
|
||||
return
|
||||
}
|
||||
inst := instance.(*resources.ProcessingInstance)
|
||||
container := Container{
|
||||
Image: inst.Access.Container.Image,
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
}
|
||||
container := Container{Image: inst.Access.Container.Image}
|
||||
if container.Image == "" {
|
||||
return
|
||||
}
|
||||
container.Command = []string{"sh", "-c"} // all is bash
|
||||
for _, v := range processing.Env {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
for _, v := range inst.Env {
|
||||
template.Inputs.Parameters = append(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range wf.Env[itemID] {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
for _, v := range inst.Inputs {
|
||||
template.Inputs.Parameters = append(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range processing.Inputs {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range wf.Inputs[itemID] {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range processing.Outputs {
|
||||
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
})
|
||||
}
|
||||
for _, v := range wf.Outputs[itemID] {
|
||||
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
})
|
||||
for _, v := range inst.Inputs {
|
||||
template.Outputs.Parameters = append(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
cmd := strings.ReplaceAll(inst.Access.Container.Command, container.Image, "")
|
||||
|
||||
for _, a := range strings.Split(cmd, " ") {
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, inst.Env))
|
||||
}
|
||||
if len(wf.Args[itemID]) > 0 {
|
||||
for _, a := range wf.Args[itemID] {
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
|
||||
}
|
||||
} else {
|
||||
for _, a := range strings.Split(inst.Access.Container.Args, " ") {
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, inst.Env))
|
||||
}
|
||||
}
|
||||
|
||||
container.Args = []string{strings.Join(container.Args, " ")}
|
||||
|
||||
template.Container = container
|
||||
}
|
||||
|
||||
// CreateServiceContainer crée le container Argo pour un ServiceResource.
|
||||
// Pour HOSTED, le container appelle le service distant (endpoint connu) ;
|
||||
// pour DEPLOYMENT, le container EST le service à déployer.
|
||||
// La logique de paramètres est identique à CreateContainer.
|
||||
func (template *Template) CreateServiceContainer(exec *workflow_execution.WorkflowExecution, wf *workflow.Workflow, itemID string, service *resources.ServiceResource, dag *Dag) {
|
||||
index := 0
|
||||
if d, ok := exec.SelectedInstances[service.GetID()]; ok {
|
||||
index = d
|
||||
}
|
||||
instance := service.GetSelectedInstance(&index)
|
||||
if instance == nil {
|
||||
return
|
||||
}
|
||||
inst := instance.(*resources.ServiceInstance)
|
||||
if inst.Access == nil || inst.Access.Container == nil || inst.Access.Container.Image == "" {
|
||||
return
|
||||
}
|
||||
container := Container{
|
||||
Image: inst.Access.Container.Image,
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
}
|
||||
container.Command = []string{"sh", "-c"}
|
||||
for _, v := range service.Env {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range wf.Env[itemID] {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range service.Inputs {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range wf.Inputs[itemID] {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range service.Outputs {
|
||||
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
})
|
||||
}
|
||||
for _, v := range wf.Outputs[itemID] {
|
||||
template.Outputs.Parameters = AppendParamIfAbsent(template.Outputs.Parameters, Parameter{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
})
|
||||
}
|
||||
cmd := strings.ReplaceAll(inst.Access.Container.Command, container.Image, "")
|
||||
for _, a := range strings.Split(cmd, " ") {
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
|
||||
}
|
||||
if len(wf.Args[itemID]) > 0 {
|
||||
for _, a := range wf.Args[itemID] {
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
|
||||
}
|
||||
} else {
|
||||
for _, a := range strings.Split(inst.Access.Container.Args, " ") {
|
||||
container.Args = append(container.Args, template.ReplacePerEnv(a, template.Inputs.Parameters))
|
||||
}
|
||||
}
|
||||
container.Args = []string{strings.Join(container.Args, " ")}
|
||||
template.Container = container
|
||||
}
|
||||
|
||||
// AppendParamIfAbsent ajoute p à params uniquement si aucun paramètre
|
||||
// portant le même nom n'est déjà présent. Évite les doublons quand les
|
||||
// variables sont définies à la fois sur le ProcessingResource et sur le
|
||||
// Workflow (overrides).
|
||||
func AppendParamIfAbsent(params []Parameter, p Parameter) []Parameter {
|
||||
for _, existing := range params {
|
||||
if existing.Name == p.Name {
|
||||
return params
|
||||
}
|
||||
}
|
||||
return append(params, p)
|
||||
}
|
||||
|
||||
func (template *Template) ReplacePerEnv(arg string, envs []Parameter) string {
|
||||
// Tri par longueur décroissante : les noms longs sont remplacés en premier
|
||||
// pour éviter que $FOO matche à l'intérieur de $FOOBAR.
|
||||
sorted := make([]Parameter, len(envs))
|
||||
copy(sorted, envs)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return len(sorted[i].Name) > len(sorted[j].Name)
|
||||
})
|
||||
for _, v := range sorted {
|
||||
needle := "$" + v.Name
|
||||
if v.Name != "" && strings.Contains(arg, needle) {
|
||||
func (template *Template) ReplacePerEnv(arg string, envs []models.Param) string {
|
||||
for _, v := range envs {
|
||||
if strings.Contains(arg, v.Name) {
|
||||
value := "{{ inputs.parameters." + v.Name + " }}"
|
||||
arg = strings.ReplaceAll(arg, needle, value)
|
||||
arg = strings.ReplaceAll(arg, v.Name, value)
|
||||
arg = strings.ReplaceAll(arg, "$"+v.Name, value)
|
||||
arg = strings.ReplaceAll(arg, "$", "")
|
||||
}
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
// AddAdmiraltyAnnotations marque le template pour qu'Admiralty route le pod
|
||||
// vers le cluster virtuel correspondant au peerId.
|
||||
//
|
||||
// - elect: "" déclenche le webhook Admiralty sur le pod créé par Argo.
|
||||
// - nodeSelector cible le virtual node Admiralty dont le label
|
||||
// multicluster.admiralty.io/cluster-name vaut peerId,
|
||||
// ce qui contraint le scheduling au cluster distant.
|
||||
// Add the metadata that allow Admiralty to pick up an Argo Workflow that needs to be reparted
|
||||
// The value of "clustername" is the peerId, which must be replaced by the node name's for this specific execution
|
||||
func (t *Template) AddAdmiraltyAnnotations(peerId string){
|
||||
if t.Metadata.Annotations == nil {
|
||||
t.Metadata.Annotations = make(map[string]string)
|
||||
}
|
||||
t.Metadata.Annotations["multicluster.admiralty.io/elect"] = ""
|
||||
if t.NodeSelector == nil {
|
||||
t.NodeSelector = make(map[string]string)
|
||||
}
|
||||
t.NodeSelector["multicluster.admiralty.io/cluster-name"] = peerId
|
||||
t.Metadata.Annotations["multicluster.admiralty.io/clustername"] = peerId
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package models
|
||||
type VolumeClaimTemplate struct {
|
||||
Metadata struct {
|
||||
Name string `yaml:"name"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty"`
|
||||
} `yaml:"metadata"`
|
||||
Spec VolumeSpec `yaml:"spec"`
|
||||
}
|
||||
@@ -16,27 +15,3 @@ type VolumeSpec struct {
|
||||
} `yaml:"requests"`
|
||||
} `yaml:"resources"`
|
||||
}
|
||||
|
||||
// PVCRef references a pre-provisioned PersistentVolumeClaim by name.
|
||||
type PVCRef struct {
|
||||
ClaimName string `yaml:"claimName"`
|
||||
}
|
||||
|
||||
// SecretRef references a K8s Secret to mount as a volume.
|
||||
type SecretRef struct {
|
||||
SecretName string `yaml:"secretName"`
|
||||
}
|
||||
|
||||
// EmptyDirRef declares an emptyDir volume. Set Medium to "Memory" for /dev/shm-style RAM backing.
|
||||
type EmptyDirRef struct {
|
||||
Medium string `yaml:"medium,omitempty"`
|
||||
}
|
||||
|
||||
// ExistingVolume represents any volume mounted into an Argo workflow spec.
|
||||
// Exactly one of PersistentVolumeClaim, Secret, or EmptyDir should be non-nil.
|
||||
type ExistingVolume struct {
|
||||
Name string `yaml:"name"`
|
||||
PersistentVolumeClaim *PVCRef `yaml:"persistentVolumeClaim,omitempty"`
|
||||
Secret *SecretRef `yaml:"secret,omitempty"`
|
||||
EmptyDir *EmptyDirRef `yaml:"emptyDir,omitempty"`
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
-542
@@ -1,542 +0,0 @@
|
||||
# Source tierce dans ProcessingAccess
|
||||
|
||||
## Contexte
|
||||
|
||||
`ProcessingResourceAccess` (oc-lib) expose trois champs :
|
||||
|
||||
```go
|
||||
type ProcessingResourceAccess struct {
|
||||
Source string // URL ou identifiant de la source tierce
|
||||
IsReachable bool // true = accessible publiquement, false = chez un peer privé
|
||||
Container *models.Container // nil si on passe par Source
|
||||
}
|
||||
```
|
||||
|
||||
Quand `Container` est nil et `Source` non vide, le workflow builder doit gérer l'accès à la source tierce selon la valeur de `IsReachable`.
|
||||
|
||||
---
|
||||
|
||||
## Cas 1 — `isReachable = true` (source publique)
|
||||
|
||||
La source est accessible via une URL publique (HTTP/S3 public, etc.).
|
||||
|
||||
**Comportement dans le builder :**
|
||||
|
||||
- Insérer une step Argo **avant** la step de processing courante
|
||||
- Cette step effectue un `curl` de la source vers le storage lié au processing
|
||||
- Si aucun storage lié → erreur immédiate
|
||||
- La commande du processing devient `<storage_mount_path>/<nom_du_fichier>`
|
||||
|
||||
```
|
||||
[step précédente] → [step curl download] → [step processing]
|
||||
↓
|
||||
storage lié (S3 ou local)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cas 2 — `isReachable = false` (source privée chez le peer dépositaire)
|
||||
|
||||
La source (`cmd.bin`) se trouve sur le réseau privé de PeerA, inaccessible de l'extérieur.
|
||||
L'exécution a lieu **sur B**, avec des protections pour limiter l'extraction du binaire.
|
||||
|
||||
### Principe général
|
||||
|
||||
PeerA expose temporairement le binaire via un **Minio partagé à usage unique**,
|
||||
avec des credentials éphémères. B exécute le binaire en mémoire sans le persister sur disque.
|
||||
|
||||
---
|
||||
|
||||
### Protocole étape par étape
|
||||
|
||||
#### 1. Demande d'accès via NATS (à la construction du template)
|
||||
|
||||
`oc-monitord` (B) envoie un message NATS à `oc-discovery` :
|
||||
|
||||
```
|
||||
Subject : PB_SOURCE_REQUEST
|
||||
Payload : {
|
||||
executions_id : string,
|
||||
source_key : string, // clé opaque — B ne connaît pas le path réel
|
||||
peer_id_src : string, // PeerA
|
||||
peer_id_dst : string, // PeerB
|
||||
ttl : duration, // durée du booking
|
||||
coupling : { // résumé du couplage pour vérification AE côté A
|
||||
compute_peer : string,
|
||||
storage_peers : []string,
|
||||
data_peers : []string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`oc-discovery` transmet la demande à A et attend sa réponse.
|
||||
|
||||
#### 2. A vérifie les AE et génère une pre-signed URL à usage unique
|
||||
|
||||
A reçoit la demande, vérifie :
|
||||
1. Que `source_key` correspond bien à une source qu'il détient
|
||||
2. Que le couplage décrit respecte ses **Autorisations d'Exploitation** (voir section dédiée)
|
||||
|
||||
Si les deux conditions sont remplies, A monte le fichier réel dans son Minio interne
|
||||
et génère une pre-signed URL Minio avec :
|
||||
|
||||
- `max-download-count = 1` → révoquée après le premier GET
|
||||
- TTL = durée du booking
|
||||
|
||||
Le path réel n'est jamais transmis à B — seule l'URL pre-signed est retournée.
|
||||
Si une AE est violée → refus + événement `AE_VIOLATION` (voir section AE).
|
||||
|
||||
#### 3. `oc-monitord` crée un Kubernetes Secret éphémère
|
||||
|
||||
La pre-signed URL revient à B via la réponse NATS.
|
||||
`oc-monitord` crée un Secret Kubernetes dans le namespace Argo **juste avant** la soumission
|
||||
du workflow, avec une `ownerReference` sur le workflow (suppression automatique à la fin) :
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: source-<executions_id>
|
||||
ownerReferences:
|
||||
- kind: Workflow
|
||||
name: <workflow_name>
|
||||
data:
|
||||
url: <base64(presigned_url)>
|
||||
```
|
||||
|
||||
L'URL n'apparaît **jamais** dans le spec du workflow (pas de trace dans `kubectl get workflow -o yaml`).
|
||||
|
||||
#### 4. Génération de la step Argo
|
||||
|
||||
Le builder injecte un wrapper script comme commande de la step processing :
|
||||
|
||||
```sh
|
||||
curl -s "$PRESIGNED_URL" -o /dev/shm/.exec
|
||||
chmod +x /dev/shm/.exec
|
||||
/dev/shm/.exec "$@" &
|
||||
PID=$!
|
||||
rm -f /dev/shm/.exec
|
||||
wait $PID
|
||||
```
|
||||
|
||||
La variable `PRESIGNED_URL` est injectée via `env.valueFrom.secretKeyRef` → jamais en clair dans le spec.
|
||||
|
||||
Le volume éphémère est déclaré en mémoire :
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- name: ephemeral-bin
|
||||
emptyDir:
|
||||
medium: Memory # tmpfs — rien sur disque, détruit avec le pod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Protections et limites
|
||||
|
||||
| Vecteur d'attaque | Protection | Efficacité |
|
||||
|---|---|---|
|
||||
| Re-téléchargement depuis l'URL | Usage unique — URL révoquée après le 1er GET | Forte |
|
||||
| Copie du fichier depuis le pod (`kubectl exec cp`) | `rm` immédiat après exec — fichier absent du FS | Forte (non-root) |
|
||||
| Extraction depuis le disque du node | `medium: Memory` — rien écrit sur disque | Forte |
|
||||
| Lecture de l'URL dans le spec Argo | Kubernetes Secret + `valueFrom.secretKeyRef` | Forte |
|
||||
| Identité / path réel de la source | Clé opaque — B ne connaît que la clé, pas le path | Forte |
|
||||
| `/proc/PID/exe` (root sur le node B) | **Aucune** — voir ci-dessous | Nulle |
|
||||
| `/proc/PID/mem` + `ptrace` / `gdb` (root) | **Aucune** | Nulle |
|
||||
| `criu dump` (snapshot mémoire container) | **Aucune** | Nulle |
|
||||
|
||||
### Angle mort : `/proc/PID/exe`
|
||||
|
||||
Le `rm` sur `/dev/shm/.exec` supprime l'entrée dans le répertoire mais **pas l'inode** — le kernel
|
||||
maintient une référence ouverte tant que le process tourne. Un admin root sur le node de B peut
|
||||
récupérer le binaire intact en une commande pendant l'exécution :
|
||||
|
||||
```sh
|
||||
cp /proc/$(pgrep .exec)/exe /tmp/recovered_binary
|
||||
```
|
||||
|
||||
De même, `/proc/PID/mem` combiné à `/proc/PID/maps` permet de dumper les segments text/data,
|
||||
et `ptrace` / `gdb --pid` d'attacher un debugger. Ces vecteurs sont **incontournables** par
|
||||
des moyens purement logiciels si B est root sur son propre cluster.
|
||||
|
||||
Les protections en place couvrent donc :
|
||||
- Les utilisateurs non-root / autres pods
|
||||
- La persistence accidentelle (disque, logs, artefacts)
|
||||
- Le re-téléchargement après exécution
|
||||
|
||||
Elles **ne protègent pas** contre un opérateur de B activement malveillant avec accès root.
|
||||
|
||||
---
|
||||
|
||||
### Options si le niveau de confiance en B est faible
|
||||
|
||||
#### ~~Option 1 — Exécution chez A~~ *(écarté)*
|
||||
|
||||
Dispatcher la step Argo sur l'infrastructure de A via Admiralty / virtual kubelet.
|
||||
**Écarté** : le service couple un datacenter — le couplage physique d'infrastructure est
|
||||
hors scope dans l'architecture actuelle.
|
||||
|
||||
#### Option 2 — Confidential Computing (Intel TDX / SGX)
|
||||
|
||||
La mémoire du pod est chiffrée au niveau hardware. Inaccessible au kernel et à root.
|
||||
Nécessite du hardware compatible côté B — non déployable universellement.
|
||||
|
||||
#### Option 3 — Binaire chiffré + clé en mémoire à usage unique
|
||||
|
||||
A transmet un binaire chiffré (AES-GCM). Un sidecar sécurisé injecte la clé de déchiffrement
|
||||
en mémoire uniquement, via un channel éphémère (ex. socket Unix dans le pod). Le loader déchiffre
|
||||
en RAM et exécute sans jamais écrire le binaire en clair.
|
||||
|
||||
- `/proc/PID/exe` → blob chiffré inutilisable ✅
|
||||
- `/proc/PID/mem` → expose le binaire déchiffré à l'exécution ❌ (angle mort résiduel)
|
||||
|
||||
Complexe à implémenter, ralentit le démarrage, mais ferme le vecteur `exe`.
|
||||
|
||||
#### Option 4 — OCI Image Encryption (ocicrypt) pour les images de conteneur
|
||||
|
||||
Complémentaire de l'Option 3 pour les images Docker (pas les binaires bruts).
|
||||
Les layers de l'image sont chiffrés dans le registry. La clé est délivrée par un KMS
|
||||
uniquement au moment du pull, uniquement à un kubelet autorisé.
|
||||
Ce que root voit sur le disque du node = des blobs chiffrés inutilisables.
|
||||
Supporté par containerd avec plugin.
|
||||
|
||||
> **Pourquoi c'est nécessaire :** quand containerd pull une image, les layers sont stockés
|
||||
> dans `/var/lib/containerd/` sur le node. Un root peut les exporter intégralement :
|
||||
> `ctr images export image.tar registry/image:tag`. Les credentials éphémères limitent
|
||||
> le re-pull mais pas la copie de ce qui est déjà sur le node.
|
||||
|
||||
---
|
||||
|
||||
### Synthèse — choix selon le niveau de confiance en B
|
||||
|
||||
| Niveau de confiance en B | Solution recommandée |
|
||||
|---|---|
|
||||
| Peer de confiance (contrat, audit) | Protections best-effort actuelles + AE |
|
||||
| Peer partiellement de confiance | Option 3 (binaire chiffré) + AE |
|
||||
| Peer non de confiance | Option 2 (Confidential Computing) + AE |
|
||||
|
||||
### Architecture de protection par couches
|
||||
|
||||
```
|
||||
Couche 1 — Protections techniques → limitent l'extraction physique (tmpfs, URL unique, Secret K8s)
|
||||
Couche 2 — Chiffrement (Options 3/4) → limitent l'utilité d'une extraction réussie
|
||||
Couche 3 — Autorisations d'Exploitation (AE) → limitent l'usage contextuel (couplage, peers autorisés, quota)
|
||||
Couche 4 — Licences / Consentements → cadre légal et traçabilité opposable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Autorisations d'Exploitation (AE)
|
||||
|
||||
### Concept
|
||||
|
||||
Au-delà de protéger l'accès à une ressource, A contrôle **dans quel contexte** sa ressource
|
||||
peut être couplée dans un workflow. L'AE répond à la question :
|
||||
|
||||
> "Mon Processing P peut être utilisé chez B, **seulement** avec le Storage S de C
|
||||
> et le Compute K de B. Toute autre combinaison est refusée."
|
||||
|
||||
On ne peut pas coupler dans un workflow une Data + un Storage d'un pair donné,
|
||||
ou un Processing avec un Compute d'un pair donné, sans que le détenteur de ces ressources
|
||||
ait explicitement accordé cette association.
|
||||
|
||||
### Modèle de données (oc-lib)
|
||||
|
||||
```go
|
||||
type ExploitationAuthorization struct {
|
||||
ID string
|
||||
GrantorPeer string // A — celui qui autorise
|
||||
GranteePeer string // B — celui qui reçoit le droit
|
||||
Resource ResourceRef // la ressource concernée (Processing, Data, Storage...)
|
||||
|
||||
Conditions ExploitAuthConditions
|
||||
License *ResourceLicense // lien avec le modèle licence
|
||||
}
|
||||
|
||||
type ExploitAuthConditions struct {
|
||||
AllowedComputePeers []string // nil = tout peer autorisé
|
||||
AllowedStoragePeers []string
|
||||
AllowedDataPeers []string
|
||||
AllowedProcessingPeers []string
|
||||
|
||||
MaxExecutions int // -1 = illimité
|
||||
ExpiresAt *time.Time
|
||||
ConsentRequired bool
|
||||
}
|
||||
```
|
||||
|
||||
### Stockage des AE — oc-catalog (copie distribuée)
|
||||
|
||||
Les AE sont publiées dans **oc-catalog**, dont chaque peer détient une **copie locale synchronisée**.
|
||||
|
||||
**Rationale :** même si un peer B tente de tricher en construisant un workflow non autorisé,
|
||||
A dispose de sa propre copie des règles et peut analyser le couplage reçu dans le payload
|
||||
`PB_SOURCE_REQUEST` au moment du booking, puis **refuser** si une AE est violée.
|
||||
|
||||
Ce mécanisme est auto-défensif : A ne dépend pas de la bonne foi de B pour appliquer ses règles.
|
||||
|
||||
### Violation d'AE — acte critique et score de réputation
|
||||
|
||||
Tenter de contourner une AE est un **acte critique** qui est :
|
||||
|
||||
1. Détecté par A au moment du booking (vérification indépendante côté A)
|
||||
2. Enregistré dans oc-catalog via un événement `AE_VIOLATION`
|
||||
3. Sanctionné par une **dégradation sérieuse du score de réputation** de B
|
||||
|
||||
Un peer dont le score s'effondre perd progressivement la possibilité de booker
|
||||
des ressources chez d'autres peers — c'est la dissuasion réseau.
|
||||
|
||||
```
|
||||
Subject : AE_VIOLATION
|
||||
Payload : {
|
||||
violator_peer_id : string,
|
||||
grantor_peer_id : string,
|
||||
resource_id : string,
|
||||
workflow_id : string,
|
||||
timestamp : time,
|
||||
attempted_coupling : { // couplage tenté vs. AE en vigueur
|
||||
compute_peer : string,
|
||||
storage_peers : []string,
|
||||
data_peers : []string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vérification dans le workflow builder (côté B)
|
||||
|
||||
Au moment de la construction du template, avant soumission Argo, B vérifie
|
||||
en local (sur sa copie des AE) la légitimité du couplage :
|
||||
|
||||
```go
|
||||
for _, res := range workflow.Resources {
|
||||
if res.PeerID != localPeerID {
|
||||
ae, err := catalog.GetExploitationAuthorization(res.PeerID, localPeerID, res.ID)
|
||||
if err != nil || !ae.AllowsCoupling(workflow.ComputePeer, workflow.StoragePeers, workflow.DataPeers) {
|
||||
return nil, ErrUnauthorizedCoupling{Resource: res.ID, Peer: res.PeerID}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
La vérification côté B est une première barrière ; la vérification côté A au booking
|
||||
est la barrière souveraine (celle que B ne peut pas contourner).
|
||||
|
||||
### Ce que les AE résolvent vs. les protections techniques
|
||||
|
||||
| Vecteur | Protection technique | AE |
|
||||
|---|---|---|
|
||||
| Copie du binaire | Partielle (loader chiffré, tmpfs) | ✗ hors scope |
|
||||
| Ré-exécution dans un workflow non autorisé | ✗ aucune | ✅ couplage rejeté + score dégradé |
|
||||
| Exfiltration vers un storage non autorisé | ✗ aucune | ✅ storage peer non listé → rejet |
|
||||
| Usage du Processing avec une Data non autorisée | ✗ aucune | ✅ data peer non listé → rejet |
|
||||
| Traçabilité légale | Partielle (logs) | ✅ violation enregistrée dans oc-catalog |
|
||||
| Dissuasion | Faible | ✅ dégradation de score = conséquence réseau réelle |
|
||||
|
||||
---
|
||||
|
||||
## Licences et Consentements (oc-lib `Resource`)
|
||||
|
||||
Couche légale complémentaire aux AE. Chaque ressource porte ses conditions d'usage.
|
||||
|
||||
```go
|
||||
type ResourceLicense struct {
|
||||
SPDX string // ex. "Apache-2.0", "Proprietary"
|
||||
ConsentURL string // lien vers les CGU / texte de licence complet
|
||||
ConsentRequired bool // si true → workflow bloqué jusqu'à ACK explicite
|
||||
MaxExecCount int // -1 = illimité, sinon quota d'exécutions
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
```
|
||||
|
||||
Le workflow builder bloque la soumission si `ConsentRequired && !consent_recorded`.
|
||||
Le consentement est enregistré dans oc-catalog (horodatage + identité du peer)
|
||||
pour constituer une trace opposable.
|
||||
|
||||
Le `MaxExecCount` croise avec la pre-signed URL à usage unique (Cas 2) — les deux
|
||||
limiteurs se renforcent mutuellement.
|
||||
|
||||
---
|
||||
|
||||
## Changements dans le workflow builder
|
||||
|
||||
```
|
||||
if access.Container == nil && access.Source != "" {
|
||||
if access.IsReachable {
|
||||
// Ajoute une step curl avant la step courante
|
||||
// Commande = <storage_mount>/<filename>
|
||||
} else {
|
||||
// 1. Vérifier les AE locales pour le couplage du workflow
|
||||
// 2. waitForConsiders(PROCESSING_RESOURCE, peerA) via NATS
|
||||
// → payload inclut le résumé de couplage (coupling{}) pour vérification AE côté A
|
||||
// → reçoit presigned URL en réponse (NATS reply), ou erreur AE_VIOLATION
|
||||
// 3. Crée K8s Secret éphémère avec ownerRef sur le workflow
|
||||
// 4. Injecte volume emptyDir medium:Memory
|
||||
// 5. Injecte wrapper script + env secretKeyRef dans la step
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Le hook naturel est le mécanisme `waitForConsiders` / `PB_CONSIDERS` déjà en place
|
||||
pour `STORAGE_RESOURCE` — à étendre avec un `PROCESSING_RESOURCE` source.
|
||||
Le payload doit être enrichi du **résumé de couplage** (peers impliqués, ressources)
|
||||
pour permettre la vérification AE souveraine côté A.
|
||||
|
||||
---
|
||||
|
||||
## Data — même système que Processing, même lacune à combler
|
||||
|
||||
### Constat actuel
|
||||
|
||||
Aujourd'hui, une **Data n'est jamais reliée à un Storage dans un workflow** — c'est un manque.
|
||||
Elle devrait l'être, exactement comme un Processing est relié à un Compute.
|
||||
|
||||
### Ce que ça implique dans le builder
|
||||
|
||||
Une Data avec une `source` doit déclencher le même mécanisme que Processing,
|
||||
à ceci près que la destination n'est pas `/dev/shm` (exécution en mémoire) mais
|
||||
le **Storage lié à la Data dans le workflow**.
|
||||
|
||||
Avant toute step de processing qui consomme ce Storage, le builder doit vérifier
|
||||
si la Data source a déjà été copiée dedans. Sinon, il injecte une step de transfert.
|
||||
|
||||
```
|
||||
Cas isReachable = true (source publique) :
|
||||
|
||||
[step curl Data→Storage] → [step processing qui lit depuis Storage]
|
||||
↓
|
||||
Storage lié à la Data
|
||||
|
||||
Cas isReachable = false (source privée) :
|
||||
|
||||
[step wrapper NATS/Minio → Storage] → [step processing qui lit depuis Storage]
|
||||
↓
|
||||
même protocole que Processing :
|
||||
PB_SOURCE_REQUEST → pre-signed URL → télécharge dans le Storage (pas en mémoire)
|
||||
```
|
||||
|
||||
La différence clé avec Processing :
|
||||
- **Processing** : la source est un exécutable → téléchargé en `/dev/shm`, exécuté, supprimé
|
||||
- **Data** : la source est une donnée → téléchargée dans le Storage lié, persistée pour le processing
|
||||
|
||||
### Changements requis
|
||||
|
||||
**oc-lib** : `DataResourceAccess` (ou généraliser `ResourceAccess`) doit exposer les mêmes champs :
|
||||
|
||||
```go
|
||||
type DataResourceAccess struct {
|
||||
Source string // URL ou clé opaque de la source
|
||||
IsReachable bool
|
||||
Container *models.Container // nil si on passe par Source
|
||||
// + lien vers le Storage cible dans le workflow (à définir)
|
||||
}
|
||||
```
|
||||
|
||||
**workflow builder** : même logique que Processing —
|
||||
|
||||
```
|
||||
if data.access.Container == nil && data.access.Source != "" {
|
||||
if data.access.IsReachable {
|
||||
// Injecte step curl source → Storage lié
|
||||
// avant toute step processing consommant ce Storage
|
||||
} else {
|
||||
// Même protocole NATS/Minio que Processing
|
||||
// mais curl destination = Storage lié (S3 mount), pas /dev/shm
|
||||
// pas de rm après (la donnée doit persister)
|
||||
// pas de wrapper exec — juste le téléchargement
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Workflow** : le lien `Data → Storage` doit être modélisé explicitement
|
||||
(aujourd'hui absent — c'est le prérequis de tout le reste).
|
||||
|
||||
Les AE s'appliquent de la même façon : A peut restreindre l'usage de sa Data
|
||||
à certains peers de storage ou de compute.
|
||||
|
||||
---
|
||||
|
||||
## Validation d'intégrité du workflow — double barrière
|
||||
|
||||
### Principe
|
||||
|
||||
La validation de l'intégrité d'un workflow ne doit **jamais** reposer uniquement sur oc-front.
|
||||
Le front peut être bypassé (appel API direct, client custom, bug). La règle est :
|
||||
|
||||
> **oc-front valide pour l'UX. oc-scheduler valide pour la sécurité.**
|
||||
|
||||
C'est le même principe que la double vérification des AE (B vérifie en local, A vérifie au booking).
|
||||
|
||||
### Liens obligatoires à valider
|
||||
|
||||
| Lien | Manquant → |
|
||||
|---|---|
|
||||
| `Processing → Compute` | Le processing ne peut pas s'exécuter |
|
||||
| `Data → Storage` | La donnée n'a nulle part où atterrir |
|
||||
| `Data.source → Storage lié` | La step de téléchargement ne peut pas être générée |
|
||||
| `Processing.source → Storage lié` (si isReachable) | Idem |
|
||||
|
||||
### oc-front — enforcement UX
|
||||
|
||||
- Bloquer la soumission d'un workflow si un Processing n'a pas de Compute lié
|
||||
- Bloquer si une Data avec source n'a pas de Storage lié
|
||||
- Afficher les erreurs inline sur le graphe du workflow (arête manquante = erreur visuelle)
|
||||
- Ces contrôles sont de la **prévention UX** — ils aident l'utilisateur, ils ne garantissent rien
|
||||
|
||||
### oc-scheduler — validation souveraine
|
||||
|
||||
Avant d'accepter un workflow pour scheduling, oc-scheduler effectue une **validation d'intégrité structurelle** :
|
||||
|
||||
```
|
||||
1. Vérifier que tout Processing a un Compute lié
|
||||
2. Vérifier que toute Data avec source a un Storage lié
|
||||
3. Vérifier que les liens source → storage sont cohérents avec les accès déclarés
|
||||
4. Vérifier les AE pour chaque ressource externe (copie locale oc-catalog)
|
||||
5. Vérifier les consentements de licence requis
|
||||
```
|
||||
|
||||
Si une règle est violée → **rejet immédiat** du workflow avec code d'erreur explicite.
|
||||
Pas de tentative de correction silencieuse — le workflow est invalide tel quel.
|
||||
|
||||
Cette validation se fait **indépendamment de la source de la soumission** (front, API, CLI, autre service).
|
||||
|
||||
### Analogie avec la vérification AE
|
||||
|
||||
```
|
||||
oc-front vérifie les liens ←→ B vérifie les AE en local
|
||||
oc-scheduler valide en entrée ←→ A vérifie les AE au booking
|
||||
|
||||
Dans les deux cas : la barrière arrière est souveraine et ne fait pas confiance à la barrière avant.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Évolutions oc-front
|
||||
|
||||
Dans la page/workflow, les Détails pour **Processing** et **Data** doivent afficher (readonly) :
|
||||
- Si `Container` non nil → afficher le conteneur
|
||||
- Sinon → afficher la source et son mode d'accès (`isReachable` ou privé)
|
||||
- La clé source est rendue via un générateur de clé opaque : humainement lisible,
|
||||
ressemblant à un path mais ne correspondant pas au path réel — dissociation intentionnelle,
|
||||
à expliquer dans l'UI
|
||||
- Les AE et licences associées à la ressource (résumé lisible)
|
||||
- **Erreurs inline** sur le graphe si un lien obligatoire est manquant (Processing sans Compute, Data sans Storage)
|
||||
|
||||
---
|
||||
|
||||
## Intégration oc-catalog
|
||||
|
||||
- Lors du `POST` d'une ressource avec `source != ""` → création automatique de la clé opaque
|
||||
et enregistrement dans la table privée de A (`source_key → real_path`)
|
||||
- Publication des AE dans oc-catalog à la création/modification d'une ressource ;
|
||||
chaque peer maintient une copie locale synchronisée
|
||||
- Enregistrement des violations AE (`AE_VIOLATION`) et mise à jour du score de réputation
|
||||
- Enregistrement des consentements de licence (horodatage + identité du peer)
|
||||
|
||||
---
|
||||
|
||||
## Évolution future — table privée de clés opaques
|
||||
|
||||
Plutôt que de transmettre l'URL pre-signed via NATS (canal réseau),
|
||||
A maintient une table interne `source_key → real_path` et résout lui-même la clé
|
||||
au moment de monter le fichier dans son Minio. B ne reçoit que la pre-signed URL,
|
||||
sans aucune information sur ce qu'elle contient.
|
||||
+1
-10
@@ -4,22 +4,14 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
type Tool interface {
|
||||
CreateArgoWorkflow(path string, ns string) (string, error)
|
||||
CreateAccessSecret(user string, password string, storageId string, namespace string) (string, error)
|
||||
// CreateSourceSecret creates an ephemeral K8s Secret holding a pre-signed URL
|
||||
// for a private source resource. The secret is labelled with the execution ID
|
||||
// so it can be bulk-cleaned up after workflow completion.
|
||||
CreateSourceSecret(secretName, presignedURL, executionID, namespace string) error
|
||||
CreateAccessSecret(ns string, login string, password string) (string, error)
|
||||
GetArgoWatch(executionId string, wfName string) (watch.Interface, error)
|
||||
GetArgoWorkflow(ns string, wfName string) (*wfv1.Workflow, error)
|
||||
GetPodLogger(ns string, wfName string, podName string) (io.ReadCloser, error)
|
||||
GetS3Secret(storageId string, namespace string) *v1.Secret
|
||||
}
|
||||
|
||||
var _service = map[string]func() (Tool, error){
|
||||
@@ -27,7 +19,6 @@ var _service = map[string]func() (Tool, error){
|
||||
}
|
||||
|
||||
func NewService(name string) (Tool, error) {
|
||||
return NewKubernetesTool()
|
||||
service, ok := _service[name]
|
||||
if !ok {
|
||||
return nil, errors.New("service not found")
|
||||
|
||||
+32
-80
@@ -2,17 +2,19 @@ package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"oc-monitord/conf"
|
||||
"oc-monitord/utils"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
|
||||
"github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned"
|
||||
"github.com/google/uuid"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
@@ -29,7 +31,7 @@ type KubernetesTools struct {
|
||||
func NewKubernetesTool() (Tool, error) {
|
||||
// Load Kubernetes config (from ~/.kube/config)
|
||||
config := &rest.Config{
|
||||
Host: "https://" + conf.GetConfig().KubeHost + ":" + conf.GetConfig().KubePort,
|
||||
Host: conf.GetConfig().KubeHost + ":" + conf.GetConfig().KubePort,
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
CAData: []byte(conf.GetConfig().KubeCA),
|
||||
CertData: []byte(conf.GetConfig().KubeCert),
|
||||
@@ -75,6 +77,7 @@ func (k *KubernetesTools) CreateArgoWorkflow(path string, ns string) (string, er
|
||||
if !ok {
|
||||
return "", errors.New("decoded object is not a Workflow")
|
||||
}
|
||||
|
||||
// Create the workflow in the "argo" namespace
|
||||
createdWf, err := k.VersionedSet.ArgoprojV1alpha1().Workflows(ns).Create(context.TODO(), workflow, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
@@ -85,20 +88,21 @@ func (k *KubernetesTools) CreateArgoWorkflow(path string, ns string) (string, er
|
||||
return createdWf.Name, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesTools) CreateAccessSecret(access string, password string, storageId string, namespace string) (string, error) {
|
||||
func (k *KubernetesTools) CreateAccessSecret(ns string, login string, password string) (string, error) {
|
||||
// Namespace where the secret will be created
|
||||
namespace := "default"
|
||||
// Encode the secret data (Kubernetes requires base64-encoded values)
|
||||
secretData := map[string][]byte{
|
||||
"access-key": []byte(access),
|
||||
"secret-key": []byte(password),
|
||||
"access-key": []byte(base64.StdEncoding.EncodeToString([]byte(login))),
|
||||
"secret-key": []byte(base64.StdEncoding.EncodeToString([]byte(password))),
|
||||
}
|
||||
|
||||
// Define the Secret object
|
||||
name := storageId + "-secret-s3"
|
||||
name := uuid.New().String()
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Namespace: ns,
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
Data: secretData,
|
||||
@@ -108,54 +112,12 @@ func (k *KubernetesTools) CreateAccessSecret(access string, password string, sto
|
||||
if err != nil {
|
||||
return "", errors.New("Error creating secret: " + err.Error())
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// CreateSourceSecret creates an ephemeral Opaque Secret containing a pre-signed URL
|
||||
// for a private source resource. The secret is labelled with the execution ID so
|
||||
// it can be bulk-cleaned up after workflow completion.
|
||||
func (k *KubernetesTools) CreateSourceSecret(secretName, presignedURL, executionID, namespace string) error {
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{
|
||||
"oc-execution-id": executionID,
|
||||
"oc-managed-by": "oc-monitord",
|
||||
"oc-secret-type": "source-presigned",
|
||||
},
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
Data: map[string][]byte{
|
||||
"presigned-url": []byte(presignedURL),
|
||||
},
|
||||
}
|
||||
_, err := k.Set.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
|
||||
if err != nil && !k8serrors.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("error creating source secret %s: %w", secretName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubernetesTools) GetS3Secret(storageId string, namespace string) *v1.Secret {
|
||||
|
||||
secret, err := k.Set.CoreV1().Secrets(namespace).Get(context.TODO(), storageId+"-secret-s3", metav1.GetOptions{})
|
||||
// Get(context.TODO(),storageId + "-artifact-server", metav1.GetOptions{})
|
||||
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
l := utils.GetLogger()
|
||||
l.Fatal().Msg("An error happened when retrieving secret in " + namespace + " : " + err.Error())
|
||||
}
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return secret
|
||||
// return secret
|
||||
}
|
||||
|
||||
func (k *KubernetesTools) GetArgoWatch(executionId string, wfName string) (watch.Interface, error){
|
||||
wfl := utils.GetWFLogger("")
|
||||
wfl.Debug().Msg("Starting argo watch with argo lib")
|
||||
options := metav1.ListOptions{FieldSelector: "metadata.name=oc-monitor-"+wfName}
|
||||
|
||||
watcher, err := k.VersionedSet.ArgoprojV1alpha1().Workflows(executionId).Watch(context.Background(), options)
|
||||
@@ -164,24 +126,22 @@ func (k *KubernetesTools) GetArgoWatch(executionId string, wfName string) (watch
|
||||
}
|
||||
|
||||
return watcher, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesTools) GetArgoWorkflow(ns string, wfName string) (*wfv1.Workflow, error) {
|
||||
return k.VersionedSet.ArgoprojV1alpha1().Workflows(ns).Get(context.TODO(), wfName, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
func (k *KubernetesTools) GetPodLogger(ns string, wfName string, nodeName string) (io.ReadCloser, error) {
|
||||
var targetPod v1.Pod
|
||||
|
||||
|
||||
pods, err := k.Set.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{
|
||||
LabelSelector: "workflows.argoproj.io/workflow="+wfName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s", "failed to list pods: "+err.Error())
|
||||
return nil, fmt.Errorf("failed to list pods: " + err.Error())
|
||||
}
|
||||
if len(pods.Items) == 0 {
|
||||
|
||||
return nil, fmt.Errorf("%s", "no pods found with label workflows.argoproj.io/workflow="+wfName+" no pods found with label workflows.argoproj.io/node-name="+nodeName+" in namespace "+ns)
|
||||
return nil, fmt.Errorf("no pods found with label workflows.argoproj.io/workflow="+ wfName + " no pods found with label workflows.argoproj.io/node-name=" + nodeName + " in namespace " + ns)
|
||||
}
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
@@ -190,48 +150,40 @@ func (k *KubernetesTools) GetPodLogger(ns string, wfName string, nodeName string
|
||||
}
|
||||
}
|
||||
|
||||
if targetPod.Name == "" {
|
||||
return nil, fmt.Errorf("no pod found matching node-name %s in namespace %s", nodeName, ns)
|
||||
}
|
||||
|
||||
// k8s API throws an error if we try getting logs while the container are not initialized, so we repeat status check there
|
||||
k.testPodReady(targetPod, ns)
|
||||
|
||||
// When using kubec logs for a pod we see it contacts /api/v1/namespaces/NAMESPACE/pods/oc-monitor-PODNAME/log?container=main so we add this container: main to the call
|
||||
req, err := k.Set.CoreV1().Pods(ns).GetLogs(targetPod.Name, &v1.PodLogOptions{Follow: true, Container: "main"}). Stream(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s", " Error when trying to get logs for "+targetPod.Name+" : "+err.Error())
|
||||
return nil, fmt.Errorf(" Error when trying to get logs for " + targetPod.Name + " : " + err.Error())
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesTools) testPodReady(pod v1.Pod, ns string) {
|
||||
wfl := utils.GetWFLogger("")
|
||||
|
||||
watcher, err := k.Set.CoreV1().Pods(ns).Watch(context.Background(), metav1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + pod.Name,
|
||||
ResourceVersion: pod.ResourceVersion,
|
||||
})
|
||||
for {
|
||||
pod, err := k.Set.CoreV1().Pods(ns).Get(context.Background(), pod.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
wfl.Error().Msg("Error watching pod: " + err.Error() + "\n")
|
||||
return
|
||||
wfl := utils.GetWFLogger("")
|
||||
wfl.Error().Msg("Error fetching pod: " + err.Error() + "\n")
|
||||
break
|
||||
}
|
||||
defer watcher.Stop()
|
||||
|
||||
for event := range watcher.ResultChan() {
|
||||
p, ok := event.Object.(*v1.Pod)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var initialized bool
|
||||
for _, cond := range pod.Status.Conditions {
|
||||
// It seems that for remote pods the pod gets the Succeeded status before it has time to display the it is ready to run in .status.conditions,so we added the OR condition
|
||||
if p.Status.Phase == v1.PodSucceeded {
|
||||
return
|
||||
}
|
||||
for _, cond := range p.Status.Conditions {
|
||||
if cond.Type == v1.PodReady && cond.Status == v1.ConditionTrue {
|
||||
if (cond.Type == v1.PodReady && cond.Status == v1.ConditionTrue) || pod.Status.Phase == v1.PodSucceeded {
|
||||
initialized = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if initialized {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second) // avoid hammering the API
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"oc-monitord/conf"
|
||||
"sync"
|
||||
|
||||
oclib "cloud.o-forge.io/core/oc-lib"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -39,31 +36,6 @@ func GetLogger() zerolog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
// EmitExecStateUpdate loads the execution, sets its state and emits a
|
||||
// CREATE_RESOURCE NATS event so oc-scheduler applies the change and fires
|
||||
// NotifyChange for the WebSocket streams.
|
||||
// Direct UpdateOne calls are replaced by this function so oc-scheduler remains
|
||||
// the single writer for WorkflowExecution.
|
||||
func EmitExecStateUpdate(execID string, state enum.BookingStatus) {
|
||||
adminReq := &tools.APIRequest{Admin: true}
|
||||
res, _, err := workflow_execution.NewAccessor(adminReq).LoadOne(execID)
|
||||
if err != nil || res == nil {
|
||||
return
|
||||
}
|
||||
exec := res.(*workflow_execution.WorkflowExecution)
|
||||
exec.State = state
|
||||
payload, marshalErr := json.Marshal(exec)
|
||||
if marshalErr != nil {
|
||||
return
|
||||
}
|
||||
tools.NewNATSCaller().SetNATSPub(tools.CREATE_RESOURCE, tools.NATSResponse{
|
||||
FromApp: "oc-monitord",
|
||||
Datatype: tools.WORKFLOW_EXECUTION,
|
||||
Method: int(tools.CREATE_RESOURCE),
|
||||
Payload: payload,
|
||||
})
|
||||
}
|
||||
|
||||
func GetWFLogger(workflowName string) zerolog.Logger {
|
||||
onceWF.Do(func(){
|
||||
wf_logger = logger.With().
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package workflow_builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"oc-monitord/utils"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
oclib "cloud.o-forge.io/core/oc-lib"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||
tools "cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
|
||||
type AdmiraltySetter struct {
|
||||
Id string // ID to identify the execution, correspond to workflow_executions id
|
||||
NodeName string // Allows to retrieve the name of the node used for this execution on each peer {"peerId": "nodeName"}
|
||||
}
|
||||
|
||||
func (s *AdmiraltySetter) InitializeAdmiralty(localPeerID string,remotePeerID string) error {
|
||||
|
||||
logger := logs.GetLogger()
|
||||
|
||||
data := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER),"",localPeerID,nil,nil).LoadOne(remotePeerID)
|
||||
if data.Code != 200 {
|
||||
logger.Error().Msg("Error while trying to instantiate remote peer " + remotePeerID)
|
||||
return fmt.Errorf(data.Err)
|
||||
}
|
||||
remotePeer := data.ToPeer()
|
||||
|
||||
data = oclib.NewRequest(oclib.LibDataEnum(oclib.PEER),"",localPeerID,nil,nil).LoadOne(localPeerID)
|
||||
if data.Code != 200 {
|
||||
logger.Error().Msg("Error while trying to instantiate local peer " + remotePeerID)
|
||||
return fmt.Errorf(data.Err)
|
||||
}
|
||||
localPeer := data.ToPeer()
|
||||
|
||||
caller := tools.NewHTTPCaller(
|
||||
map[tools.DataType]map[tools.METHOD]string{
|
||||
tools.ADMIRALTY_SOURCE: {
|
||||
tools.POST :"/:id",
|
||||
},
|
||||
tools.ADMIRALTY_KUBECONFIG: {
|
||||
tools.GET:"/:id",
|
||||
},
|
||||
tools.ADMIRALTY_SECRET: {
|
||||
tools.POST:"/:id/" + remotePeerID,
|
||||
},
|
||||
tools.ADMIRALTY_TARGET: {
|
||||
tools.POST:"/:id/" + remotePeerID,
|
||||
},
|
||||
tools.ADMIRALTY_NODES: {
|
||||
tools.GET:"/:id/" + remotePeerID,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
logger.Info().Msg("\n\n Creating the Admiralty Source on " + remotePeerID + " ns-" + s.Id)
|
||||
_ = s.callRemoteExecution(remotePeer, []int{http.StatusCreated, http.StatusConflict},caller, s.Id, tools.ADMIRALTY_SOURCE, tools.POST, nil, true)
|
||||
logger.Info().Msg("\n\n Retrieving kubeconfig with the secret on " + remotePeerID + " ns-" + s.Id)
|
||||
kubeconfig := s.getKubeconfig(remotePeer, caller)
|
||||
logger.Info().Msg("\n\n Creating a secret from the kubeconfig " + localPeerID + " ns-" + s.Id)
|
||||
_ = s.callRemoteExecution(localPeer, []int{http.StatusCreated}, caller,s.Id, tools.ADMIRALTY_SECRET, tools.POST,kubeconfig, true)
|
||||
logger.Info().Msg("\n\n Creating the Admiralty Target on " + localPeerID + " in namespace " + s.Id )
|
||||
_ = s.callRemoteExecution(localPeer,[]int{http.StatusCreated, http.StatusConflict},caller,s.Id,tools.ADMIRALTY_TARGET,tools.POST, nil, true)
|
||||
logger.Info().Msg("\n\n Checking for the creation of the admiralty node on " + localPeerID + " ns-" + s.Id)
|
||||
s.checkNodeStatus(localPeer,caller)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdmiraltySetter) getKubeconfig(peer *peer.Peer, caller *tools.HTTPCaller) map[string]string {
|
||||
var kubedata map[string]string
|
||||
_ = s.callRemoteExecution(peer, []int{http.StatusOK}, caller, s.Id, tools.ADMIRALTY_KUBECONFIG, tools.GET, nil, true)
|
||||
if caller.LastResults["body"] == nil || len(caller.LastResults["body"].([]byte)) == 0 {
|
||||
l := utils.GetLogger()
|
||||
l.Error().Msg("Something went wrong when retrieving data from Get call for kubeconfig")
|
||||
panic(0)
|
||||
}
|
||||
err := json.Unmarshal(caller.LastResults["body"].([]byte), &kubedata)
|
||||
if err != nil {
|
||||
l := utils.GetLogger()
|
||||
l.Error().Msg("Something went wrong when unmarshalling data from Get call for kubeconfig")
|
||||
panic(0)
|
||||
}
|
||||
|
||||
return kubedata
|
||||
}
|
||||
|
||||
func (*AdmiraltySetter) callRemoteExecution(peer *peer.Peer, expectedCode []int,caller *tools.HTTPCaller, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, panicCode bool) *peer.PeerExecution {
|
||||
l := utils.GetLogger()
|
||||
resp, err := peer.LaunchPeerExecution(peer.UUID, dataID, dt, method, body, caller)
|
||||
if err != nil {
|
||||
l.Error().Msg("Error when executing on peer at" + peer.Url)
|
||||
l.Error().Msg(err.Error())
|
||||
panic(0)
|
||||
}
|
||||
|
||||
if !slices.Contains(expectedCode, caller.LastResults["code"].(int)) {
|
||||
l.Error().Msg(fmt.Sprint("Didn't receive the expected code :", caller.LastResults["code"], "when expecting", expectedCode))
|
||||
if _, ok := caller.LastResults["body"]; ok {
|
||||
l.Info().Msg(string(caller.LastResults["body"].([]byte)))
|
||||
}
|
||||
if panicCode {
|
||||
panic(0)
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *AdmiraltySetter) storeNodeName(caller *tools.HTTPCaller){
|
||||
var data map[string]interface{}
|
||||
if resp, ok := caller.LastResults["body"]; ok {
|
||||
json.Unmarshal(resp.([]byte), &data)
|
||||
}
|
||||
|
||||
if node, ok := data["node"]; ok {
|
||||
metadata := node.(map[string]interface{})["metadata"]
|
||||
name := metadata.(map[string]interface{})["name"].(string)
|
||||
s.NodeName = name
|
||||
} else {
|
||||
l := utils.GetLogger()
|
||||
l.Error().Msg("Could not retrieve data about the recently created node")
|
||||
panic(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdmiraltySetter) checkNodeStatus(localPeer *peer.Peer, caller *tools.HTTPCaller){
|
||||
for i := range(5) {
|
||||
time.Sleep(10 * time.Second) // let some time for kube to generate the node
|
||||
_ = s.callRemoteExecution(localPeer,[]int{http.StatusOK},caller,s.Id,tools.ADMIRALTY_NODES,tools.GET, nil, false)
|
||||
if caller.LastResults["code"] == 200 {
|
||||
s.storeNodeName(caller)
|
||||
return
|
||||
}
|
||||
if i == 5 {
|
||||
logger.Error().Msg("Node on " + localPeer.Name + " was never found, panicking !")
|
||||
panic(0)
|
||||
}
|
||||
logger.Info().Msg("Could not verify that node is up. Retrying...")
|
||||
}
|
||||
|
||||
}
|
||||
+186
-862
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
||||
# argo_builder.go — Résumé
|
||||
|
||||
## Rôle général
|
||||
|
||||
`argo_builder.go` traduit un **Workflow Open Cloud** (graphe de nœuds : processings,
|
||||
stockages, computes, sous-workflows) en un **fichier YAML Argo Workflow** prêt à
|
||||
être soumis à un cluster Kubernetes.
|
||||
|
||||
---
|
||||
|
||||
## Structures principales
|
||||
|
||||
| Struct | Rôle |
|
||||
|---|---|
|
||||
| `ArgoBuilder` | Constructeur principal. Porte le workflow source, la structure YAML en cours de build, les services k8s, le timeout et la liste des peers distants (Admiralty). |
|
||||
| `Workflow` | Racine du YAML Argo (`apiVersion`, `kind`, `metadata`, `spec`). |
|
||||
| `Spec` | Spécification du workflow : compte de service, entrypoint, templates, volumes, timeout, référence au dépôt d'artefacts S3. |
|
||||
| `ArgoKubeEvent` | Événement publié sur NATS lors de la demande de provisionnement d'une ressource distante (compute ou stockage S3). Contient `executions_id`, `dest_peer_id`, `source_peer_id`, `data_type`, `origin_id`. |
|
||||
|
||||
---
|
||||
|
||||
## Flux d'exécution principal
|
||||
|
||||
```
|
||||
CreateDAG()
|
||||
└─ createTemplates()
|
||||
├─ [pour chaque processing] createArgoTemplates()
|
||||
│ ├─ addTaskToArgo() → ajoute la tâche au DAG + dépendances
|
||||
│ ├─ CreateContainer() → template container Argo
|
||||
│ ├─ AddAdmiraltyAnnotations() → si peer distant détecté
|
||||
│ └─ addStorageAnnotations() → S3 + volumes locaux
|
||||
├─ [pour chaque native tool WORKFLOW_EVENT] createArgoTemplates()
|
||||
└─ [pour chaque sous-workflow]
|
||||
├─ CreateDAG() récursif
|
||||
└─ fusion DAG + recâblage des dépendances
|
||||
└─ createVolumes() → PersistentVolumeClaims
|
||||
|
||||
CompleteBuild()
|
||||
├─ waitForConsiders() × N peers → validation Admiralty (COMPUTE_RESOURCE)
|
||||
├─ mise à jour annotations Admiralty (clustername)
|
||||
└─ écriture du YAML dans ./argo_workflows/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fonctions clés
|
||||
|
||||
### `CreateDAG(exec, namespace, write) → (nbTâches, firstItems, lastItems, err)`
|
||||
Point d'entrée. Initialise le logger, déclenche la création des templates et des
|
||||
volumes, configure les métadonnées globales du workflow Argo.
|
||||
|
||||
### `createTemplates(exec, namespace) → (firstItems, lastItems, volumes)`
|
||||
Itère sur tous les nœuds du graphe.
|
||||
- Processings → template container.
|
||||
- Native tools `WORKFLOW_EVENT` → template événement.
|
||||
- Sous-workflows → build récursif + fusion DAG + recâblage des dépendances entrantes/sortantes.
|
||||
|
||||
### `createArgoTemplates(exec, namespace, id, obj, …)`
|
||||
Crée le template Argo pour un nœud donné.
|
||||
Détecte si le processing est **réparti** (peer distant via `isReparted`) → ajoute les
|
||||
annotations Admiralty et enregistre le peer dans `RemotePeers`.
|
||||
Délègue la configuration du stockage à `addStorageAnnotations`.
|
||||
|
||||
### `addStorageAnnotations(exec, id, template, namespace, volumes)`
|
||||
Pour chaque stockage lié au processing :
|
||||
- **S3** : appelle `waitForConsiders(STORAGE_RESOURCE)` pour chaque compute associé,
|
||||
puis configure la référence au dépôt d'artefacts via `addS3annotations`.
|
||||
- **Local** : monte un `VolumeMount` dans le container.
|
||||
|
||||
### `waitForConsiders(executionsId, dataType, event)`
|
||||
**Fonction bloquante.**
|
||||
1. Publie l'`ArgoKubeEvent` sur le canal NATS `ARGO_KUBE_EVENT`.
|
||||
2. S'abonne à `PROPALGATION_EVENT`.
|
||||
3. Attend un `PropalgationMessage` vérifiant :
|
||||
- `Action == PB_CONSIDERS`
|
||||
- `DataType == dataType`
|
||||
- `Payload.executions_id == executionsId`
|
||||
4. Timeout : **5 minutes**.
|
||||
|
||||
| Appelant | DataType attendu | Signification |
|
||||
|---|---|---|
|
||||
| `addStorageAnnotations` (S3) | `STORAGE_RESOURCE` | Le stockage S3 distant est prêt |
|
||||
| `CompleteBuild` (Admiralty) | `COMPUTE_RESOURCE` | Le cluster cible Admiralty est configuré |
|
||||
|
||||
### `CompleteBuild(executionsId) → (cheminYAML, err)`
|
||||
Finalise le build :
|
||||
1. Pour chaque peer dans `RemotePeers` → `waitForConsiders(COMPUTE_RESOURCE)` (bloquant, séquentiel).
|
||||
2. Met à jour les annotations `multicluster.admiralty.io/clustername` avec `target-<peerId>-<executionsId>`.
|
||||
3. Sérialise le workflow en YAML et l'écrit dans `./argo_workflows/<nom>_<timestamp>.yml`.
|
||||
|
||||
### `isReparted(processing, graphID) → (bool, *peer.Peer)`
|
||||
Retrouve le Compute attaché au processing, charge le Peer propriétaire via l'API
|
||||
oc-lib, et vérifie si `Relation != 1` (pas le peer local).
|
||||
|
||||
### `addTaskToArgo(exec, dag, graphItemID, processing, …)`
|
||||
Crée une `Task` Argo (nom unique, template, dépendances DAG, paramètres env/inputs/outputs)
|
||||
et la rattache au DAG. Met à jour `firstItems` / `lastItems`.
|
||||
|
||||
### `isArgoDependancy(id) → (bool, []string)`
|
||||
Vérifie si un nœud est utilisé comme source d'un lien sortant vers un autre
|
||||
processing ou workflow (il est donc une dépendance pour quelqu'un).
|
||||
|
||||
### `getArgoDependencies(id) → []string`
|
||||
Retourne les noms des tâches Argo dont ce nœud dépend (liens entrants).
|
||||
|
||||
---
|
||||
|
||||
## Protocole NATS utilisé
|
||||
|
||||
```
|
||||
Publication → canal : ARGO_KUBE_EVENT
|
||||
payload : NATSResponse{Method: PROPALGATION_EVENT, Payload: ArgoKubeEvent}
|
||||
|
||||
Attente ← canal : PROPALGATION_EVENT
|
||||
filtre : PropalgationMessage{
|
||||
Action = PB_CONSIDERS,
|
||||
DataType = COMPUTE_RESOURCE | STORAGE_RESOURCE,
|
||||
Payload = {"executions_id": "<id en cours>"}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichier YAML produit
|
||||
|
||||
- Nom : `oc-monitor-<mot1>-<mot2>_<DD_MM_YYYY_hhmmss>.yml`
|
||||
- Dossier : `./argo_workflows/`
|
||||
- Permissions : `0660`
|
||||
@@ -5,11 +5,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func (b *ArgoBuilder) CreateService(exec *workflow_execution.WorkflowExecution, id string, processing resources.ResourceInterface) {
|
||||
func (b *ArgoBuilder) CreateService(id string, processing *resources.ProcessingResource) {
|
||||
new_service := models.Service{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
@@ -25,15 +24,17 @@ func (b *ArgoBuilder) CreateService(exec *workflow_execution.WorkflowExecution,
|
||||
if processing == nil {
|
||||
return
|
||||
}
|
||||
b.completeServicePorts(exec, &new_service, id, processing)
|
||||
b.completeServicePorts(&new_service, id, processing)
|
||||
b.Services = append(b.Services, &new_service)
|
||||
}
|
||||
|
||||
func (b *ArgoBuilder) completeServicePorts(exec *workflow_execution.WorkflowExecution, service *models.Service, id string, processing resources.ResourceInterface) {
|
||||
for _, execute := range b.OriginWorkflow.Exposes[processing.GetID()] {
|
||||
func (b *ArgoBuilder) completeServicePorts(service *models.Service, id string, processing *resources.ProcessingResource) {
|
||||
instance := processing.GetSelectedInstance()
|
||||
if instance != nil && instance.(*resources.ProcessingInstance).Access != nil && instance.(*resources.ProcessingInstance).Access.Container != nil {
|
||||
for _, execute := range instance.(*resources.ProcessingInstance).Access.Container.Exposes {
|
||||
if execute.PAT != 0 {
|
||||
new_port_translation := models.ServicePort{
|
||||
Name: strings.ToLower(processing.GetName()) + id,
|
||||
Name: strings.ToLower(processing.Name) + id,
|
||||
Port: execute.Port,
|
||||
TargetPort: execute.PAT,
|
||||
Protocol: "TCP",
|
||||
@@ -41,7 +42,7 @@ func (b *ArgoBuilder) completeServicePorts(exec *workflow_execution.WorkflowExec
|
||||
service.Spec.Ports = append(service.Spec.Ports, new_port_translation)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ArgoBuilder) addServiceToArgo() error {
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
package workflow_builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
// ── considersCache (signal-only) ─────────────────────────────────────────────
|
||||
|
||||
// considersCache stocke les canaux en attente d'un PB_CONSIDERS,
|
||||
// indexés par "executionsId:dataType". Un même message NATS réveille
|
||||
// tous les waiters enregistrés sous la même clé (broadcast).
|
||||
type considersCache struct {
|
||||
mu sync.Mutex
|
||||
pending map[string][]chan struct{}
|
||||
}
|
||||
|
||||
var globalConsidersCache = &considersCache{
|
||||
pending: make(map[string][]chan struct{}),
|
||||
}
|
||||
|
||||
// considersKey construit la clé du cache à partir de l'ID d'exécution,
|
||||
// du type de données et du peer compute (SourcePeerID).
|
||||
// peerID permet de différencier plusieurs waiters COMPUTE_RESOURCE du même
|
||||
// executionsId (1 local + N distants en parallèle).
|
||||
func considersKey(executionsId string, dataType tools.DataType, peerID string) string {
|
||||
key := executionsId + ":" + strconv.Itoa(dataType.EnumIndex())
|
||||
if peerID != "" {
|
||||
key += ":" + peerID
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// register inscrit un nouveau canal d'attente pour la clé donnée.
|
||||
// Retourne le canal à lire et une fonction de désinscription à appeler en defer.
|
||||
func (c *considersCache) register(key string) (<-chan struct{}, func()) {
|
||||
ch := make(chan struct{}, 1)
|
||||
c.mu.Lock()
|
||||
c.pending[key] = append(c.pending[key], ch)
|
||||
c.mu.Unlock()
|
||||
|
||||
unregister := func() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
list := c.pending[key]
|
||||
for i, existing := range list {
|
||||
if existing == ch {
|
||||
c.pending[key] = append(list[:i], list[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(c.pending[key]) == 0 {
|
||||
delete(c.pending, key)
|
||||
}
|
||||
}
|
||||
return ch, unregister
|
||||
}
|
||||
|
||||
// confirm réveille tous les waiters enregistrés sous la clé donnée
|
||||
// et les supprime du cache.
|
||||
func (c *considersCache) confirm(key string) {
|
||||
c.mu.Lock()
|
||||
list := c.pending[key]
|
||||
delete(c.pending, key)
|
||||
c.mu.Unlock()
|
||||
|
||||
for _, ch := range list {
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── sourcePresignedCache (value-bearing) ─────────────────────────────────────
|
||||
|
||||
// sourcePresignedCache stocke les canaux en attente d'une URL pré-signée pour
|
||||
// une source privée (isReachable=false), indexés par la clé sourceConsidersKey.
|
||||
// La valeur transportée est l'URL pré-signée elle-même.
|
||||
type sourcePresignedCache struct {
|
||||
mu sync.Mutex
|
||||
pending map[string][]chan string
|
||||
}
|
||||
|
||||
var globalSourceCache = &sourcePresignedCache{
|
||||
pending: make(map[string][]chan string),
|
||||
}
|
||||
|
||||
// sourceConsidersKey construit une clé unique pour une demande de source privée.
|
||||
// La clé encode l'executionsID, le peerID du propriétaire et le resourceID
|
||||
// pour permettre des requêtes parallèles distinctes.
|
||||
func sourceConsidersKey(executionsID, peerID, resourceID string) string {
|
||||
return executionsID + ":src:" + peerID + ":" + resourceID
|
||||
}
|
||||
|
||||
// register inscrit un nouveau canal d'attente pour la clé donnée.
|
||||
// Retourne le canal à lire et une fonction de désinscription à appeler en defer.
|
||||
func (c *sourcePresignedCache) register(key string) (<-chan string, func()) {
|
||||
ch := make(chan string, 1)
|
||||
c.mu.Lock()
|
||||
c.pending[key] = append(c.pending[key], ch)
|
||||
c.mu.Unlock()
|
||||
|
||||
unregister := func() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
list := c.pending[key]
|
||||
for i, existing := range list {
|
||||
if existing == ch {
|
||||
c.pending[key] = append(list[:i], list[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(c.pending[key]) == 0 {
|
||||
delete(c.pending, key)
|
||||
}
|
||||
}
|
||||
return ch, unregister
|
||||
}
|
||||
|
||||
// confirm réveille tous les waiters enregistrés sous la clé donnée
|
||||
// en leur transmettant l'URL pré-signée, puis les supprime du cache.
|
||||
func (c *sourcePresignedCache) confirm(key, url string) {
|
||||
c.mu.Lock()
|
||||
list := c.pending[key]
|
||||
delete(c.pending, key)
|
||||
c.mu.Unlock()
|
||||
|
||||
for _, ch := range list {
|
||||
select {
|
||||
case ch <- url:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── StartConsidersListener ────────────────────────────────────────────────────
|
||||
|
||||
// StartConsidersListener démarre un abonné NATS global via ListenNats (oclib)
|
||||
// qui reçoit les messages CONSIDERS_EVENT et réveille les goroutines en attente.
|
||||
//
|
||||
// Deux chemins de dispatch :
|
||||
// - Si presigned_url est présent dans le payload → globalSourceCache (Phase 4).
|
||||
// - Sinon → globalConsidersCache (Phases COMPUTE / STORAGE, signal sans valeur).
|
||||
//
|
||||
// Doit être appelé une seule fois au démarrage.
|
||||
func StartConsidersListener() {
|
||||
log := logs.GetLogger()
|
||||
log.Info().Msg("Considers NATS listener starting on " + tools.CONSIDERS_EVENT.GenerateKey())
|
||||
go tools.NewNATSCaller().ListenNats(map[tools.NATSMethod]func(tools.NATSResponse){
|
||||
tools.CONSIDERS_EVENT: func(resp tools.NATSResponse) {
|
||||
fmt.Println("CONSIDERS")
|
||||
var body struct {
|
||||
ExecutionsID string `json:"executions_id"`
|
||||
PeerID string `json:"peer_id,omitempty"`
|
||||
// PresignedURL est non-vide uniquement pour les réponses de source privée (Phase 4).
|
||||
PresignedURL string `json:"presigned_url,omitempty"`
|
||||
// ResourceID identifie la ressource Processing/Data pour la Phase 4.
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Payload, &body); err != nil {
|
||||
log.Error().Msg("CONSIDERS_EVENT: cannot unmarshal payload: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if body.PresignedURL != "" {
|
||||
// Phase 4 — source privée : transmettre l'URL pré-signée.
|
||||
key := sourceConsidersKey(body.ExecutionsID, body.PeerID, body.ResourceID)
|
||||
log.Info().Msg(fmt.Sprintf("CONSIDERS_EVENT (presigned) dispatched for key=%s", key))
|
||||
globalSourceCache.confirm(key, body.PresignedURL)
|
||||
} else {
|
||||
// Phases COMPUTE / STORAGE — simple signal.
|
||||
key := considersKey(body.ExecutionsID, resp.Datatype, body.PeerID)
|
||||
log.Info().Msg(fmt.Sprintf("CONSIDERS_EVENT dispatched for key=%s", key))
|
||||
globalConsidersCache.confirm(key)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
oclib "cloud.o-forge.io/core/oc-lib"
|
||||
workflow "cloud.o-forge.io/core/oc-lib/models/workflow"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
)
|
||||
|
||||
type WorflowDB struct {
|
||||
@@ -14,20 +13,20 @@ type WorflowDB struct {
|
||||
}
|
||||
|
||||
// Create the obj!ects from the mxgraphxml stored in the workflow given as a parameter
|
||||
func (w *WorflowDB) LoadFrom(workflow_id string) error {
|
||||
func (w *WorflowDB) LoadFrom(workflow_id string, peerID string) error {
|
||||
logger.Info().Msg("Loading workflow from " + workflow_id)
|
||||
var err error
|
||||
if w.Workflow, err = w.getWorkflow(workflow_id); err != nil {
|
||||
if w.Workflow, err = w.getWorkflow(workflow_id, peerID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use oclib to retrieve the graph contained in the workflow referenced
|
||||
func (w *WorflowDB) getWorkflow(workflow_id string) (workflow *workflow.Workflow, err error) {
|
||||
func (w *WorflowDB) getWorkflow(workflow_id string, peerID string) (workflow *workflow.Workflow, err error) {
|
||||
logger := oclib.GetLogger()
|
||||
|
||||
lib_data := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.WORKFLOW), nil).LoadOne(workflow_id)
|
||||
lib_data := oclib.NewRequest(oclib.LibDataEnum(oclib.WORKFLOW), "", peerID, []string{}, nil).LoadOne(workflow_id)
|
||||
logger.Info().Msg(fmt.Sprint("ERR", lib_data.Code, lib_data.Err))
|
||||
if lib_data.Code != 200 {
|
||||
logger.Error().Msg("Error loading the graph")
|
||||
@@ -42,7 +41,7 @@ func (w *WorflowDB) getWorkflow(workflow_id string) (workflow *workflow.Workflow
|
||||
return new_wf, nil
|
||||
}
|
||||
|
||||
func (w *WorflowDB) ExportToArgo(exec *workflow_execution.WorkflowExecution, timeout int) (*ArgoBuilder, int, error) {
|
||||
func (w *WorflowDB) ExportToArgo(namespace string, timeout int) (*ArgoBuilder, int, error) {
|
||||
logger := oclib.GetLogger()
|
||||
logger.Info().Msg(fmt.Sprint("Exporting to Argo", w.Workflow))
|
||||
if len(w.Workflow.Name) == 0 || w.Workflow.Graph == nil {
|
||||
@@ -50,7 +49,7 @@ func (w *WorflowDB) ExportToArgo(exec *workflow_execution.WorkflowExecution, tim
|
||||
}
|
||||
|
||||
argoBuilder := ArgoBuilder{OriginWorkflow: w.Workflow, Timeout: timeout}
|
||||
stepMax, _, _, err := argoBuilder.CreateDAG(exec, exec.ExecutionsID, true)
|
||||
stepMax, _, _, err := argoBuilder.CreateDAG(namespace, true)
|
||||
if err != nil {
|
||||
logger.Error().Msg("Could not create the argo file for " + w.Workflow.Name)
|
||||
return nil, 0, err
|
||||
|
||||
@@ -6,5 +6,5 @@ import (
|
||||
|
||||
func TestGetGraph(t *testing.T) {
|
||||
w := WorflowDB{}
|
||||
w.LoadFrom("test-log")
|
||||
w.LoadFrom("test-log", "")
|
||||
}
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
package workflow_builder
|
||||
|
||||
// source_fetch.go — Phase 3 : gestion des sources tierces (isReachable = true)
|
||||
//
|
||||
// Pour chaque ressource (Processing ou Data) dont l'instance expose une source
|
||||
// publique (access.Container == nil, access.Source != "", access.IsReachable),
|
||||
// le builder injecte une step Argo de téléchargement (curl) AVANT la step qui
|
||||
// consomme la ressource.
|
||||
//
|
||||
// Garde critique : si la step aval (processing) contient déjà un curl ciblant
|
||||
// la même URL dans sa commande de container, on n'injecte PAS de step
|
||||
// supplémentaire — ce serait un double téléchargement.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
. "oc-monitord/models"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
)
|
||||
|
||||
// curlImage est l'image utilisée pour la step de téléchargement.
|
||||
// alpine dispose de wget ; on installe curl à la volée, ou on utilise
|
||||
// directement wget. Utiliser curlimages/curl évite l'installation.
|
||||
const curlImage = "curlimages/curl:latest"
|
||||
|
||||
// ── Garde ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// sourceAlreadyFetchedByStep retourne true si le container du processing
|
||||
// identifié par processingItemID contient déjà un appel curl/wget ciblant
|
||||
// sourceURL dans sa commande ou ses arguments.
|
||||
//
|
||||
// Si c'est le cas on NE doit PAS injecter une step curl supplémentaire :
|
||||
// le processing gère lui-même le téléchargement et injecter une step
|
||||
// amont serait un double téléchargement.
|
||||
func (b *ArgoBuilder) sourceAlreadyFetchedByStep(
|
||||
exec *workflow_execution.WorkflowExecution,
|
||||
processingItemID string,
|
||||
sourceURL string,
|
||||
) bool {
|
||||
item, ok := b.OriginWorkflow.Graph.Items[processingItemID]
|
||||
if !ok || item.ItemResource.Processing == nil {
|
||||
return false
|
||||
}
|
||||
index := 0
|
||||
if d, ok := exec.SelectedInstances[item.ItemResource.Processing.GetID()]; ok {
|
||||
index = d
|
||||
}
|
||||
inst := item.ItemResource.Processing.GetSelectedInstance(&index)
|
||||
if inst == nil {
|
||||
return false
|
||||
}
|
||||
procInst, ok := inst.(*resources.ProcessingInstance)
|
||||
if !ok || procInst.Access == nil || procInst.Access.Container == nil {
|
||||
// Pas de container → le step sera lui-même construit depuis la source,
|
||||
// pas de double téléchargement possible.
|
||||
return false
|
||||
}
|
||||
fullCmd := procInst.Access.Container.Command + " " + procInst.Access.Container.Args
|
||||
hasFetch := strings.Contains(fullCmd, "curl") || strings.Contains(fullCmd, "wget")
|
||||
hasURL := strings.Contains(fullCmd, sourceURL)
|
||||
return hasFetch && hasURL
|
||||
}
|
||||
|
||||
// ── Injection de la step curl ─────────────────────────────────────────────────
|
||||
func (b *ArgoBuilder) injectSourceFetchStep(
|
||||
stepBaseName string,
|
||||
sourceURL string,
|
||||
destPath string,
|
||||
isExecutable bool,
|
||||
dependsOn []string,
|
||||
) string {
|
||||
curlStepName := stepBaseName + "-src-fetch"
|
||||
|
||||
filename := sourceFilename(sourceURL)
|
||||
fullDest := destPath + "/" + filename
|
||||
|
||||
var script string
|
||||
if isExecutable {
|
||||
script = fmt.Sprintf(
|
||||
"curl -fsSL '%s' -o '%s' && chmod +x '%s'",
|
||||
sourceURL, fullDest, fullDest,
|
||||
)
|
||||
} else {
|
||||
script = fmt.Sprintf("curl -fsSL '%s' -o '%s'", sourceURL, fullDest)
|
||||
}
|
||||
|
||||
// Tâche dans le DAG.
|
||||
fetchTask := Task{
|
||||
Name: curlStepName,
|
||||
Template: curlStepName,
|
||||
Dependencies: dependsOn,
|
||||
}
|
||||
b.Workflow.getDag().Tasks = append(b.Workflow.getDag().Tasks, fetchTask)
|
||||
|
||||
// Template Argo correspondant.
|
||||
fetchTemplate := Template{
|
||||
Name: curlStepName,
|
||||
Container: Container{
|
||||
Image: curlImage,
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
Command: []string{"sh", "-c"},
|
||||
Args: []string{script},
|
||||
},
|
||||
}
|
||||
b.Workflow.Spec.Templates = append(b.Workflow.Spec.Templates, fetchTemplate)
|
||||
|
||||
logger.Info().Msg(fmt.Sprintf(
|
||||
"[source-fetch] injected curl step '%s' → %s → %s",
|
||||
curlStepName, sourceURL, fullDest,
|
||||
))
|
||||
return curlStepName
|
||||
}
|
||||
|
||||
// ── Traitement Processing source (isReachable = true) ────────────────────────
|
||||
|
||||
// handleProcessingSource gère le cas où un ProcessingInstance a une source
|
||||
// publique (access.HasSource() && access.IsReachable) sans container associé.
|
||||
//
|
||||
// Elle injecte une step curl avant la step processing dans le DAG, puis
|
||||
// modifie le template du processing pour exécuter le binaire téléchargé
|
||||
// depuis le storage lié.
|
||||
//
|
||||
// Retourne une erreur si aucun storage n'est lié (prérequis obligatoire).
|
||||
func (b *ArgoBuilder) handleProcessingSource(
|
||||
exec *workflow_execution.WorkflowExecution,
|
||||
graphID string,
|
||||
procResource *resources.ProcessingResource,
|
||||
procInst *resources.ProcessingInstance,
|
||||
argoStepName string,
|
||||
template *Template,
|
||||
) error {
|
||||
access := procInst.Access
|
||||
if !access.HasSource() || !access.Source.IsReachable {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Récupérer le storage lié à ce processing.
|
||||
related := b.OriginWorkflow.GetByRelatedProcessing(graphID, b.OriginWorkflow.Graph.IsStorage)
|
||||
if len(related) == 0 {
|
||||
return fmt.Errorf(
|
||||
"processing '%s' has source '%s' but no storage linked — cannot inject fetch step",
|
||||
procResource.GetName(), access.Source,
|
||||
)
|
||||
}
|
||||
|
||||
// On utilise le premier storage lié (cas nominal).
|
||||
var mountPath string
|
||||
for _, r := range related {
|
||||
n := r.Node
|
||||
storage := n.(*resources.StorageResource)
|
||||
if len(storage.Instances) > 0 && storage.Instances[0].Source != "" {
|
||||
mountPath = storage.Instances[0].Source
|
||||
break
|
||||
}
|
||||
}
|
||||
if mountPath == "" {
|
||||
return fmt.Errorf(
|
||||
"processing '%s': linked storage has no mount path configured",
|
||||
procResource.GetName(),
|
||||
)
|
||||
}
|
||||
|
||||
// Dépendances courantes de la step processing (pour les câbler sur la step curl).
|
||||
existingDeps := b.getArgoDependencies(exec, graphID)
|
||||
|
||||
// Injection de la step curl.
|
||||
fetchStepName := b.injectSourceFetchStep(
|
||||
argoStepName,
|
||||
access.Source.Source,
|
||||
mountPath,
|
||||
true, // binaire exécutable
|
||||
existingDeps,
|
||||
)
|
||||
|
||||
// La step processing dépend maintenant de la step curl.
|
||||
// On met à jour la tâche DAG existante.
|
||||
dag := b.Workflow.getDag()
|
||||
for i, task := range dag.Tasks {
|
||||
if task.Name == argoStepName {
|
||||
dag.Tasks[i].Dependencies = []string{fetchStepName}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Le template processing doit exécuter le binaire téléchargé.
|
||||
filename := sourceFilename(access.Source.Source)
|
||||
binaryPath := mountPath + "/" + filename
|
||||
template.Container = Container{
|
||||
Image: "alpine:latest",
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
Command: []string{"sh", "-c"},
|
||||
Args: []string{binaryPath},
|
||||
}
|
||||
// Propagation des paramètres d'entrée/sortie du workflow.
|
||||
for _, v := range procResource.GetEnv() {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
for _, v := range b.OriginWorkflow.Env[graphID] {
|
||||
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Traitement Data source (isReachable = true) ───────────────────────────────
|
||||
|
||||
// HandleDataSources parcourt tous les items Data du graphe dont une instance
|
||||
// expose une source publique (access.HasSource() && access.IsReachable) et
|
||||
// injecte pour chacun une step curl de téléchargement dans le storage lié.
|
||||
//
|
||||
// Les sources privées (isReachable=false) sont gérées par HandlePrivateDataSources
|
||||
// (Phase 4), appelée en fin de cette fonction.
|
||||
//
|
||||
// Garde : si la step processing aval contient déjà un curl ciblant la même URL,
|
||||
// on saute l'injection pour ce processing.
|
||||
//
|
||||
// Cette fonction est appelée depuis createTemplates() après la boucle principale.
|
||||
func (b *ArgoBuilder) HandleDataSources(exec *workflow_execution.WorkflowExecution, namespace string) {
|
||||
for itemID, item := range b.OriginWorkflow.Graph.Items {
|
||||
if !b.OriginWorkflow.Graph.IsData(item) || item.ItemResource.Data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Chercher une instance avec source PUBLIQUE (isReachable=true).
|
||||
// Les sources privées sont traitées par HandlePrivateDataSources.
|
||||
var sourceURL string
|
||||
var mountPath string
|
||||
|
||||
for _, inst := range item.ItemResource.Data.Instances {
|
||||
if inst == nil || !inst.Access.HasSource() || !inst.Access.Source.IsReachable {
|
||||
continue
|
||||
}
|
||||
sourceURL = inst.Access.Source.Source
|
||||
break
|
||||
}
|
||||
if sourceURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Storage lié à cette Data (ValidateIntegrity garantit qu'il en existe un).
|
||||
linkedStorageIDs := b.OriginWorkflow.Graph.GetLinkedStorageForData(itemID)
|
||||
if len(linkedStorageIDs) == 0 {
|
||||
logger.Error().Msg(fmt.Sprintf(
|
||||
"[source-fetch] data '%s' has source but no storage linked — skipping",
|
||||
item.ItemResource.Data.GetName(),
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
storageItemID := linkedStorageIDs[0]
|
||||
storageItem, ok := b.OriginWorkflow.Graph.Items[storageItemID]
|
||||
if !ok || storageItem.ItemResource.Storage == nil || len(storageItem.ItemResource.Storage.Instances) == 0 {
|
||||
continue
|
||||
}
|
||||
mountPath = storageItem.ItemResource.Storage.Instances[0].Source
|
||||
if mountPath == "" {
|
||||
logger.Error().Msg(fmt.Sprintf(
|
||||
"[source-fetch] storage linked to data '%s' has no mount path — skipping",
|
||||
item.ItemResource.Data.GetName(),
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
// Trouver tous les processings qui lisent depuis ce storage.
|
||||
downstreamProcIDs := b.processingsThatReadStorage(storageItemID)
|
||||
|
||||
// Pour chaque processing aval, appliquer la garde puis injecter si nécessaire.
|
||||
for _, procItemID := range downstreamProcIDs {
|
||||
if b.sourceAlreadyFetchedByStep(exec, procItemID, sourceURL) {
|
||||
logger.Info().Msg(fmt.Sprintf(
|
||||
"[source-fetch] data '%s': downstream processing '%s' already curls source — skipping injection",
|
||||
item.ItemResource.Data.GetName(), procItemID,
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
procItem := b.OriginWorkflow.Graph.Items[procItemID]
|
||||
if procItem.ItemResource.Processing == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Dépendances courantes de la step processing aval.
|
||||
existingDeps := b.getArgoDependencies(exec, procItemID)
|
||||
|
||||
// Nom de la step curl : basé sur le nom de la Data + storage.
|
||||
fetchBaseName := strings.ToLower(strings.ReplaceAll(item.ItemResource.Data.GetName(), " ", "-")) +
|
||||
"-" + strings.ToLower(strings.ReplaceAll(storageItem.ItemResource.Storage.GetName(), " ", "-"))
|
||||
|
||||
fetchStepName := b.injectSourceFetchStep(
|
||||
fetchBaseName,
|
||||
sourceURL,
|
||||
mountPath,
|
||||
false, // donnée, pas un binaire
|
||||
existingDeps,
|
||||
)
|
||||
|
||||
// Ajouter la step curl comme dépendance de CHAQUE instance (peer) du processing aval.
|
||||
dag := b.Workflow.getDag()
|
||||
for _, pb := range getAllPeersForItem(exec, procItemID) {
|
||||
procArgoName := getArgoName(procItem.ItemResource.Processing.GetName(), pb.BookingID)
|
||||
for i, task := range dag.Tasks {
|
||||
if task.Name == procArgoName {
|
||||
// Remplacer les dépendances existantes par [fetchStepName].
|
||||
// Les anciennes dépendances sont déjà portées par la step curl.
|
||||
dag.Tasks[i].Dependencies = []string{fetchStepName}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4 — sources privées (isReachable=false).
|
||||
b.HandlePrivateDataSources(exec, namespace)
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// sourceFilename extrait le nom de fichier depuis une URL source.
|
||||
// Fallback : "source-binary" si l'URL n'a pas de chemin exploitable.
|
||||
func sourceFilename(sourceURL string) string {
|
||||
u, err := url.Parse(sourceURL)
|
||||
if err == nil && u.Path != "" {
|
||||
if base := path.Base(u.Path); base != "." && base != "/" {
|
||||
return base
|
||||
}
|
||||
}
|
||||
return "source-binary"
|
||||
}
|
||||
|
||||
// processingsThatReadStorage retourne les IDs des items Processing
|
||||
// connectés (via un lien quelconque) au storage identifié par storageItemID.
|
||||
func (b *ArgoBuilder) processingsThatReadStorage(storageItemID string) []string {
|
||||
var result []string
|
||||
for _, link := range b.OriginWorkflow.Graph.Links {
|
||||
var otherID string
|
||||
if link.Source.ID == storageItemID {
|
||||
otherID = link.Destination.ID
|
||||
} else if link.Destination.ID == storageItemID {
|
||||
otherID = link.Source.ID
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if other, ok := b.OriginWorkflow.Graph.Items[otherID]; ok && b.OriginWorkflow.Graph.IsProcessing(other) {
|
||||
result = append(result, otherID)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
package workflow_builder
|
||||
|
||||
// source_private.go — Phase 4 : sources privées (isReachable = false)
|
||||
//
|
||||
// Pour les ressources (Processing ou Data) dont la source n'est pas
|
||||
// directement accessible (access.IsReachable == false), le protocole est :
|
||||
//
|
||||
// 1. oc-monitord publie un ARGO_KUBE_EVENT(PROCESSING_RESOURCE) sur NATS
|
||||
// avec les informations de couplage (vérification AE côté peer distant).
|
||||
//
|
||||
// 2. Le peer propriétaire valide l'AE, génère une URL pré-signée Minio
|
||||
// éphémère et répond via CONSIDERS_EVENT avec presigned_url + resource_id.
|
||||
//
|
||||
// 3. oc-monitord crée un Secret Kubernetes éphémère contenant l'URL,
|
||||
// labelisé oc-execution-id pour nettoyage post-exécution.
|
||||
//
|
||||
// 4. La step Argo injecte un wrapper sh qui :
|
||||
// • Processing binary : lit l'URL → /dev/shm/.exec → chmod+x → fork → rm → wait
|
||||
// • Data : lit l'URL → destPath/filename (pas de chmod/rm/exec)
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"oc-monitord/conf"
|
||||
. "oc-monitord/models"
|
||||
"oc-monitord/tools"
|
||||
|
||||
oclib "cloud.o-forge.io/core/oc-lib"
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
|
||||
octools "cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
// ── Demande NATS d'URL pré-signée ────────────────────────────────────────────
|
||||
|
||||
// waitForPresignedURL publie un ARGO_KUBE_EVENT(PROCESSING_RESOURCE) sur NATS
|
||||
// pour demander une URL pré-signée au peer propriétaire de la source privée,
|
||||
// puis attend la réponse via globalSourceCache.
|
||||
//
|
||||
// Résultats envoyés dans urlCh / errCh ; wg.Done() est toujours appelé.
|
||||
func waitForPresignedURL(
|
||||
executionsID string,
|
||||
event ArgoKubeEvent,
|
||||
wg *sync.WaitGroup,
|
||||
urlCh chan<- string,
|
||||
errCh chan<- error,
|
||||
) {
|
||||
defer wg.Done()
|
||||
|
||||
b, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
logger.Error().Msg("[source-private] cannot marshal ArgoKubeEvent: " + err.Error())
|
||||
urlCh <- ""
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
octools.NewNATSCaller().SetNATSPub(octools.ARGO_KUBE_EVENT, octools.NATSResponse{
|
||||
FromApp: "oc-monitord",
|
||||
Datatype: octools.PROCESSING_RESOURCE,
|
||||
User: "root",
|
||||
Method: int(octools.ARGO_KUBE_EVENT),
|
||||
Payload: b,
|
||||
})
|
||||
|
||||
key := sourceConsidersKey(executionsID, event.SourcePeerID, event.SourceResourceID)
|
||||
ch, unregister := globalSourceCache.register(key)
|
||||
defer unregister()
|
||||
|
||||
select {
|
||||
case url := <-ch:
|
||||
logger.Info().Msg(fmt.Sprintf(
|
||||
"[source-private] presigned URL received resource=%s exec=%s",
|
||||
event.SourceResourceID, executionsID,
|
||||
))
|
||||
urlCh <- url
|
||||
errCh <- nil
|
||||
case <-time.After(5 * time.Minute):
|
||||
ferr := fmt.Errorf(
|
||||
"timeout waiting for presigned URL resource=%s exec=%s",
|
||||
event.SourceResourceID, executionsID,
|
||||
)
|
||||
logger.Error().Msg(ferr.Error())
|
||||
urlCh <- ""
|
||||
errCh <- ferr
|
||||
}
|
||||
}
|
||||
|
||||
// ── Nommage du Secret ─────────────────────────────────────────────────────────
|
||||
|
||||
// secretNameFor génère un nom de Secret K8s valide (63 chars max, alphanum+-)
|
||||
// à partir d'un nom de step et d'un execution ID.
|
||||
func secretNameFor(stepBaseName, executionID string) string {
|
||||
base := strings.ToLower(strings.ReplaceAll(stepBaseName, "_", "-"))
|
||||
if len(base) > 30 {
|
||||
base = base[:30]
|
||||
}
|
||||
suffix := executionID
|
||||
if len(suffix) > 8 {
|
||||
suffix = suffix[:8]
|
||||
}
|
||||
name := "oc-src-" + base + "-" + suffix
|
||||
|
||||
// Éliminer les caractères non autorisés par K8s (alphanum + '-').
|
||||
var clean strings.Builder
|
||||
for _, c := range name {
|
||||
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' {
|
||||
clean.WriteRune(c)
|
||||
}
|
||||
}
|
||||
s := strings.Trim(clean.String(), "-")
|
||||
if len(s) > 63 {
|
||||
s = s[:63]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ── Injection step Processing (source privée) ─────────────────────────────────
|
||||
|
||||
// injectPrivateProcessingStep ajoute dans le DAG et dans les templates Argo une
|
||||
// step wrapper pour un Processing à source privée.
|
||||
//
|
||||
// Le script sh :
|
||||
// 1. Lit l'URL pré-signée depuis le Secret monté à /var/oc-secrets/presigned-url
|
||||
// 2. Télécharge le binaire dans /dev/shm/.exec (RAM — jamais sur disque)
|
||||
// 3. Le rend exécutable, le lance en background, supprime le fichier, attend la fin
|
||||
//
|
||||
// Le Secret est déclaré dans spec.volumes (ExistingVolume) et monté en lecture
|
||||
// seule dans le container.
|
||||
//
|
||||
// Retourne le nom Argo de la step créée.
|
||||
func (b *ArgoBuilder) injectPrivateProcessingStep(
|
||||
stepBaseName string,
|
||||
secretName string,
|
||||
dependsOn []string,
|
||||
) string {
|
||||
stepName := stepBaseName + "-prv-fetch"
|
||||
volName := strings.ReplaceAll(secretName, ".", "-")
|
||||
|
||||
script := `PRESIGNED=$(cat /var/oc-secrets/presigned-url)
|
||||
curl -fsSL "$PRESIGNED" -o /dev/shm/.exec
|
||||
chmod +x /dev/shm/.exec
|
||||
/dev/shm/.exec &
|
||||
PID=$!
|
||||
rm -f /dev/shm/.exec
|
||||
wait $PID`
|
||||
|
||||
// Volume Secret dans le spec du workflow (partagé entre toutes les steps).
|
||||
b.addSecretVolumeIfAbsent(volName, secretName)
|
||||
|
||||
fetchTask := Task{
|
||||
Name: stepName,
|
||||
Template: stepName,
|
||||
Dependencies: dependsOn,
|
||||
}
|
||||
b.Workflow.getDag().Tasks = append(b.Workflow.getDag().Tasks, fetchTask)
|
||||
|
||||
fetchTemplate := Template{
|
||||
Name: stepName,
|
||||
Container: Container{
|
||||
Image: curlImage,
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
Command: []string{"sh", "-c"},
|
||||
Args: []string{script},
|
||||
VolumeMounts: []VolumeMount{
|
||||
{Name: volName, MountPath: "/var/oc-secrets", ReadOnly: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
b.Workflow.Spec.Templates = append(b.Workflow.Spec.Templates, fetchTemplate)
|
||||
|
||||
logger.Info().Msg(fmt.Sprintf(
|
||||
"[source-private] injected private processing step '%s' (secret=%s)",
|
||||
stepName, secretName,
|
||||
))
|
||||
return stepName
|
||||
}
|
||||
|
||||
// ── Injection step Data (source privée) ──────────────────────────────────────
|
||||
|
||||
// injectPrivateDataStep ajoute dans le DAG une step de téléchargement sécurisé
|
||||
// pour une Data à source privée vers le storage lié (pas de /dev/shm, pas de rm).
|
||||
func (b *ArgoBuilder) injectPrivateDataStep(
|
||||
stepBaseName string,
|
||||
secretName string,
|
||||
destPath string,
|
||||
filename string,
|
||||
dependsOn []string,
|
||||
) string {
|
||||
stepName := stepBaseName + "-prv-fetch"
|
||||
volName := strings.ReplaceAll(secretName, ".", "-")
|
||||
fullDest := destPath + "/" + filename
|
||||
|
||||
script := fmt.Sprintf(
|
||||
"PRESIGNED=$(cat /var/oc-secrets/presigned-url)\ncurl -fsSL \"$PRESIGNED\" -o '%s'",
|
||||
fullDest,
|
||||
)
|
||||
|
||||
b.addSecretVolumeIfAbsent(volName, secretName)
|
||||
|
||||
fetchTask := Task{
|
||||
Name: stepName,
|
||||
Template: stepName,
|
||||
Dependencies: dependsOn,
|
||||
}
|
||||
b.Workflow.getDag().Tasks = append(b.Workflow.getDag().Tasks, fetchTask)
|
||||
|
||||
fetchTemplate := Template{
|
||||
Name: stepName,
|
||||
Container: Container{
|
||||
Image: curlImage,
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
Command: []string{"sh", "-c"},
|
||||
Args: []string{script},
|
||||
VolumeMounts: []VolumeMount{
|
||||
{Name: volName, MountPath: "/var/oc-secrets", ReadOnly: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
b.Workflow.Spec.Templates = append(b.Workflow.Spec.Templates, fetchTemplate)
|
||||
|
||||
logger.Info().Msg(fmt.Sprintf(
|
||||
"[source-private] injected private data step '%s' → %s (secret=%s)",
|
||||
stepName, fullDest, secretName,
|
||||
))
|
||||
return stepName
|
||||
}
|
||||
|
||||
// addSecretVolumeIfAbsent déclare un volume de type Secret dans spec.volumes
|
||||
// uniquement s'il n'est pas déjà présent (déduplication par nom).
|
||||
func (b *ArgoBuilder) addSecretVolumeIfAbsent(volName, secretName string) {
|
||||
for _, v := range b.Workflow.Spec.ExistingVolumes {
|
||||
if v.Name == volName {
|
||||
return
|
||||
}
|
||||
}
|
||||
b.Workflow.Spec.ExistingVolumes = append(b.Workflow.Spec.ExistingVolumes, ExistingVolume{
|
||||
Name: volName,
|
||||
Secret: &SecretRef{SecretName: secretName},
|
||||
})
|
||||
}
|
||||
|
||||
// ── handlePrivateProcessingSource ────────────────────────────────────────────
|
||||
|
||||
// handlePrivateProcessingSource gère le cas où un ProcessingInstance a une source
|
||||
// privée (access.HasSource() && !access.IsReachable).
|
||||
//
|
||||
// Orchestration :
|
||||
// 1. Demande de l'URL pré-signée via NATS (waitForPresignedURL)
|
||||
// 2. Création du Secret K8s éphémère
|
||||
// 3. Injection de la step wrapper dans le DAG
|
||||
// 4. Recâblage des dépendances (processing dépend du step wrapper)
|
||||
func (b *ArgoBuilder) handlePrivateProcessingSource(
|
||||
exec *workflow_execution.WorkflowExecution,
|
||||
graphID string,
|
||||
procResource *resources.ProcessingResource,
|
||||
procInst *resources.ProcessingInstance,
|
||||
argoStepName string,
|
||||
namespace string,
|
||||
) error {
|
||||
access := procInst.Access
|
||||
if !access.HasSource() || access.Source.IsReachable {
|
||||
return nil
|
||||
}
|
||||
|
||||
self, err := oclib.GetMySelf()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[source-private] cannot get local peer ID: %w", err)
|
||||
}
|
||||
|
||||
// Demande de l'URL pré-signée au peer propriétaire.
|
||||
var wg sync.WaitGroup
|
||||
urlCh := make(chan string, 1)
|
||||
errCh := make(chan error, 1)
|
||||
wg.Add(1)
|
||||
go waitForPresignedURL(exec.ExecutionsID, ArgoKubeEvent{
|
||||
ExecutionsID: exec.ExecutionsID,
|
||||
Type: octools.PROCESSING_RESOURCE,
|
||||
SourcePeerID: procResource.GetCreatorID(),
|
||||
DestPeerID: self.GetID(),
|
||||
OriginID: conf.GetConfig().PeerID,
|
||||
SourceResourceID: procResource.GetID(),
|
||||
}, &wg, urlCh, errCh)
|
||||
wg.Wait()
|
||||
close(urlCh)
|
||||
close(errCh)
|
||||
|
||||
presignedURL := <-urlCh
|
||||
if ferr := <-errCh; ferr != nil || presignedURL == "" {
|
||||
if ferr == nil {
|
||||
ferr = fmt.Errorf("empty presigned URL for processing '%s'", procResource.GetName())
|
||||
}
|
||||
return ferr
|
||||
}
|
||||
|
||||
// Création du Secret K8s contenant l'URL.
|
||||
secretName := secretNameFor(argoStepName, exec.ExecutionsID)
|
||||
kube, kerr := tools.NewKubernetesTool()
|
||||
if kerr != nil {
|
||||
return fmt.Errorf("[source-private] cannot create K8s client: %w", kerr)
|
||||
}
|
||||
if kerr = kube.CreateSourceSecret(secretName, presignedURL, exec.ExecutionsID, namespace); kerr != nil {
|
||||
return fmt.Errorf("[source-private] CreateSourceSecret: %w", kerr)
|
||||
}
|
||||
|
||||
// Dépendances actuelles de la step processing (seront portées par le step wrapper).
|
||||
existingDeps := b.getArgoDependencies(exec, graphID)
|
||||
|
||||
// Injection de la step wrapper.
|
||||
fetchStepName := b.injectPrivateProcessingStep(argoStepName, secretName, existingDeps)
|
||||
|
||||
// Recâblage : la step processing ne dépend plus que du step wrapper.
|
||||
dag := b.Workflow.getDag()
|
||||
for i, task := range dag.Tasks {
|
||||
if task.Name == argoStepName {
|
||||
dag.Tasks[i].Dependencies = []string{fetchStepName}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info().Msg(fmt.Sprintf(
|
||||
"[source-private] processing '%s' wired: %v → %s → %s",
|
||||
procResource.GetName(), existingDeps, fetchStepName, argoStepName,
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── HandlePrivateDataSources ─────────────────────────────────────────────────
|
||||
|
||||
// HandlePrivateDataSources parcourt tous les items Data dont une instance a
|
||||
// une source privée (access.HasSource() && !access.IsReachable) et injecte
|
||||
// pour chacun une step de téléchargement sécurisé dans le storage lié.
|
||||
//
|
||||
// Appelé depuis HandleDataSources() en fin de createTemplates().
|
||||
func (b *ArgoBuilder) HandlePrivateDataSources(
|
||||
exec *workflow_execution.WorkflowExecution,
|
||||
namespace string,
|
||||
) {
|
||||
self, err := oclib.GetMySelf()
|
||||
if err != nil {
|
||||
logger.Error().Msg("[source-private] cannot get local peer ID: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for itemID, item := range b.OriginWorkflow.Graph.Items {
|
||||
if !b.OriginWorkflow.Graph.IsData(item) || item.ItemResource.Data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var sourceURL string
|
||||
var resourceID string
|
||||
var creatorID string
|
||||
|
||||
for _, inst := range item.ItemResource.Data.Instances {
|
||||
if inst == nil || !inst.Access.HasSource() || inst.Access.Source.IsReachable {
|
||||
continue
|
||||
}
|
||||
sourceURL = inst.Access.Source.Source
|
||||
resourceID = item.ItemResource.Data.GetID()
|
||||
creatorID = item.ItemResource.Data.GetCreatorID()
|
||||
break
|
||||
}
|
||||
if sourceURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Storage lié à cette Data.
|
||||
linkedStorageIDs := b.OriginWorkflow.Graph.GetLinkedStorageForData(itemID)
|
||||
if len(linkedStorageIDs) == 0 {
|
||||
logger.Error().Msg(fmt.Sprintf(
|
||||
"[source-private] data '%s' has private source but no storage linked — skipping",
|
||||
item.ItemResource.Data.GetName(),
|
||||
))
|
||||
continue
|
||||
}
|
||||
storageItem, ok := b.OriginWorkflow.Graph.Items[linkedStorageIDs[0]]
|
||||
if !ok || storageItem.ItemResource.Storage == nil || len(storageItem.ItemResource.Storage.Instances) == 0 {
|
||||
continue
|
||||
}
|
||||
mountPath := storageItem.ItemResource.Storage.Instances[0].Source
|
||||
if mountPath == "" {
|
||||
logger.Error().Msg(fmt.Sprintf(
|
||||
"[source-private] storage linked to data '%s' has no mount path — skipping",
|
||||
item.ItemResource.Data.GetName(),
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
// Demande de l'URL pré-signée.
|
||||
var wg sync.WaitGroup
|
||||
urlCh := make(chan string, 1)
|
||||
errCh := make(chan error, 1)
|
||||
wg.Add(1)
|
||||
go waitForPresignedURL(exec.ExecutionsID, ArgoKubeEvent{
|
||||
ExecutionsID: exec.ExecutionsID,
|
||||
Type: octools.PROCESSING_RESOURCE,
|
||||
SourcePeerID: creatorID,
|
||||
DestPeerID: self.GetID(),
|
||||
OriginID: conf.GetConfig().PeerID,
|
||||
SourceResourceID: resourceID,
|
||||
}, &wg, urlCh, errCh)
|
||||
wg.Wait()
|
||||
close(urlCh)
|
||||
close(errCh)
|
||||
|
||||
presignedURL := <-urlCh
|
||||
if ferr := <-errCh; ferr != nil || presignedURL == "" {
|
||||
logger.Error().Msg(fmt.Sprintf(
|
||||
"[source-private] data '%s': failed to get presigned URL: %v",
|
||||
item.ItemResource.Data.GetName(), ferr,
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
// Création du Secret K8s.
|
||||
fetchBaseName := strings.ToLower(strings.ReplaceAll(item.ItemResource.Data.GetName(), " ", "-"))
|
||||
secretName := secretNameFor(fetchBaseName, exec.ExecutionsID)
|
||||
kube, kerr := tools.NewKubernetesTool()
|
||||
if kerr != nil {
|
||||
logger.Error().Msg("[source-private] cannot create K8s client: " + kerr.Error())
|
||||
continue
|
||||
}
|
||||
if kerr = kube.CreateSourceSecret(secretName, presignedURL, exec.ExecutionsID, namespace); kerr != nil {
|
||||
logger.Error().Msg("[source-private] cannot create source secret: " + kerr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Injection pour chaque processing aval lisant ce storage.
|
||||
downstreamProcIDs := b.processingsThatReadStorage(linkedStorageIDs[0])
|
||||
filename := sourceFilename(sourceURL)
|
||||
|
||||
for _, procItemID := range downstreamProcIDs {
|
||||
procItem := b.OriginWorkflow.Graph.Items[procItemID]
|
||||
if procItem.ItemResource.Processing == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
existingDeps := b.getArgoDependencies(exec, procItemID)
|
||||
fetchStepName := b.injectPrivateDataStep(
|
||||
fetchBaseName, secretName, mountPath, filename, existingDeps,
|
||||
)
|
||||
|
||||
// Recâblage des steps processing aval.
|
||||
dag := b.Workflow.getDag()
|
||||
for _, pb := range getAllPeersForItem(exec, procItemID) {
|
||||
procArgoName := getArgoName(procItem.ItemResource.Processing.GetName(), pb.BookingID)
|
||||
for i, task := range dag.Tasks {
|
||||
if task.Name == procArgoName {
|
||||
dag.Tasks[i].Dependencies = []string{fetchStepName}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user