4 Commits

Author SHA1 Message Date
mr 04ab15cb09 test 2025-06-24 15:31:45 +02:00
mr 2b002152a4 add resource use 2025-06-18 07:09:58 +02:00
mr 7fa115c5e1 correct error 2025-06-12 14:02:07 +02:00
mr 91f421af1e Refactor + Multi admiralty test 2025-06-12 14:01:16 +02:00
30 changed files with 1157 additions and 3935 deletions
-2
View File
@@ -22,5 +22,3 @@
go.work go.work
argo_workflows/* argo_workflows/*
env.env
oc-monitord
+11 -8
View File
@@ -3,8 +3,6 @@
build: clean build: clean
go build . go build .
dev: build
run: run:
./oc-monitord ./oc-monitord
@@ -12,16 +10,21 @@ clean:
rm -rf oc-monitord rm -rf oc-monitord
docker: docker:
DOCKER_BUILDKIT=1 docker build -t oc-monitord -f Dockerfile . DOCKER_BUILDKIT=1 docker build -t oc/oc-monitord:0.0.1 -f Dockerfile . --build-arg=HOST=$(HOST)
docker tag oc-monitord opencloudregistry/oc-monitord:latest 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: 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: publish-registry:
docker push opencloudregistry/oc-monitord:latest @echo "TODO"
all: docker publish-kind docker-deploy:
docker compose up -d
ci: docker publish-registry run-docker: docker publish-kind publish-registry docker-deploy
all: docker publish-kind publish-registry
.PHONY: build run clean docker publish-kind publish-registry .PHONY: build run clean docker publish-kind publish-registry
+6 -29
View File
@@ -1,15 +1,15 @@
package conf package conf
import ( import "sync"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/config"
)
type Config struct { type Config struct {
MongoURL string
Database string
LokiURL string
NatsURL string
ExecutionID string ExecutionID string
PeerID string PeerID string
Groups []string
Timeout int Timeout int
WorkflowID string WorkflowID string
Logs string Logs string
@@ -20,29 +20,6 @@ type Config struct {
KubeCert string KubeCert string
KubeData string KubeData string
ArgoHost string // when executed in a container will replace addresses with "localhost" in their url 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 var instance *Config
+3
View File
@@ -0,0 +1,3 @@
{
"oc-catalog": "https://oc-catalog:8087"
}
+3
View File
@@ -0,0 +1,3 @@
{
"oc-catalog": "https://localhost:8087"
}
+3 -3
View File
@@ -1,4 +1,4 @@
KUBERNETES_SERVICE_HOST=192.168.1.169 KUBERNETES_SERVICE_HOST=192.168.1.169
KUBE_CA="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTnpReU56STVNVEF3SGhjTk1qWXdNekl6TVRNek5URXdXaGNOTXpZd016SXdNVE16TlRFdwpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTnpReU56STVNVEF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSSGpYRDVpbnRIYWZWSk5VaDFlRnIxcXBKdFlkUmc5NStKVENEa0tadTIKYjUxRXlKaG1zanRIY3BDUndGL1VGMzlvdzY4TFBUcjBxaUorUHlhQTBLZUtvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTdWQkNzZVN3ajJ2cmczMFE5UG8vCnV6ZzAvMjR3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUlEOVY2aFlUSS83ZW1hRzU0dDdDWVU3TXFSdDdESUkKNlgvSUwrQ0RLbzlNQWlCdlFEMGJmT0tVWDc4UmRGdUplcEhEdWFUMUExaGkxcWdIUGduM1dZdDBxUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" KUBE_CA="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTWpNeE1USXdNell3SGhjTk1qUXdPREE0TVRBeE16VTJXaGNOTXpRd09EQTJNVEF4TXpVMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTWpNeE1USXdNell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTVlk3ZHZhNEdYTVdkMy9jMlhLN3JLYjlnWXgyNSthaEE0NmkyNVBkSFAKRktQL2UxSVMyWVF0dzNYZW1TTUQxaStZdzJSaVppNUQrSVZUamNtNHdhcnFvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWtlUVJpNFJiODduME5yRnZaWjZHClc2SU55NnN3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnRXA5ck04WmdNclRZSHYxZjNzOW5DZXZZeWVVa3lZUk4KWjUzazdoaytJS1FDSVFDbk05TnVGKzlTakIzNDFacGZ5ays2NEpWdkpSM3BhcmVaejdMd2lhNm9kdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
KUBE_CERT="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJUU5KbFNJQUJPMDR3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOemMwTWpjeU9URXdNQjRYRFRJMk1ETXlNekV6TXpVeE1Gb1hEVEkzTURNeQpNekV6TXpVeE1Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMY3Uwb2pUbVg4RFhTQkYKSHZwZDZNVEoyTHdXc1lRTmdZVURXRDhTVERIUWlCczlMZ0x5ZTdOMEFvZk85RkNZVW1HamhiaVd3WFVHR3dGTgpUdlRMU2lXalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUlJhRW9wQzc5NGJyTHlnR0g5SVhvbDZTSmlFREFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQWhaRUlrSWV3Y1loL1NmTFVCVjE5MW1CYTNRK0J5S2J5eTVlQmpwL3kzeWtDSUIxWTJicTVOZTNLUUU4RAprNnNzeFJrbjJmN0VoWWVRQU1pUlJ2MjIweDNLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTnpReU56STVNVEF3SGhjTk1qWXdNekl6TVRNek5URXdXaGNOTXpZd016SXdNVE16TlRFdwpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTnpReU56STVNVEF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTcTdVTC85MEc1ZmVTaE95NjI3eGFZWlM5dHhFdWFoWFQ3Vk5wZkpQSnMKaEdXd2UxOXdtbXZzdlp6dlNPUWFRSzJaMmttN0hSb1IrNlA1YjIyamczbHVvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVVXaEtLUXUvZUc2eThvQmgvU0Y2Ckpla2lZaEF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUk3cGxHczFtV20ySDErbjRobDBNTk13RmZzd0o5ZXIKTzRGVkM0QzhwRG44QWlCN3NZMVFwd2M5VkRUeGNZaGxuZzZNUzRXai85K0lHWjJxcy94UStrMjdTQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" KUBE_CERT="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJWUxWNkFPQkdrU1F3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekl6TVRFeU1ETTJNQjRYRFRJME1EZ3dPREV3TVRNMU5sb1hEVEkxTURndwpPREV3TVRNMU5sb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJGQ2Q1MFdPeWdlQ2syQzcKV2FrOWY4MVAvSkJieVRIajRWOXBsTEo0ck5HeHFtSjJOb2xROFYxdUx5RjBtOTQ2Nkc0RmRDQ2dqaXFVSk92Swp3NVRPNnd5alNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVFJkOFI5cXVWK2pjeUVmL0ovT1hQSzMyS09XekFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQTArbThqTDBJVldvUTZ0dnB4cFo4NVlMalF1SmpwdXM0aDdnSXRxS3NmUVVDSUI2M2ZNdzFBMm5OVWU1TgpIUGZOcEQwSEtwcVN0Wnk4djIyVzliYlJUNklZCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTWpNeE1USXdNell3SGhjTk1qUXdPREE0TVRBeE16VTJXaGNOTXpRd09EQTJNVEF4TXpVMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTWpNeE1USXdNell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRc3hXWk9pbnIrcVp4TmFEQjVGMGsvTDF5cE01VHAxOFRaeU92ektJazQKRTFsZWVqUm9STW0zNmhPeVljbnN3d3JoNnhSUnBpMW5RdGhyMzg0S0Z6MlBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBYZkVmYXJsZm8zTWhIL3lmemx6Cnl0OWlqbHN3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQUxJL2dNYnNMT3MvUUpJa3U2WHVpRVMwTEE2cEJHMXgKcnBlTnpGdlZOekZsQWlFQW1wdjBubjZqN3M0MVI0QzFNMEpSL0djNE53MHdldlFmZWdEVGF1R2p3cFk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
KUBE_DATA="LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUROZDRnWXd6aVRhK1hwNnFtNVc3SHFzc1JJNkREaUJTbUV2ZHoxZzk3VGxvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFdHk3U2lOT1pmd05kSUVVZStsM294TW5ZdkJheGhBMkJoUU5ZUHhKTU1kQ0lHejB1QXZKNwpzM1FDaDg3MFVKaFNZYU9GdUpiQmRRWWJBVTFPOU10S0pRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=" KUBE_DATA="LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU5ZS1BFb1dhd1NKUzJlRW5oWmlYMk5VZlY1ZlhKV2krSVNnV09TNFE5VTlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUozblJZN0tCNEtUWUx0WnFUMS96VS84a0Z2Sk1lUGhYMm1Vc25pczBiR3FZblkyaVZEeApYVzR2SVhTYjNqcm9iZ1YwSUtDT0twUWs2OHJEbE03ckRBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
+42 -41
View File
@@ -1,34 +1,30 @@
module oc-monitord module oc-monitord
go 1.25.0 go 1.23.1
toolchain go1.23.3
require ( require (
cloud.o-forge.io/core/oc-lib v0.0.0-20260527135023-cef23b5f307b cloud.o-forge.io/core/oc-lib v0.0.0-20250624102227-e600fedcab06
github.com/akamensky/argparse v1.4.0 github.com/akamensky/argparse v1.4.0
github.com/google/uuid v1.6.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/nwtgck/go-fakelish v0.1.3
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/beego/beego/v2 v2.3.8 // indirect github.com/beego/beego/v2 v2.3.7 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/golang/protobuf v1.5.4 // 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/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/sirupsen/logrus v1.9.3 // indirect
github.com/ugorji/go/codec v1.1.7 // 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 v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api 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/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.63.0 // 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 ( require (
@@ -37,10 +33,10 @@ require (
github.com/biter777/countries v1.7.5 // indirect github.com/biter777/countries v1.7.5 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.23.0 // 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/go-playground/universal-translator v0.18.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v1.0.0 // 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/hashicorp/golang-lru v1.0.2 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // 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/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.44.0 github.com/nats-io/nats.go v1.41.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.10 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.0 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/procfs v0.17.0 // 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/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/x448/float16 v0.8.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/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect go.mongodb.org/mongo-driver v1.17.3 // indirect
golang.org/x/crypto v0.44.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.37.0 // indirect golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.9.0 // indirect golang.org/x/time v0.7.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/api v0.35.1 k8s.io/api v0.32.1
k8s.io/apimachinery v0.35.1 k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.35.1 k8s.io/client-go v0.32.1
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
) )
+107 -158
View File
@@ -1,44 +1,36 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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.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-20250217072519-cafadec1469f h1:esLB0EAn8IuOChW35kcBrPaN80z4A4yYyz1mXT45GQo=
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-20250217072519-cafadec1469f/go.mod h1:2roQbUpv3a6mTIr5oU1ux31WbN8YucyyQvCQ0FqwbcE=
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-20250313155727-88c88cac5bc9 h1:mSFFPwil5Ih+RPBvn88MBerQMtsoHnOuyCZQaf91a34=
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-20250313155727-88c88cac5bc9/go.mod h1:2roQbUpv3a6mTIr5oU1ux31WbN8YucyyQvCQ0FqwbcE=
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-20250612084738-2a0ab8e54963 h1:ADDfqwtWF+VQTMSNAWPuhc4mmiKdgpHNmBB+UI2jRPE=
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-20250612084738-2a0ab8e54963/go.mod h1:2roQbUpv3a6mTIr5oU1ux31WbN8YucyyQvCQ0FqwbcE=
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-20250617130633-8f2adb76e41c h1:k2y+ocElqwUK5yzyCf3rWrDUzPWbds4MbtG58+Szos0=
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-20250617130633-8f2adb76e41c/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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-20250617133502-9e5266326157 h1:853UvpMOM1QuWLrr/V8biDS8IcQcqHvoJsOT4epxDng=
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-20250617133502-9e5266326157/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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-20250617141444-0b0952b28c7e h1:Z5vLv+Wzzz58abmHRnovoqbkVlKHuC8u8/RLv7FjtZw=
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-20250617141444-0b0952b28c7e/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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-20250617144221-ec7a7e474637 h1:YiZbn6KmjgZ62uM+kH95Snd2nQliDKDnGMAxRr/VoUw=
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-20250617144221-ec7a7e474637/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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-20250624064953-2c8dcbe93d14 h1:iCTrYc2+W2BFLOupRK1sD6sOgsK4NIs6WMC+4LiWCaY=
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-20250624064953-2c8dcbe93d14/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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-20250624093207-3fdf5c3ebf29 h1:JitS1izRltTyOaWnvXnmYywHj0napsL6y0nBYiWUCNo=
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-20250624093207-3fdf5c3ebf29/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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-20250624095852-147c7bc3a1d5 h1:0eV0E3kBZkOyoAurRmP9h4eHmFrZajOxSqoBgM3l3dk=
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-20250624095852-147c7bc3a1d5/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
cloud.o-forge.io/core/oc-lib v0.0.0-20260414104622-dc0041999d22 h1:lum7G12vCKYKQWXTOYtl2Qh9hLRlzrcOPO3pozUBL40= cloud.o-forge.io/core/oc-lib v0.0.0-20250624102227-e600fedcab06 h1:+RSv62uIC7wsmibsp1XTanQMNznNeOGgPpfhb6ZHT4c=
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-20250624102227-e600fedcab06/go.mod h1:vHWauJsS6ryf7UDqq8hRXoYD5RsONxcFTxeZPOztEuI=
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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= 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/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 h1:5+Cc1UwaQE5ka3w7R3hxZ1TK3M6VjDEXA5WSQ/IXrxY=
github.com/argoproj/argo-workflows/v3 v3.6.4/go.mod h1:2f5zB8CkbNCCO1od+kd1dWkVokqcuyvu+tc+Jwx1MZg= 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/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.7 h1:z4btKtjU/rfp5BiYHkGD2QPjK9i1E9GH+I7vfhn6Agk=
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg= 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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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= github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
@@ -59,27 +51,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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 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.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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.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.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/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/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/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/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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 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/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.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 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= github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
@@ -92,8 +80,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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 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= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -108,15 +96,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/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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.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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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.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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -131,8 +122,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/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/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -144,8 +133,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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -154,10 +141,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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/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= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -169,8 +152,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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -180,61 +161,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/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 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.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.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 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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.41.0 h1:PzxEva7fflkd+n87OtQTXqCTyLfIIMFJBpyccHLE2Ko=
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.41.0/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= 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 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 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 h1:bA8/xa9hQmzppexIhBvdmztcd/PJ4SPuAUTBdMKZ8G4=
github.com/nwtgck/go-fakelish v0.1.3/go.mod h1:2HC44/OwVWwOa/g3+P2jUM3FEHQ0ya4gyCSU19PPd3Y= 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/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 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.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 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.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 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/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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= 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.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.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 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/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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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/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 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
@@ -249,24 +216,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 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 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 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/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/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.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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 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 v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
@@ -285,33 +248,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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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= 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.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver v1.17.3/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=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-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-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-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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= 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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -324,12 +277,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-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-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.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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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-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.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -337,8 +290,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-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-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.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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -354,20 +307,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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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-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.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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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.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.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -378,8 +331,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-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.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.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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -402,13 +355,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.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= 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/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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 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 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 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= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -420,25 +373,21 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/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.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= 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 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
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=
+33 -391
View File
@@ -11,9 +11,8 @@ import (
"sync" "sync"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
octools "cloud.o-forge.io/core/oc-lib/tools"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"k8s.io/apimachinery/pkg/watch"
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 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{ return &ArgoLogs{
Name: "oc-monitor-" + name, Name: "oc-monitor-" + name,
Namespace: namespace, 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, StepCount: 0,
StepMax: stepMax, StepMax: stepMax,
stop: false, stop: false,
@@ -94,6 +93,7 @@ func (a *ArgoLogs) StartStepRecording(current_watch *ArgoWatch, logger zerolog.L
a.Started = time.Now() a.Started = time.Now()
} }
type ArgoPodLog struct { type ArgoPodLog struct {
PodName string PodName string
Step 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. func LogKubernetesArgo(wfName string, namespace string, watcher watch.Interface) {
// 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) {
var argoWatcher *ArgoWatch var argoWatcher *ArgoWatch
var pods []string var pods []string
var node wfv1.NodeStatus
wfl := utils.GetWFLogger("") wfl := utils.GetWFLogger("")
wfl.Debug().Msg("Starting to log " + wfName) wfl.Debug().Msg("Starting to log " + wfName)
var wg sync.WaitGroup var wg sync.WaitGroup
// nodePhases tracks the last known phase of each step node so we can detect for event := range (watcher.ResultChan()) {
// 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() {
wf, ok := event.Object.(*wfv1.Workflow) wf, ok := event.Object.(*wfv1.Workflow)
if !ok { if !ok {
wfl.Error().Msg("unexpected type") wfl.Error().Msg("unexpected type")
continue continue
} }
if len(wf.Status.Nodes) == 0 { if len(wf.Status.Nodes) == 0 {
wfl.Info().Msg("No node status yet") wfl.Info().Msg("No node status yet") // The first output of the channel doesn't contain Nodes so we skip it
continue 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) conditions := retrieveCondition(wf)
// Retrieving the Status for the main node, which is named after the workflow // Retrieving the Status for the main node, which is named after the workflow
if node, ok = wf.Status.Nodes[wfName]; !ok { if node, ok = wf.Status.Nodes[wfName]; !ok {
bytified, _ := json.MarshalIndent(wf.Status.Nodes, "", "\t") bytified, _ := json.MarshalIndent(wf.Status.Nodes,"","\t")
wfl.Error().Msg("Could not find the " + wfName + " node in \n" + string(bytified)) wfl.Fatal().Msg("Could not find the " + wfName + " node in \n" + string(bytified))
continue
} }
now := time.Now().UTC() now := time.Now()
start := node.StartedAt.Time.UTC() start, _ := time.Parse(time.RFC3339, node.StartedAt.String() )
var duration time.Duration duration := now.Sub(start)
if !start.IsZero() {
duration = now.Sub(start)
}
newWatcher := ArgoWatch{ newWatcher := ArgoWatch{
Name: node.Name, Name: node.Name,
@@ -218,69 +156,18 @@ func LogKubernetesArgo(wfName string, execID string, executionsID string, namesp
argoWatcher = &newWatcher argoWatcher = &newWatcher
} }
if !newWatcher.Equals(argoWatcher) { if !newWatcher.Equals(argoWatcher){
jsonified, _ := json.Marshal(newWatcher) jsonified, _ := json.Marshal(newWatcher)
wfl.Info().Msg(string(jsonified)) wfl.Info().Msg(string(jsonified))
argoWatcher = &newWatcher argoWatcher = &newWatcher
} }
// ── Per-step completion detection ──────────────────────────────────── // I don't think we need to use WaitGroup here, because the loop itself
for _, stepNode := range wf.Status.Nodes { // acts as blocking process for the main thread, because Argo watch never closes the channel
if stepNode.Name == wfName { for _, pod := range wf.Status.Nodes{
continue // skip the main DAG node if !slices.Contains(pods,pod.Name){
}
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 ────────────────────────────────────────────────
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() 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) pl.Info().Msg("Found a new pod to log : " + pod.Name)
wg.Add(1) wg.Add(1)
go logKubernetesPods(namespace, wfName, pod.Name, pl, &wg) go logKubernetesPods(namespace, wfName, pod.Name, pl, &wg)
@@ -288,246 +175,19 @@ func LogKubernetesArgo(wfName string, execID string, executionsID string, namesp
} }
} }
// ── Workflow terminal phase ────────────────────────────────────────── // Stop listening to the chan when the Workflow is completed or something bad happened
if node.Phase.Completed() || node.Phase.FailedOrError() {
if node.Phase.Completed() { if node.Phase.Completed() {
wfl.Info().Msg(wfName + " workflow completed") wfl.Info().Msg(wfName + " worflow completed")
} else {
wfl.Error().Msg(wfName + " has failed, please refer to the logs")
wfl.Error().Msg(node.Message)
}
wg.Wait() wg.Wait()
wfl.Info().Msg(wfName + " exiting") wfl.Info().Msg(wfName + " exiting")
break
finalState := enum.SUCCESS }
if node.Phase.FailedOrError() { if node.Phase.FailedOrError() {
if !(strings.Contains(node.Message, "context cancel") || strings.Contains(node.Message, "exit")) { wfl.Error().Msg(wfName + "has failed, please refer to the logs")
fmt.Println("2 baraka", node.Message) wfl.Error().Msg(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
break 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) { func retrieveCondition(wf *wfv1.Workflow) (c Conditions) {
@@ -539,18 +199,16 @@ func retrieveCondition(wf *wfv1.Workflow) (c Conditions) {
c.Completed = cond.Status == "True" c.Completed = cond.Status == "True"
} }
} }
return 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) { func logKubernetesPods(executionId string, wfName string,podName string, logger zerolog.Logger, wg *sync.WaitGroup){
defer wg.Done() defer wg.Done()
s := strings.SplitN(podName, ".", 2) s := strings.Split(podName, ".")
if len(s) < 2 {
logger.Error().Str("pod", podName).Msg("Unexpected pod name format, expected wfName.stepName")
return
}
name := s[0] + "-" + s[1] name := s[0] + "-" + s[1]
step := s[1] step := s[1]
@@ -567,27 +225,11 @@ func logKubernetesPods(executionId string, wfName string, podName string, logger
} }
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() log := scanner.Text()
podLog := NewArgoPodLog(name, step, line) podLog := NewArgoPodLog(name,step,log)
jsonified, err := json.Marshal(podLog) jsonified, _ := 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:
logger.Info().Msg(string(jsonified)) logger.Info().Msg(string(jsonified))
} }
}
if err := scanner.Err(); err != nil {
logger.Error().Err(err).Str("pod", podName).Msg("Pod log scanner error")
}
} }
+137 -45
View File
@@ -1,23 +1,27 @@
package main package main
import ( import (
"bufio"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
"oc-monitord/conf" "oc-monitord/conf"
l "oc-monitord/logger" l "oc-monitord/logger"
"oc-monitord/models"
u "oc-monitord/utils" u "oc-monitord/utils"
"oc-monitord/workflow_builder" "oc-monitord/workflow_builder"
oclib "cloud.o-forge.io/core/oc-lib" 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/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/peer"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution" "cloud.o-forge.io/core/oc-lib/models/workflow_execution"
@@ -27,6 +31,7 @@ import (
"github.com/akamensky/argparse" "github.com/akamensky/argparse"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/goraz/onion"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@@ -42,23 +47,31 @@ var wf_logger zerolog.Logger
var parser argparse.Parser var parser argparse.Parser
var workflowName string var workflowName string
func main() { const defaultConfigFile = "/etc/oc/ocmonitord_conf.json"
parser = *argparse.NewParser("oc-monitord", "Launch the execution of a workflow given as a parameter and sends the produced logs to a loki database") const localConfigFile = "./conf/local_ocmonitord_conf.json"
loadConfig(&parser)
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")
setConf(&parser)
oclib.InitDaemon("oc-monitord") oclib.InitDaemon("oc-monitord")
// Lance l'abonné NATS centralisé pour les confirmations PB_CONSIDERS. oclib.SetConfig(
workflow_builder.StartConsidersListener() conf.GetConfig().MongoURL,
conf.GetConfig().Database,
conf.GetConfig().NatsURL,
conf.GetConfig().LokiURL,
conf.GetConfig().Logs,
)
logger = u.GetLogger() logger = u.GetLogger()
logger.Debug().Msg("Loki URL : " + config.GetConfig().LokiUrl) logger.Debug().Msg("Loki URL : " + conf.GetConfig().LokiURL)
logger.Info().Msg("Workflow executed : " + conf.GetConfig().ExecutionID) logger.Info().Msg("Workflow executed : " + conf.GetConfig().ExecutionID)
exec := u.GetExecution(conf.GetConfig().ExecutionID) exec := u.GetExecution(conf.GetConfig().ExecutionID)
if exec == nil { if exec == nil {
logger.Fatal().Msg("Could not retrieve workflow ID from execution ID " + conf.GetConfig().ExecutionID + " on peer " + conf.GetConfig().PeerID) 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 return
} }
conf.GetConfig().WorkflowID = exec.WorkflowID conf.GetConfig().WorkflowID = exec.WorkflowID
@@ -73,55 +86,41 @@ func main() {
// // create argo // // create argo
new_wf := workflow_builder.WorflowDB{} 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 { if err != nil {
logger.Error().Msg("Could not retrieve workflow " + conf.GetConfig().WorkflowID + " from oc-catalog API") 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 builder, _, err := new_wf.ExportToArgo(exec.ExecutionsID, conf.GetConfig().Timeout) // Removed stepMax so far, I don't know if we need it anymore
fmt.Println("ExportToArgo", err)
if err != nil { if err != nil {
logger.Error().Msg("Could not create the Argo file for " + conf.GetConfig().WorkflowID) logger.Error().Msg("Could not create the Argo file for " + conf.GetConfig().WorkflowID)
logger.Error().Msg(err.Error()) logger.Error().Msg(err.Error())
u.EmitExecStateUpdate(exec.GetID(), enum.FAILURE)
return
} }
fmt.Println("CompleteBuild")
argoFilePath, err := builder.CompleteBuild(exec.ExecutionsID) argoFilePath, err := builder.CompleteBuild(exec.ExecutionsID)
fmt.Println("CompleteBuild", err)
if err != nil { if err != nil {
logger.Error().Msg("Error when completing the build of the workflow: " + err.Error()) logger.Error().Msg("Error when completing the build of the workflow: " + err.Error())
u.EmitExecStateUpdate(exec.GetID(), enum.FAILURE)
return
} }
workflowName = getContainerName(argoFilePath) workflowName = getContainerName(argoFilePath)
fmt.Println("getContainerName", workflowName, conf.GetConfig().KubeHost)
if conf.GetConfig().KubeHost == "" { if conf.GetConfig().KubeHost == "" {
// Not in a k8s environment, get conf from parameters // 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 { } else {
// Executed in a k8s environment // Executed in a k8s environment
logger.Info().Msg("Executes inside a k8s") logger.Info().Msg("Executes inside a k8s")
// Wait until the scheduled start time if prep finished early. // executeInside(exec.GetID(), "argo", argo_file_path, stepMax) // commenting to use conf.ExecutionID instead of exec.GetID()
if st := conf.GetConfig().ScheduledTime; !st.IsZero() && time.Now().Before(st) { executeInside(exec.ExecutionsID, argoFilePath)
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)
} }
} }
// So far we only log the output from // So far we only log the output from
func executeInside(ns string, execID string, argo_file_path string) { func executeInside(ns string, argo_file_path string) {
t, err := tools2.NewService(conf.GetConfig().Mode) t, err := tools2.NewService(conf.GetConfig().Mode)
if err != nil { if err != nil {
logger.Error().Msg("Could not create KubernetesTool : " + err.Error()) logger.Error().Msg("Could not create KubernetesTool")
return return
} }
@@ -134,19 +133,86 @@ func executeInside(ns string, execID string, argo_file_path string) {
logger.Info().Msg(fmt.Sprint("Data :" + conf.GetConfig().KubeData)) logger.Info().Msg(fmt.Sprint("Data :" + conf.GetConfig().KubeData))
return return
} else { } 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...") logger.Info().Msg("Finished, exiting...")
} }
} }
func loadConfig(parser *argparse.Parser) { func executeOutside(argo_file_path string, workflow *models.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 setConf(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"}) 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"}) 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"}) peer := parser.String("p", "peer", &argparse.Options{Required: false, Default: "", Help: "Peer ID of the workflow to request from oc-catalog API"})
groups := parser.String("g", "groups", &argparse.Options{Required: false, Default: "", Help: "Groups of the peer 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"}) 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"}) 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"}) cert := parser.String("C", "cert", &argparse.Options{Required: false, Default: "", Help: "Cert file for the Kubernetes cluster"})
@@ -155,7 +221,6 @@ func loadConfig(parser *argparse.Parser) {
host := parser.String("H", "host", &argparse.Options{Required: false, Default: "", Help: "Host for the Kubernetes cluster"}) 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"}) 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 // 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) err := parser.Parse(os.Args)
@@ -164,15 +229,14 @@ func loadConfig(parser *argparse.Parser) {
os.Exit(1) os.Exit(1)
} }
conf.GetConfig().Logs = "debug" conf.GetConfig().Logs = "debug"
conf.GetConfig().LokiURL = *url
conf.GetConfig().MongoURL = *mongo
conf.GetConfig().Database = *db
conf.GetConfig().Timeout = *timeout conf.GetConfig().Timeout = *timeout
conf.GetConfig().Mode = *mode conf.GetConfig().Mode = *mode
conf.GetConfig().ExecutionID = *execution conf.GetConfig().ExecutionID = *execution
conf.GetConfig().PeerID = *peer conf.GetConfig().PeerID = *peer
conf.GetConfig().OCNamespace = *ocNamespace conf.GetConfig().Groups = strings.Split((*groups), ",")
if *scheduledUnix > 0 {
conf.GetConfig().ScheduledTime = time.Unix(int64(*scheduledUnix), 0)
}
conf.GetConfig().NatsUrl = *natsUrl
conf.GetConfig().KubeHost = *host conf.GetConfig().KubeHost = *host
conf.GetConfig().KubePort = *port conf.GetConfig().KubePort = *port
@@ -192,6 +256,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 { func IsValidUUID(u string) bool {
_, err := uuid.Parse(u) _, err := uuid.Parse(u)
return err == nil return err == nil
@@ -213,7 +305,7 @@ func updateStatus(status string, log string) {
wf_exec.ArgoStatusToState(status) wf_exec.ArgoStatusToState(status)
exec, _, err := workflow_execution.NewAccessor(&tools.APIRequest{ exec, _, err := workflow_execution.NewAccessor(&tools.APIRequest{
PeerID: conf.GetConfig().PeerID, PeerID: conf.GetConfig().PeerID,
}).UpdateOne(wf_exec.Serialize(wf_exec), exec_id) }).UpdateOne(wf_exec, exec_id)
if err != nil { if err != nil {
logger.Error().Msg("Could not update status for workflow execution " + exec_id + err.Error()) logger.Error().Msg("Could not update status for workflow execution " + exec_id + err.Error())
} }
-66
View File
@@ -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
+20
View File
@@ -1,5 +1,7 @@
package models package models
import "gopkg.in/yaml.v3"
type ServiceResource struct { type ServiceResource struct {
Action string `yaml:"action,omitempty"` Action string `yaml:"action,omitempty"`
SuccessCondition string `yaml:"successCondition,omitempty"` SuccessCondition string `yaml:"successCondition,omitempty"`
@@ -15,6 +17,24 @@ type Service struct {
Spec ServiceSpec `yaml:"spec"` Spec ServiceSpec `yaml:"spec"`
} }
func (s *Service) BindToArgo(workflow *Workflow) error {
service_manifest, err := yaml.Marshal(s)
if err != nil {
return err
}
service_template := Template{Name: "workflow-service-pod",
Resource: ServiceResource{
Action: "create",
SuccessCondition: "status.succeeded > 0",
FailureCondition: "status.failed > 3",
SetOwnerReference: true,
Manifest: string(service_manifest),
},
}
workflow.Spec.Templates = append(workflow.Spec.Templates, service_template)
return nil
}
type Metadata struct { type Metadata struct {
Name string `yaml:"name"` Name string `yaml:"name"`
} }
+233 -212
View File
@@ -3,15 +3,15 @@ package models
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os/exec" "strconv"
"sort"
"strings" "strings"
w "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"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"
"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 { type Parameter struct {
@@ -19,12 +19,60 @@ type Parameter struct {
Value string `yaml:"value,omitempty"` Value string `yaml:"value,omitempty"`
} }
type Bounds struct {
CPU string `yaml:"cpu,omitempty"`
Memory string `yaml:"memory,omitempty"`
GPU string `yaml:"nvidia.com/gpu,omitempty"`
}
func NewBounds() *Bounds {
return &Bounds{
CPU: "0",
Memory: "0",
GPU: "0",
}
}
func (b *Bounds) Set(value float64, what string, isMin bool) bool {
i := float64(0)
switch what {
case "cpu":
if newI, err := strconv.ParseFloat(b.CPU, 64); err == nil {
i = newI
}
case "ram":
if newI, err := strconv.ParseFloat(b.Memory, 64); err == nil {
i = newI
}
case "gpu":
if newI, err := strconv.ParseFloat(b.GPU, 64); err == nil {
i = newI
}
}
ok := (value > i && !isMin) || (value < i && isMin)
if ok {
switch what {
case "cpu":
b.CPU = fmt.Sprintf("%f", value)
return true
case "ram":
b.Memory = fmt.Sprintf("%fGi", value)
return true
case "gpu":
b.GPU = fmt.Sprintf("%f", value)
return true
}
}
return false
}
type Container struct { type Container struct {
Image string `yaml:"image"` Image string `yaml:"image"`
ImagePullPolicy string `yaml:"imagePullPolicy,omitempty"`
Command []string `yaml:"command,omitempty,flow"` Command []string `yaml:"command,omitempty,flow"`
Args []string `yaml:"args,omitempty,flow"` Args []string `yaml:"args,omitempty,flow"`
VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"` VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"`
Requests Bounds `yaml:"requests,omitempty"`
Limits Bounds `yaml:"limits,omitempty"`
} }
func (c *Container) AddVolumeMount(volumeMount VolumeMount, volumes []VolumeMount) []VolumeMount { func (c *Container) AddVolumeMount(volumeMount VolumeMount, volumes []VolumeMount) []VolumeMount {
@@ -45,27 +93,85 @@ func (c *Container) AddVolumeMount(volumeMount VolumeMount, volumes []VolumeMoun
return volumes return volumes
} }
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 { type Task struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Template string `yaml:"template"` Template string `yaml:"template"`
Dependencies []string `yaml:"dependencies,omitempty"` Dependencies []string `yaml:"dependencies,omitempty"`
NodeSelector map[string]string `yaml:"nodeSelector,omitempty"`
Arguments struct { Arguments struct {
Parameters []Parameter `yaml:"parameters,omitempty"` Parameters []Parameter `yaml:"parameters,omitempty"`
} `yaml:"arguments,omitempty"` } `yaml:"arguments,omitempty"`
} }
func NewTask(processingName string, graphItemID string) *Task {
unique_name := GetArgoName(processingName, graphItemID)
return &Task{
Name: unique_name,
Template: unique_name,
}
}
func (t *Task) BindToArgo(
dag *Dag,
graphItemID string,
originWf *w.Workflow,
processing *resources.ProcessingResource,
firstItems, lastItems []string,
) (*Dag, []string, []string) {
if instance := processing.GetSelectedInstance(); instance != nil {
t.addParams(instance.(*resources.ProcessingInstance).Env)
t.addParams(instance.(*resources.ProcessingInstance).Inputs)
t.addParams(instance.(*resources.ProcessingInstance).Outputs)
}
t.Dependencies = TransformDepsToArgo(originWf.GetDependencies(graphItemID))
name := ""
if originWf.Graph.Items[graphItemID].Processing != nil {
name = originWf.Graph.Items[graphItemID].Processing.GetName()
}
if originWf.Graph.Items[graphItemID].Workflow != nil {
name = originWf.Graph.Items[graphItemID].Workflow.GetName()
}
if len(t.Dependencies) == 0 && name != "" {
firstItems = append(firstItems, GetArgoName(name, graphItemID))
}
if deps := originWf.IsDependancy(graphItemID); len(deps) == 0 && name != "" {
lastItems = append(lastItems, GetArgoName(name, graphItemID))
}
dag.Tasks = append(dag.Tasks, *t)
return dag, firstItems, lastItems
}
func (t *Task) addParams(params []models.Param) {
for _, value := range params {
t.Arguments.Parameters = append(t.Arguments.Parameters, Parameter{
Name: value.Name,
Value: value.Value,
})
}
}
func (t *Task) GetDeps(name string) (int, string) {
for i, deps := range t.Dependencies {
if strings.Contains(deps, name) {
return i, deps
}
}
return 0, ""
}
type Dag struct { type Dag struct {
Tasks []Task `yaml:"tasks,omitempty"` Tasks []Task `yaml:"tasks,omitempty"`
} }
func (d *Dag) GetTask(taskName string) *Task {
for _, task := range d.Tasks {
if strings.Contains(task.Name, taskName) {
return &task
}
}
return nil
}
type TemplateMetadata struct { type TemplateMetadata struct {
Labels map[string]string `yaml:"labels,omitempty"` Labels map[string]string `yaml:"labels,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty"`
@@ -76,23 +182,76 @@ type Secret struct {
Key string `yaml:"key"` Key string `yaml:"key"`
} }
func NewSecret(name string, key string) *Secret {
return &Secret{Name: name, Key: key + "-key"}
}
type Key struct { type Key struct {
Key string `yaml:"key"` Key string `yaml:"key"`
Bucket string `yaml:"bucket"` Bucket string `yaml:"bucket"`
EndPoint string `yaml:"endpoint"` EndPoint string `yaml:"endpoint"`
Insecure bool `yaml:"insecure"` Insecure bool `yaml:"insecure"`
AccessKeySecret *Secret `yaml:"accessKeySecret"` AccessKeySecret *Secret `yaml accessKeySecret`
SecretKeySecret *Secret `yaml:"secretKeySecret"` SecretKeySecret *Secret `yaml secretKeySecret`
} }
type Artifact struct { type Artifact struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Path string `yaml:"path"` Path string `yaml:"path"`
S3 *Key `yaml:"s3,omitempty"`
} }
type ArtifactRepositoryRef struct { func NewArtifact(name string, rw graph.StorageProcessingGraphLink, params []models.Param, template Template) *Artifact {
ConfigMap string `yaml:"configMap,omitempty"` if rw.Write {
Key string `yaml:"key,omitempty"` name += "-" + rw.Destination + "-input-write"
} else {
name = "-" + rw.Destination + "-input-read"
}
return &Artifact{
Name: name,
Path: template.ReplacePerEnv(rw.Source, params),
}
}
func (a *Artifact) BindToArgo(storageType enum.StorageType, rw graph.StorageProcessingGraphLink, params []models.Param, template Template) {
if rw.Write {
template.Outputs.Artifacts = append(template.Inputs.Artifacts, *a)
} else {
template.Inputs.Artifacts = append(template.Outputs.Artifacts, *a)
}
}
func (a *Artifact) bindS3(rw graph.StorageProcessingGraphLink, params []models.Param, template Template) {
a.S3 = &Key{
Key: template.ReplacePerEnv(rw.Destination+"/"+rw.FileName, params),
Insecure: true, // temporary
}
/* sel := storage.GetSelectedInstance()
if sel != nil {
if sel.(*resources.StorageResourceInstance).Credentials != nil {
tool, err := tools2.NewService(conf.GetConfig().Mode)
if err != nil || tool == nil {
logger.Error().Msg("Could not create the access secret")
} else {
id, err := tool.CreateAccessSecret(namespace,
sel.(*resources.StorageResourceInstance).Credentials.Login,
sel.(*resources.StorageResourceInstance).Credentials.Pass)
if err == nil {
a.S3.AccessKeySecret = NewSecret(id, "access")
a.S3.SecretKeySecret = NewSecret(id, "secret")
}
}
}
source := sel.(*resources.StorageResourceInstance).Source
a.S3.Key = strings.ReplaceAll(strings.ReplaceAll(a.S3.Key, source+"/", ""), source, "")
splits := strings.Split(a.S3.EndPoint, "/")
if len(splits) > 1 {
a.S3.Bucket = splits[0]
a.S3.EndPoint = strings.Join(splits[1:], "/")
} else {
a.S3.Bucket = splits[0]
}
} */
} }
type InOut struct { type InOut struct {
@@ -108,230 +267,92 @@ type Template struct {
Dag *Dag `yaml:"dag,omitempty"` Dag *Dag `yaml:"dag,omitempty"`
Metadata TemplateMetadata `yaml:"metadata,omitempty"` Metadata TemplateMetadata `yaml:"metadata,omitempty"`
Resource ServiceResource `yaml:"resource,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) { func (template *Template) CreateContainer(processing *resources.ProcessingResource, dag *Dag) {
container := Container{Image: "natsio/nats-box", ImagePullPolicy: "IfNotPresent"} instance := processing.GetSelectedInstance()
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)
if instance == nil { if instance == nil {
return return
} }
inst := instance.(*resources.ProcessingInstance) inst := instance.(*resources.ProcessingInstance)
container := Container{ container := Container{Image: inst.Access.Container.Image}
Image: inst.Access.Container.Image,
ImagePullPolicy: "IfNotPresent",
}
if container.Image == "" { if container.Image == "" {
return return
} }
container.Command = []string{"sh", "-c"} // all is bash container.Command = []string{"sh", "-c"} // all is bash
for _, v := range processing.Env { for _, v := range inst.Env {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name}) template.Inputs.Parameters = append(template.Inputs.Parameters, Parameter{Name: v.Name})
} }
for _, v := range wf.Env[itemID] { for _, v := range inst.Inputs {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name}) template.Inputs.Parameters = append(template.Inputs.Parameters, Parameter{Name: v.Name})
} }
for _, v := range processing.Inputs { for _, v := range inst.Inputs {
template.Inputs.Parameters = AppendParamIfAbsent(template.Inputs.Parameters, Parameter{Name: v.Name}) template.Outputs.Parameters = append(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,
})
} }
cmd := strings.ReplaceAll(inst.Access.Container.Command, container.Image, "") cmd := strings.ReplaceAll(inst.Access.Container.Command, container.Image, "")
for _, a := range strings.Split(cmd, " ") { 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, " ") { 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, " ")} container.Args = []string{strings.Join(container.Args, " ")}
template.Container = container template.Container = container
} }
// CreateServiceContainer crée le container Argo pour un ServiceResource. func (template *Template) ReplacePerEnv(arg string, envs []models.Param) string {
// Pour HOSTED, le container appelle le service distant (endpoint connu) ; for _, v := range envs {
// pour DEPLOYMENT, le container EST le service à déployer. if strings.Contains(arg, v.Name) {
// 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) {
value := "{{ inputs.parameters." + 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 return arg
} }
// AddAdmiraltyAnnotations marque le template pour qu'Admiralty route le pod // Add the metadata that allow Admiralty to pick up an Argo Workflow that needs to be reparted
// vers le cluster virtuel correspondant au peerId. // 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, namespace string) error {
// - 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.
func (t *Template) AddAdmiraltyAnnotations(peerId string) {
if t.Metadata.Annotations == nil { if t.Metadata.Annotations == nil {
t.Metadata.Annotations = make(map[string]string) t.Metadata.Annotations = make(map[string]string)
} }
t.Metadata.Annotations["multicluster.admiralty.io/elect"] = ""
if t.NodeSelector == nil { const key = "admiralty.io/multi-cluster-scheduler"
t.NodeSelector = make(map[string]string)
var annotation SchedulerAnnotation
// Parse existing annotation if it exists
if val, ok := t.Metadata.Annotations[key]; ok && val != "" {
if err := json.Unmarshal([]byte(val), &annotation); err != nil {
return fmt.Errorf("failed to parse existing scheduler annotation: %w", err)
} }
t.NodeSelector["multicluster.admiralty.io/cluster-name"] = peerId }
// Add new affinity
annotation.Affinities = append(annotation.Affinities, affinity{
Cluster: "target-" + peerID + "-" + namespace,
Namespace: namespace,
})
// Encode back to JSON
bytes, err := json.Marshal(annotation)
if err != nil {
return fmt.Errorf("failed to encode scheduler annotation: %w", err)
}
t.Metadata.Annotations[key] = string(bytes)
return nil
}
type affinity struct {
Cluster string `json:"cluster"`
Namespace string `json:"namespace"`
}
type SchedulerAnnotation struct {
Affinities []affinity `json:"affinities"`
} }
+92
View File
@@ -0,0 +1,92 @@
package models
import (
"strings"
w "cloud.o-forge.io/core/oc-lib/models/workflow"
)
type WorkflowsDependancies struct {
FirstWfTasks map[string][]string
RelatedWfTasks map[string][]string
LastWfTasks map[string][]string
}
func NewWorkflowDependancies() *WorkflowsDependancies {
return &WorkflowsDependancies{
FirstWfTasks: map[string][]string{},
RelatedWfTasks: map[string][]string{},
LastWfTasks: map[string][]string{},
}
}
func (w *WorkflowsDependancies) BindFirstTasks(depsFunc func(v string) []w.Deps, dag *Dag) {
for wfID, firstTasks := range w.FirstWfTasks {
deps := depsFunc(wfID)
if task := dag.GetTask(wfID); task != nil && len(deps) > 0 {
task.Dependencies = append(task.Dependencies, firstTasks...)
}
}
}
func (w *WorkflowsDependancies) BindRelatedTasks(dag *Dag) {
for wfID, relatedWfTasks := range w.RelatedWfTasks {
for _, dep := range relatedWfTasks {
if task := dag.GetTask(dep); task != nil {
index := -1
if i, deps := task.GetDeps(wfID); deps != "" {
index = i
}
if index != -1 {
task.Dependencies = append(task.Dependencies[:index], task.Dependencies[index+1:]...)
}
if w.LastWfTasks[wfID] != nil {
task.Dependencies = append(task.Dependencies, w.LastWfTasks[wfID]...)
}
}
}
}
}
type Workflow struct {
ApiVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata struct {
Name string `yaml:"name"`
} `yaml:"metadata"`
Spec Spec `yaml:"spec,omitempty"`
}
func (b *Workflow) GetDag() *Dag {
for _, t := range b.Spec.Templates {
if t.Name == "dag" {
return t.Dag
}
}
b.Spec.Templates = append(b.Spec.Templates, Template{Name: "dag", Dag: &Dag{}})
return b.Spec.Templates[len(b.Spec.Templates)-1].Dag
}
type Spec struct {
ServiceAccountName string `yaml:"serviceAccountName"`
Entrypoint string `yaml:"entrypoint"`
Arguments []Parameter `yaml:"arguments,omitempty"`
Volumes []VolumeClaimTemplate `yaml:"volumeClaimTemplates,omitempty"`
Templates []Template `yaml:"templates"`
Timeout int `yaml:"activeDeadlineSeconds,omitempty"`
}
func GetArgoName(raw_name string, component_id string) (formatedName string) {
formatedName = strings.ReplaceAll(raw_name, " ", "-")
formatedName += "-" + component_id
formatedName = strings.ToLower(formatedName)
return
}
func TransformDepsToArgo(deps []w.Deps) []string {
argoDeps := []string{}
for _, dep := range deps {
argoDeps = append(argoDeps, GetArgoName(dep.Source, dep.Dest))
}
return argoDeps
}
+23 -22
View File
@@ -1,9 +1,15 @@
package models package models
import (
"fmt"
"strings"
"cloud.o-forge.io/core/oc-lib/models/resources"
)
type VolumeClaimTemplate struct { type VolumeClaimTemplate struct {
Metadata struct { Metadata struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Annotations map[string]string `yaml:"annotations,omitempty"`
} `yaml:"metadata"` } `yaml:"metadata"`
Spec VolumeSpec `yaml:"spec"` Spec VolumeSpec `yaml:"spec"`
} }
@@ -17,26 +23,21 @@ type VolumeSpec struct {
} `yaml:"resources"` } `yaml:"resources"`
} }
// PVCRef references a pre-provisioned PersistentVolumeClaim by name. type VolumeMount struct {
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"` Name string `yaml:"name"`
PersistentVolumeClaim *PVCRef `yaml:"persistentVolumeClaim,omitempty"` MountPath string `yaml:"mountPath"`
Secret *SecretRef `yaml:"secret,omitempty"` Storage *resources.StorageResource `yaml:"-"`
EmptyDir *EmptyDirRef `yaml:"emptyDir,omitempty"` }
func (v *VolumeMount) BindToArgo(workflow *Workflow) { // TODO : one think about remote volume but TG
index := 0
if v.Storage.SelectedInstanceIndex != nil && (*v.Storage.SelectedInstanceIndex) >= 0 {
index = *v.Storage.SelectedInstanceIndex
}
storage := v.Storage.Instances[index]
new_volume := VolumeClaimTemplate{}
new_volume.Metadata.Name = strings.ReplaceAll(strings.ToLower(v.Name), " ", "-")
new_volume.Spec.AccessModes = []string{"ReadWriteOnce"}
new_volume.Spec.Resources.Requests.Storage = fmt.Sprintf("%v", storage.SizeGB) + storage.SizeType.ToArgo()
workflow.Spec.Volumes = append(workflow.Spec.Volumes, new_volume)
} }
BIN
View File
Binary file not shown.
-542
View File
@@ -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
View File
@@ -4,22 +4,14 @@ import (
"errors" "errors"
"io" "io"
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
) )
type Tool interface { type Tool interface {
CreateArgoWorkflow(path string, ns string) (string, error) CreateArgoWorkflow(path string, ns string) (string, error)
CreateAccessSecret(user string, password string, storageId string, namespace string) (string, error) CreateAccessSecret(ns string, login string, password 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
GetArgoWatch(executionId string, wfName string) (watch.Interface, 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) GetPodLogger(ns string, wfName string, podName string) (io.ReadCloser, error)
GetS3Secret(storageId string, namespace string) *v1.Secret
} }
var _service = map[string]func() (Tool, error){ var _service = map[string]func() (Tool, error){
@@ -27,7 +19,6 @@ var _service = map[string]func() (Tool, error){
} }
func NewService(name string) (Tool, error) { func NewService(name string) (Tool, error) {
return NewKubernetesTool()
service, ok := _service[name] service, ok := _service[name]
if !ok { if !ok {
return nil, errors.New("service not found") return nil, errors.New("service not found")
+34 -84
View File
@@ -2,17 +2,19 @@ package tools
import ( import (
"context" "context"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"oc-monitord/conf" "oc-monitord/conf"
"oc-monitord/utils" "oc-monitord/utils"
"os" "os"
"time"
wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
"github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned" "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned"
"github.com/google/uuid"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
@@ -29,7 +31,7 @@ type KubernetesTools struct {
func NewKubernetesTool() (Tool, error) { func NewKubernetesTool() (Tool, error) {
// Load Kubernetes config (from ~/.kube/config) // Load Kubernetes config (from ~/.kube/config)
config := &rest.Config{ config := &rest.Config{
Host: "https://" + conf.GetConfig().KubeHost + ":" + conf.GetConfig().KubePort, Host: conf.GetConfig().KubeHost + ":" + conf.GetConfig().KubePort,
TLSClientConfig: rest.TLSClientConfig{ TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(conf.GetConfig().KubeCA), CAData: []byte(conf.GetConfig().KubeCA),
CertData: []byte(conf.GetConfig().KubeCert), CertData: []byte(conf.GetConfig().KubeCert),
@@ -75,6 +77,7 @@ func (k *KubernetesTools) CreateArgoWorkflow(path string, ns string) (string, er
if !ok { if !ok {
return "", errors.New("decoded object is not a Workflow") return "", errors.New("decoded object is not a Workflow")
} }
// Create the workflow in the "argo" namespace // Create the workflow in the "argo" namespace
createdWf, err := k.VersionedSet.ArgoprojV1alpha1().Workflows(ns).Create(context.TODO(), workflow, metav1.CreateOptions{}) createdWf, err := k.VersionedSet.ArgoprojV1alpha1().Workflows(ns).Create(context.TODO(), workflow, metav1.CreateOptions{})
if err != nil { if err != nil {
@@ -85,20 +88,21 @@ func (k *KubernetesTools) CreateArgoWorkflow(path string, ns string) (string, er
return createdWf.Name, nil 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 where the secret will be created
namespace := "default"
// Encode the secret data (Kubernetes requires base64-encoded values) // Encode the secret data (Kubernetes requires base64-encoded values)
secretData := map[string][]byte{ secretData := map[string][]byte{
"access-key": []byte(access), "access-key": []byte(base64.StdEncoding.EncodeToString([]byte(login))),
"secret-key": []byte(password), "secret-key": []byte(base64.StdEncoding.EncodeToString([]byte(password))),
} }
// Define the Secret object // Define the Secret object
name := storageId + "-secret-s3" name := uuid.New().String()
secret := &v1.Secret{ secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: namespace, Namespace: ns,
}, },
Type: v1.SecretTypeOpaque, Type: v1.SecretTypeOpaque,
Data: secretData, Data: secretData,
@@ -108,55 +112,11 @@ func (k *KubernetesTools) CreateAccessSecret(access string, password string, sto
if err != nil { if err != nil {
return "", errors.New("Error creating secret: " + err.Error()) return "", errors.New("Error creating secret: " + err.Error())
} }
return name, nil return name, nil
} }
// CreateSourceSecret creates an ephemeral Opaque Secret containing a pre-signed URL func (k *KubernetesTools) GetArgoWatch(executionId string, wfName string) (watch.Interface, error){
// for a private source resource. The secret is labelled with the execution ID so options := metav1.ListOptions{FieldSelector: "metadata.name=oc-monitor-"+wfName}
// 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) {
options := metav1.ListOptions{FieldSelector: "metadata.name=oc-monitor-" + wfName}
watcher, err := k.VersionedSet.ArgoprojV1alpha1().Workflows(executionId).Watch(context.Background(), options) watcher, err := k.VersionedSet.ArgoprojV1alpha1().Workflows(executionId).Watch(context.Background(), options)
if err != nil { if err != nil {
@@ -164,24 +124,22 @@ func (k *KubernetesTools) GetArgoWatch(executionId string, wfName string) (watch
} }
return watcher, nil 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) { func (k *KubernetesTools) GetPodLogger(ns string, wfName string, nodeName string) (io.ReadCloser, error) {
var targetPod v1.Pod var targetPod v1.Pod
pods, err := k.Set.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{ pods, err := k.Set.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{
LabelSelector: "workflows.argoproj.io/workflow=" + wfName, LabelSelector: "workflows.argoproj.io/workflow="+wfName,
}) })
if err != nil { 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 { 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 { for _, pod := range pods.Items {
@@ -190,48 +148,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 // 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) 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 // 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()) req, err := k.Set.CoreV1().Pods(ns).GetLogs(targetPod.Name, &v1.PodLogOptions{Follow: true, Container: "main"}). Stream(context.Background())
if err != nil { 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 return req, nil
} }
func (k *KubernetesTools) testPodReady(pod v1.Pod, ns string) { func (k *KubernetesTools) testPodReady(pod v1.Pod, ns string) {
wfl := utils.GetWFLogger("") for {
pod, err := k.Set.CoreV1().Pods(ns).Get(context.Background(), pod.Name, metav1.GetOptions{})
watcher, err := k.Set.CoreV1().Pods(ns).Watch(context.Background(), metav1.ListOptions{
FieldSelector: "metadata.name=" + pod.Name,
ResourceVersion: pod.ResourceVersion,
})
if err != nil { if err != nil {
wfl.Error().Msg("Error watching pod: " + err.Error() + "\n") wfl := utils.GetWFLogger("")
return wfl.Error().Msg("Error fetching pod: " + err.Error() + "\n")
break
} }
defer watcher.Stop()
for event := range watcher.ResultChan() { var initialized bool
p, ok := event.Object.(*v1.Pod) for _, cond := range pod.Status.Conditions {
if !ok {
continue
}
// 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 // 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 { if (cond.Type == v1.PodReady && cond.Status == v1.ConditionTrue) || pod.Status.Phase == v1.PodSucceeded {
return initialized = true
}
for _, cond := range p.Status.Conditions {
if cond.Type == v1.PodReady && cond.Status == v1.ConditionTrue {
return return
} }
} }
if initialized {
return
}
time.Sleep(2 * time.Second) // avoid hammering the API
} }
} }
-28
View File
@@ -1,15 +1,12 @@
package utils package utils
import ( import (
"encoding/json"
"oc-monitord/conf" "oc-monitord/conf"
"sync" "sync"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/logs" "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/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@@ -39,31 +36,6 @@ func GetLogger() zerolog.Logger {
return 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 { func GetWFLogger(workflowName string) zerolog.Logger {
onceWF.Do(func(){ onceWF.Do(func(){
wf_logger = logger.With(). wf_logger = logger.With().
+146
View File
@@ -0,0 +1,146 @@
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) map[string]interface{} {
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...")
}
}
File diff suppressed because it is too large Load Diff
-128
View File
@@ -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`
+9 -20
View File
@@ -5,11 +5,9 @@ import (
"strings" "strings"
"cloud.o-forge.io/core/oc-lib/models/resources" "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{ new_service := models.Service{
APIVersion: "v1", APIVersion: "v1",
Kind: "Service", Kind: "Service",
@@ -25,15 +23,17 @@ func (b *ArgoBuilder) CreateService(exec *workflow_execution.WorkflowExecution,
if processing == nil { if processing == nil {
return return
} }
b.completeServicePorts(exec, &new_service, id, processing) b.completeServicePorts(&new_service, id, processing)
b.Services = append(b.Services, &new_service) b.Services = append(b.Services, &new_service)
} }
func (b *ArgoBuilder) completeServicePorts(exec *workflow_execution.WorkflowExecution, service *models.Service, id string, processing resources.ResourceInterface) { func (b *ArgoBuilder) completeServicePorts(service *models.Service, id string, processing *resources.ProcessingResource) {
for _, execute := range b.OriginWorkflow.Exposes[processing.GetID()] { 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 { if execute.PAT != 0 {
new_port_translation := models.ServicePort{ new_port_translation := models.ServicePort{
Name: strings.ToLower(processing.GetName()) + id, Name: strings.ToLower(processing.Name) + id,
Port: execute.Port, Port: execute.Port,
TargetPort: execute.PAT, TargetPort: execute.PAT,
Protocol: "TCP", Protocol: "TCP",
@@ -41,25 +41,14 @@ func (b *ArgoBuilder) completeServicePorts(exec *workflow_execution.WorkflowExec
service.Spec.Ports = append(service.Spec.Ports, new_port_translation) service.Spec.Ports = append(service.Spec.Ports, new_port_translation)
} }
} }
}
} }
func (b *ArgoBuilder) addServiceToArgo() error { func (b *ArgoBuilder) addServiceToArgo() error {
for _, service := range b.Services { for _, service := range b.Services {
service_manifest, err := yaml.Marshal(service) if err := service.BindToArgo(b.Workflow); err != nil {
if err != nil {
return err return err
} }
service_template := models.Template{Name: "workflow-service-pod",
Resource: models.ServiceResource{
Action: "create",
SuccessCondition: "status.succeeded > 0",
FailureCondition: "status.failed > 3",
SetOwnerReference: true,
Manifest: string(service_manifest),
},
}
b.Workflow.Spec.Templates = append(b.Workflow.Spec.Templates, service_template)
} }
return nil return nil
} }
-184
View File
@@ -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)
}
},
})
}
+8 -8
View File
@@ -3,10 +3,10 @@ package workflow_builder
import ( import (
"errors" "errors"
"fmt" "fmt"
"oc-monitord/models"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
workflow "cloud.o-forge.io/core/oc-lib/models/workflow" workflow "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
) )
type WorflowDB struct { type WorflowDB struct {
@@ -14,20 +14,20 @@ type WorflowDB struct {
} }
// Create the obj!ects from the mxgraphxml stored in the workflow given as a parameter // 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) logger.Info().Msg("Loading workflow from " + workflow_id)
var err error 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 err
} }
return nil return nil
} }
// Use oclib to retrieve the graph contained in the workflow referenced // 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() 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)) logger.Info().Msg(fmt.Sprint("ERR", lib_data.Code, lib_data.Err))
if lib_data.Code != 200 { if lib_data.Code != 200 {
logger.Error().Msg("Error loading the graph") logger.Error().Msg("Error loading the graph")
@@ -42,15 +42,15 @@ func (w *WorflowDB) getWorkflow(workflow_id string) (workflow *workflow.Workflow
return new_wf, nil 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 := oclib.GetLogger()
logger.Info().Msg(fmt.Sprint("Exporting to Argo", w.Workflow)) logger.Info().Msg(fmt.Sprint("Exporting to Argo", w.Workflow))
if len(w.Workflow.Name) == 0 || w.Workflow.Graph == nil { if len(w.Workflow.Name) == 0 || w.Workflow.Graph == nil {
return nil, 0, fmt.Errorf("can't export a graph that has not been loaded yet") return nil, 0, fmt.Errorf("can't export a graph that has not been loaded yet")
} }
argoBuilder := ArgoBuilder{OriginWorkflow: w.Workflow, Timeout: timeout} argoBuilder := ArgoBuilder{OriginWorkflow: w.Workflow, Workflow: &models.Workflow{}, Timeout: timeout}
stepMax, _, _, err := argoBuilder.CreateDAG(exec, exec.ExecutionsID, true) stepMax, _, _, err := argoBuilder.CreateDAG(namespace, true)
if err != nil { if err != nil {
logger.Error().Msg("Could not create the argo file for " + w.Workflow.Name) logger.Error().Msg("Could not create the argo file for " + w.Workflow.Name)
return nil, 0, err return nil, 0, err
+1 -1
View File
@@ -6,5 +6,5 @@ import (
func TestGetGraph(t *testing.T) { func TestGetGraph(t *testing.T) {
w := WorflowDB{} w := WorflowDB{}
w.LoadFrom("test-log") w.LoadFrom("test-log", "")
} }
-354
View File
@@ -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
}
-459
View File
@@ -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
}
}
}
}
}
}