133 Commits

Author SHA1 Message Date
mr
5bdd2554a7 Infinite loop debug 2026-03-23 09:03:07 +01:00
mr
ea2a98d84a ShouldVerifyAuthdisable on admin request 2026-03-23 08:11:24 +01:00
mr
b82b955045 CanUpdate 2026-03-21 15:08:01 +01:00
mr
88d2e52628 Correct 2026-03-20 16:14:07 +01:00
mr
9f861e5b8d Set up 2026-03-20 15:41:33 +01:00
mr
e4506f3b42 longest trace 2026-03-20 15:21:48 +01:00
mr
75d08aae7c time longest 2026-03-20 15:09:52 +01:00
mr
b288085f32 if 100% kick 2026-03-20 14:57:01 +01:00
mr
bd3e81be0c CHECK log 2026-03-20 14:51:08 +01:00
mr
fafa1186c2 out * 1 hour 2026-03-20 14:42:48 +01:00
mr
471eaff94c missing instanceID 2026-03-20 14:38:52 +01:00
mr
c9fcabac6e debug time 2026-03-20 14:32:46 +01:00
mr
478e68e6d4 Workout Time Scheduling 2026-03-20 14:20:26 +01:00
mr
5619010838 correct time loc 2026-03-20 14:01:14 +01:00
mr
f1a9214ac7 Check trigger strange 2026-03-20 13:41:12 +01:00
mr
e6eb516f39 ensurePricing 2026-03-20 13:28:35 +01:00
mr
1508cc3611 PricedItem evolved 2026-03-20 13:07:06 +01:00
mr
2abc035ec0 planner trace 2026-03-20 12:09:28 +01:00
mr
c34b8c6703 correction planner 2026-03-20 11:33:59 +01:00
mr
a62fbc6c7a Workflow lifecycle events + resource instance duration tracking
- Add WorkflowLifecycleEvent + StepMetric to tools/workflow_lifecycle.go
- Add WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT NATS methods
- ResourceInstance.UpdateAverageDuration for AverageDurationS running average
- Support Steps recap in WORKFLOW_DONE_EVENT for catch-up by oc-scheduler/oc-catalog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:30:30 +01:00
mr
6e28dce02c provisionning 2026-03-19 15:52:55 +01:00
mr
fe3b185b60 err trace 2026-03-19 12:05:33 +01:00
mr
6641d38d9d DBAbstract 2026-03-19 11:32:51 +01:00
mr
93ad8db9a8 decoded CA 2026-03-19 11:17:14 +01:00
mr
4eb53917b8 Log 2026-03-19 10:50:00 +01:00
mr
c7884f5cde NewKubernetesService decoded 2026-03-19 09:05:42 +01:00
mr
5fca0480af suppress check error on get 2026-03-19 08:44:25 +01:00
mr
28b5b7d39f Provisionning Ns + TearDown Ns 2026-03-19 08:18:18 +01:00
mr
5b7edb53a9 OcLib 2026-03-19 07:56:47 +01:00
mr
5976795d44 New Channel to Clarify Movement 2026-03-18 15:38:22 +01:00
mr
3d22ff40fb PB -> ADMIRALTY + MINIO 2026-03-18 14:58:21 +01:00
mr
889656a95e argo kube event remains 2026-03-18 14:52:07 +01:00
mr
c66fbc809e argo event 2026-03-18 14:46:35 +01:00
mr
1a37a1b4aa loki adjust 2026-03-18 10:28:31 +01:00
mr
d4ac398cdb plantuml debug 2026-03-18 09:41:09 +01:00
mr
4eb112bee3 Debug 2026-03-18 09:17:22 +01:00
mr
d1214fe622 adjust Export 2026-03-18 09:10:58 +01:00
mr
6a907236fa export 2026-03-18 08:40:39 +01:00
mr
85314baac3 PlantUML doc & Human Readable commentary 2026-03-18 08:30:02 +01:00
mr
cec8033ddc by pass restriction 2026-03-17 16:46:40 +01:00
mr
d0645f5ca7 publishing is only allowed is it can be monitored and be accessible temp disable 2026-03-17 16:42:03 +01:00
mr
c39bc52312 setup draft as live 2026-03-17 16:35:35 +01:00
mr
0a87343e3e Copy 2026-03-17 16:24:42 +01:00
mr
96beaade24 access 2026-03-17 16:15:12 +01:00
mr
5753450965 oclib setup 2026-03-17 16:09:39 +01:00
mr
7f8d697e4c \n replaceAll 2026-03-17 15:49:27 +01:00
mr
94837f8d24 kicks out Required not Required 2026-03-17 15:31:25 +01:00
mr
e758144b46 forgot 2026-03-17 15:25:54 +01:00
mr
72be3118b7 NATSMethod 2026-03-17 14:59:27 +01:00
mr
67778e1e47 err 2026-03-17 14:54:13 +01:00
mr
562dfb18c1 graphItem 2026-03-17 14:37:06 +01:00
mr
2a2dd96870 graphVarName 2026-03-17 14:32:39 +01:00
mr
333476e2c5 Setup 2026-03-17 14:26:11 +01:00
mr
0fd2513278 Setup 2026-03-17 14:17:49 +01:00
mr
e79101f58d oc-lib 2026-03-17 14:03:19 +01:00
mr
b3dbc7687e setup 2026-03-17 13:52:43 +01:00
mr
8fd4f5faef items 2026-03-17 13:36:36 +01:00
mr
f7012e285f setup 2026-03-17 13:29:54 +01:00
mr
088b45b2cf Set up 2026-03-17 13:19:51 +01:00
mr
1ac735cef1 Stop rebuild id 2026-03-17 10:04:40 +01:00
mr
65237f0d1f implement 2026-03-17 09:32:02 +01:00
mr
9b2f945176 forgot about sessionID use ExecutionsID 2026-03-17 09:01:47 +01:00
mr
b110cbc260 error on usage start 2026-03-16 15:59:19 +01:00
mr
a4d81cbb67 sec 2026-03-16 13:16:50 +01:00
mr
9bf2c566e9 test 2026-03-16 12:48:21 +01:00
mr
6d8efd137a After 2026-03-16 12:32:39 +01:00
mr
40a986af41 order per session 2026-03-16 11:47:51 +01:00
mr
4a076ba237 SchedulingSessionID 2026-03-16 11:45:58 +01:00
mr
deb819c5af After check 2026-03-16 11:41:05 +01:00
mr
55a039bd66 follow date 2026-03-16 11:40:00 +01:00
mr
a86e78841b base draft 2026-03-16 10:59:31 +01:00
mr
48f034316b booking strange state 2026-03-16 10:49:39 +01:00
mr
9e5102893f not found del 2026-03-16 09:28:48 +01:00
mr
465b91fd6e Draft booking flow 2026-03-16 08:52:31 +01:00
mr
12ba346427 bookingstate 2026-03-13 14:32:05 +01:00
mr
2cdf15d722 default to subscription 2026-03-13 14:25:04 +01:00
mr
aeebd8b5b2 pricing strategy defaul is subscription 2026-03-13 14:03:18 +01:00
mr
e355af2bac pricing.PERMANENT 2026-03-13 13:55:40 +01:00
mr
a335c905b3 oclib PB_CLOSE_SEARCH 2026-03-12 15:11:50 +01:00
mr
a30173921f restricted update 2026-03-12 11:56:33 +01:00
mr
e28b79ac0d missing payload 2026-03-12 11:45:24 +01:00
mr
9645e71b54 Acces execution verification for manual verif 2026-03-12 11:40:17 +01:00
mr
9f514a133e add verification flow 2026-03-12 11:37:45 +01:00
mr
f5e1991324 add input + payload 2026-03-12 09:33:10 +01:00
mr
d7a8f2adaa Interface{} 2026-03-12 09:25:44 +01:00
mr
7d9addf760 enrich workflow event manual validation 2026-03-12 09:19:10 +01:00
mr
2c9c42dd51 Add Groups 2026-03-12 08:36:34 +01:00
mr
97bfb0582a peer not found 2026-03-11 09:40:29 +01:00
mr
933b7147e9 execution pllaner 2026-03-11 08:25:18 +01:00
mr
e03a0d3dd0 OR 2026-03-04 15:57:47 +01:00
mr
340f2a6301 OR missing 2026-03-04 15:39:17 +01:00
mr
a426bdf655 isDraft 2026-03-04 13:54:43 +01:00
mr
2bfcfb5736 New 2026-03-04 13:51:43 +01:00
mr
5d18512f67 models 2026-03-04 13:43:14 +01:00
mr
66ee4156e2 is_draft 2026-03-04 13:37:27 +01:00
mr
f1eaf497aa resource as draft for update 2026-03-04 13:31:05 +01:00
mr
b47b51126a by pass temp 2026-03-04 13:15:01 +01:00
mr
473dc62660 decode 2026-03-04 13:02:53 +01:00
mr
334de8ca2e err = res_mongo.Decode(&data); err != nil 2026-03-04 12:43:47 +01:00
mr
ae7e297622 loadone catch error 2026-03-04 12:40:06 +01:00
mr
3e0f369850 prospect 2026-03-04 12:34:11 +01:00
mr
6217618e6c a.Type 2026-03-04 12:22:54 +01:00
mr
f033182382 Apply 2026-03-04 12:18:13 +01:00
mr
542b0b73ab getmyself 2026-03-02 16:24:14 +01:00
mr
44812309db Update try 2026-03-02 15:46:05 +01:00
mr
cb3771c17a reverse VARs 2026-02-26 10:12:17 +01:00
mr
f4e2d8057d crypto 2026-02-26 09:57:54 +01:00
mr
959fce48ef try -> crypto adjust 2026-02-26 09:48:51 +01:00
mr
ce8ef70516 offical pb for remote config 2026-02-24 14:08:21 +01:00
mr
d18b031a29 WorkflowResource 2026-02-24 13:29:00 +01:00
mr
0f6aa1fe78 ARGO_KUBE_EVENT 2026-02-24 13:00:19 +01:00
mr
a9ebad78f3 kube 2026-02-24 10:36:10 +01:00
mr
54aef164ba kubernetes lib 2026-02-24 10:29:28 +01:00
mr
ff830065ec set up 2026-02-23 17:26:37 +01:00
mr
e039fa56b6 execution_id 2026-02-23 15:56:40 +01:00
mr
e10bb55455 p.SetID(uuid.NewString()) 2026-02-23 15:50:10 +01:00
mr
f28e2c3620 State in WF 2026-02-23 15:41:48 +01:00
mr
b08bbf51dd priced.GetInstanceID() 2026-02-23 15:22:48 +01:00
mr
5d32b4646a Add InstanceID 2026-02-23 15:18:27 +01:00
mr
25e4e67111 missing Instance hit per Purchase +Booking 2026-02-23 15:08:27 +01:00
mr
19b0f10e71 adjust self 2026-02-23 14:07:39 +01:00
mr
12c506e9a7 Native Schedule 2026-02-23 10:23:55 +01:00
mr
2871353635 close planner 2026-02-23 09:30:03 +01:00
mr
59923ac5c1 Missing action 2026-02-23 09:25:38 +01:00
mr
da8b8ec397 Planner 2026-02-23 09:20:00 +01:00
mr
9afbbb5c82 Planner Improve 2026-02-23 08:38:43 +01:00
mr
9662ac6d67 Add New 2026-02-19 09:43:44 +01:00
mr
0b41e2505e Nats Native Behaviors + Peer is Stateless 2026-02-18 14:25:56 +01:00
mr
fa5c3a3c60 Adjust + Test 2026-02-18 12:24:19 +01:00
mr
842e09f22f by pass pricing profile need 2026-02-17 10:02:44 +01:00
mr
403913d8cf new oclib match 2026-02-12 13:39:52 +01:00
mr
7e8546bbea New dynamic URL match 2026-02-12 13:20:06 +01:00
mr
1895b7ac8a prospect 2026-02-10 09:37:39 +01:00
83 changed files with 4387 additions and 1845 deletions

190
UNUSED_AND_ISSUES.md Normal file
View File

@@ -0,0 +1,190 @@
# Rapport d'audit Éléments inutilisés et problèmes identifiés
> Généré le 2026-02-18 branche `feature/event`
---
## 1. Bugs critiques corrigés dans cette session
| Fichier | Ligne | Description | Statut |
|---------|-------|-------------|--------|
| `entrypoint.go` | 652, 664, 676, 688 | `fmt.Errorf(res.Err)` format string non-constant (erreur de build) | Corrigé |
| `models/utils/abstracts.go` | 136 | `VerifyAuth` déréférençait `request.Admin` avant de vérifier `request != nil` | Corrigé |
| `models/utils/abstracts.go` | 68-78 | `DeepCopy()` faisait `Unmarshal` dans un pointeur nil retournait toujours `nil` | Corrigé |
| `models/resources/resource.go` | 176 | `instances = append(instances)` argument manquant, l'instance n'était jamais ajoutée | Corrigé |
| `models/resources/priced_resource.go` | 63-69 | Code mort après `return true` dans `IsBooked()` | Corrigé |
| `tools/remote_caller.go` | 118 | `CallDelete` vérifiait `req.Body == nil` (toujours vrai pour DELETE), court-circuitant la lecture de la réponse | Corrigé |
---
## 2. Debug prints à supprimer (fmt.Println en production)
Ces appels `fmt.Println` polluent stdout et peuvent exposer des informations sensibles.
| Fichier | Lignes | Contenu |
|---------|--------|---------|
| `models/bill/bill.go` | ~197 | `fmt.Println(err)` |
| `models/collaborative_area/collaborative_area_mongo_accessor.go` | ~95, 109, 118, 123 | Debug sur `res`, `sharedWorkspace.AllowedPeersGroup`, `canFound`, `peerskey` |
| `models/peer/peer_cache.go` | ~44, 55 | URL et `"Launching peer execution on..."` |
| `models/resources/storage.go` | ~196 | `fmt.Println("GetPriceHT", ...)` |
| `models/workflow/workflow.go` | ~158, 164, 170, 176 | 4× `fmt.Println(err)` |
| `tools/nats_caller.go` | ~110, 117, 122, 126 | 4× `fmt.Println()` divers |
| `tools/remote_caller.go` | 227 | `fmt.Println("Error reading the body...")` (devrait utiliser le logger) |
| `dbs/dbs.go` | 47 | `fmt.Println("Recovered. Error:\n", r, debug.Stack())` |
> **Note :** `priced_resource.go` et `data.go` corrigés dans cette session.
---
## 3. Code commenté significatif
### 3.1 Validation de pricing désactivée (workflow)
**Fichier :** `models/workflow/workflow.go` ~lignes 631-634
```go
// Should be commented once the Pricing selection feature has been implemented
// if priced.SelectPricing() == nil {
// return resources, priceds, errors.New("no pricings are selected... can't proceed")
// }
```
Une vérification de sécurité critique est désactivée. Sans elle, des ressources sans pricing peuvent être traitées silencieusement.
### 3.2 PAY_PER_USE stratégie supprimée mais traces restantes
**Fichier :** `models/common/pricing/pricing_strategy.go` lignes 47, 61-63
```go
// PAY_PER_USE // per request. ( unpredictible )
/*case PAY_PER_USE:
return bs, true*/
```
La constante `PAY_PER_USE` a été supprimée mais les commentaires laissés créent de la confusion.
### 3.3 Vérification d'autorisation peer désactivée
**Fichier :** `models/resources/resource.go` lignes 98-104
```go
/*if ok, _ := utils.IsMySelf(request.PeerID, ...); ok {*/
profile = pricing.GetDefaultPricingProfile()
/*} else {
return nil, errors.New("no pricing profile found")
}*/
```
Le profil par défaut est retourné sans vérifier si le pair est bien `myself`. Sécurité à revoir.
---
## 4. Logique erronée non corrigée (à traiter)
### 4.1 IsTimeStrategy logique inversée
**Fichier :** `models/common/pricing/pricing_strategy.go` ligne 88
```go
func IsTimeStrategy(i int) bool {
return len(TimePricingStrategyList()) < i // BUG: devrait être ">"
}
```
La condition est inversée. Retourne `true` pour des valeurs hors de la liste. Fonction actuellement non utilisée (voir §5).
### 4.2 IsBillingStrategyAllowed case SUBSCRIPTION sans retour
**Fichier :** `models/common/pricing/pricing_strategy.go` lignes 54-65
```go
case SUBSCRIPTION:
/*case PAY_PER_USE:
return bs, true*/
// Aucun return ici → tombe dans le default
```
Le cas `SUBSCRIPTION` ne retourne rien explicitement, ce qui est trompeur.
---
## 5. Éléments inutilisés
### 5.1 Fonction jamais appelée
| Symbole | Fichier | Ligne |
|---------|---------|-------|
| `IsTimeStrategy(i int) bool` | `models/common/pricing/pricing_strategy.go` | 88 |
De plus, cette fonction a une logique erronée (voir §4.1).
### 5.2 Variable singleton inutilisée
| Symbole | Fichier | Ligne |
|---------|---------|-------|
| `HTTPCallerInstance` | `tools/remote_caller.go` | 57 |
Déclarée comme singleton mais jamais utilisée de nouvelles instances sont créées via `NewHTTPCaller()`.
---
## 6. Tests supprimés (couverture perdue)
Les fichiers suivants ont été supprimés sur la branche `feature/event` et la couverture correspondante n'est plus assurée :
| Fichier supprimé | Modèles non couverts |
|------------------|----------------------|
| `models/peer/tests/peer_cache_test.go` | `PeerCache` logique d'exécution distribuée |
| `models/peer/tests/peer_test.go` | `Peer` modèle et accesseur |
| `models/utils/tests/abstracts_test.go` | `AbstractObject` méthodes de base |
| `models/utils/tests/common_test.go` | `GenericStoreOne`, `GenericDeleteOne`, etc. |
| `models/workflow_execution/tests/workflow_test.go` | `WorkflowExecution` modèle et accesseur |
> `models/order/tests/order_test.go` existe mais ne contient **aucune fonction de test**.
---
## 7. Fautes d'orthographe dans les identifiants publics
Ces typos sont dans des noms exportés (API publique) les corriger est un **breaking change**.
### 7.1 `Instanciated` `Instantiated`
Apparaît 50+ fois dans les types exportés centraux :
- `AbstractInstanciatedResource[T]` (resource.go, compute.go, data.go, storage.go, processing.go, workflow.go)
- `AbstractInstanciatedResource.Instances`
- Tests : `resources.AbstractInstanciatedResource[*MockInstance]{...}`
### 7.2 `ressource` `resource` (dans les messages d'erreur)
**Fichier :** `entrypoint.go` messages dans `LoadOneStorage`, `LoadOneComputing`, etc.
```go
"Error while loading storage ressource " + storageId // "ressource" est du français
```
### 7.3 `GARANTED` `GUARANTEED`
**Fichiers :** `models/common/pricing/pricing_profile.go`, `models/resources/storage.go`
```go
GARANTED_ON_DELAY // pricing_profile.go:72
GARANTED // pricing_profile.go:73
GARANTED_ON_DELAY_STORAGE // storage.go:106
GARANTED_STORAGE // storage.go:107
```
### 7.4 `CREATE_EXECTUTION` `CREATE_EXECUTION`
**Fichier :** `tools/nats_caller.go` ligne 34
```go
CREATE_EXECTUTION // faute de frappe dans la constante enum
```
### 7.5 `PROPALGATION` `PROPAGATION`
**Fichier :** `tools/nats_caller.go` lignes 29, 45, 56
```go
"propalgation event" // et PROPALGATION_EVENT
```
---
## 8. Incohérences de nommage mineures
| Fichier | Problème |
|---------|----------|
| `models/resources/interfaces.go:19` | Paramètre `instance_id` en snake_case dans une signature Go (devrait être `instanceID`) |
| `entrypoint.go:505` | Message de panique dans `CopyOne` dit `"Panic recovered in UpdateOne"` |
| `tools/remote_caller.go:110` | Commentaire `// CallPut calls the DELETE method` (copie-colle incorrect) |
---
## 9. Résumé
| Catégorie | Nombre | Priorité |
|-----------|--------|----------|
| Bugs critiques corrigés | 6 | Fait |
| Debug `fmt.Println` restants | 15+ | 🔴 Haute |
| Code commenté important | 3 | 🟠 Moyenne |
| Logique erronée (non corrigée) | 2 | 🟠 Moyenne |
| Éléments inutilisés | 2 | 🟡 Faible |
| Tests supprimés (couverture perdue) | 5 fichiers | 🟠 Moyenne |
| Typos dans API publique | 5 types | 🟡 Faible (breaking change) |
| Incohérences mineures | 3 | 🟢 Très faible |

View File

@@ -20,6 +20,14 @@ type Config struct {
Whitelist bool Whitelist bool
PrivateKeyPath string PrivateKeyPath string
PublicKeyPath string PublicKeyPath string
InternalCatalogAPI string
InternalSharedAPI string
InternalWorkflowAPI string
InternalWorkspaceAPI string
InternalPeerAPI string
InternalDatacenterAPI string
InternalSchedulerAPI string
} }
func (c Config) GetUrl() string { func (c Config) GetUrl() string {
@@ -40,7 +48,10 @@ func GetConfig() *Config {
return instance return instance
} }
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int, pkPath, ppPath string) *Config { func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int,
pkPath, ppPath,
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI, internalWorkspaceAPI,
internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *Config {
GetConfig().MongoUrl = mongoUrl GetConfig().MongoUrl = mongoUrl
GetConfig().MongoDatabase = database GetConfig().MongoDatabase = database
GetConfig().NATSUrl = natsUrl GetConfig().NATSUrl = natsUrl
@@ -50,5 +61,13 @@ func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string,
GetConfig().APIPort = port GetConfig().APIPort = port
GetConfig().PrivateKeyPath = pkPath GetConfig().PrivateKeyPath = pkPath
GetConfig().PublicKeyPath = ppPath GetConfig().PublicKeyPath = ppPath
GetConfig().InternalCatalogAPI = internalCatalogAPI
GetConfig().InternalSharedAPI = internalSharedAPI
GetConfig().InternalWorkflowAPI = internalWorkflowAPI
GetConfig().InternalWorkspaceAPI = internalWorkspaceAPI
GetConfig().InternalPeerAPI = internalPeerAPI
GetConfig().InternalDatacenterAPI = internalDatacenterAPI
GetConfig().InternalSchedulerAPI = internalSchedulerAPI
return GetConfig() return GetConfig()
} }

View File

@@ -35,6 +35,7 @@ var str = [...]string{
"equal", "equal",
"not", "not",
"elemMatch", "elemMatch",
"or",
} }
func (m Operator) String() string { func (m Operator) String() string {
@@ -47,30 +48,30 @@ func (m Operator) ToMongoOperator(k string, value interface{}) bson.M {
fmt.Println("Recovered. Error:\n", r, debug.Stack()) fmt.Println("Recovered. Error:\n", r, debug.Stack())
} }
}() }()
defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value)}} defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
switch m { switch m {
case LIKE: case LIKE:
return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case EXISTS: case EXISTS:
return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case IN: case IN:
return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case GTE: case GTE:
return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case GT: case GT:
return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case LTE: case LTE:
return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case LT: case LT:
return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case ELEMMATCH: case ELEMMATCH:
return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value)}} return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case EQUAL: case EQUAL:
return bson.M{k: value} return bson.M{k: value}
case NOT: case NOT:
return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value)} return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value, false)}
case OR: case OR:
return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value)} return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value, true)}
default: default:
return defaultValue return defaultValue
} }
@@ -112,10 +113,19 @@ func GetBson(filters *Filters) bson.D {
return f return f
} }
func (m Operator) ToValueOperator(operator Operator, value interface{}) interface{} { func (m Operator) ToValueOperator(operator Operator, value interface{}, or bool) interface{} {
switch value := value.(type) { switch value := value.(type) {
case *Filters: case *Filters:
return GetBson(value) bson := GetBson(value)
if or {
for _, b := range bson {
if b.Key == "$or" {
return b.Value
}
}
} else {
return bson
}
default: default:
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" { if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
value = "" value = ""

View File

@@ -267,6 +267,9 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
} }
filter := bson.M{"_id": id} filter := bson.M{"_id": id}
targetDBCollection := CollectionMap[collection_name] targetDBCollection := CollectionMap[collection_name]
if targetDBCollection == nil {
return nil, 503, errors.New("collection " + collection_name + " not initialized")
}
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel() //defer cancel()
@@ -286,6 +289,9 @@ func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.C
opts := options.Find() opts := options.Find()
opts.SetLimit(1000) opts.SetLimit(1000)
targetDBCollection := CollectionMap[collection_name] targetDBCollection := CollectionMap[collection_name]
if targetDBCollection == nil {
return nil, 503, errors.New("collection " + collection_name + " not initialized")
}
f := dbs.GetBson(filters) f := dbs.GetBson(filters)

View File

@@ -0,0 +1,214 @@
# PlantUML Format de commentaire human-readable
Ce document décrit la syntaxe des commentaires attachés aux ressources et aux liens
dans les fichiers PlantUML importés par OpenCloud.
---
## Syntaxe générale
```plantuml
TypeRessource(varName, "Nom affiché") ' clé: valeur, clé.sous_clé: valeur
```
### Règles de parsing
| Règle | Détail |
|---|---|
| Séparateur de paires | `,` |
| Séparateur clé/valeur | premier `:` de la paire (les URLs `http://...` sont gérées) |
| Sous-objets | notation pointée `access.container.image: nginx` |
| Types | auto-inférés : `bool` > `float64` > `string` |
| Fallback | JSON brut si le commentaire commence par `{` (compatibilité ascendante) |
### Comportement à l'import
Chaque ressource reçoit automatiquement une **instance par défaut**, seedée avec les
attributs de la ressource parente. Le commentaire vient ensuite **surcharger** uniquement
les champs explicitement renseignés.
> **Exception :** `WorkflowEvent` n'a pas d'instance (voir section dédiée).
---
## Ressources disponibles
### `Data(var, "nom")` Données
Ressource de données. Les attributs qualifient le modèle de données **et** son instance
(source d'accès).
| Clé | Type | Description |
|---|---|---|
| `type` | string | Type de données (`raster`, `vector`, `tabular`) |
| `quality` | string | Niveau de qualité |
| `open_data` | bool | Données en accès libre |
| `static` | bool | Données statiques (pas de mise à jour) |
| `personal_data` | bool | Contient des données personnelles |
| `anonymized_personal_data` | bool | Données personnelles anonymisées |
| `size` | float64 | Taille en GB |
| `access_protocol` | string | Protocole d'accès (`http`, `s3`, `ftp`) |
| `country` | string | Code pays ISO (`FR`, `DE`) |
| `location.latitude` | float64 | Latitude géographique |
| `location.longitude` | float64 | Longitude géographique |
| `source` | string | URL / endpoint d'accès à la donnée |
```plantuml
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
```
---
### `Processing(var, "nom")` Traitement
Ressource de traitement (algorithme, conteneur, service). Les attributs qualifient
le modèle de traitement **et** sa configuration d'exécution.
| Clé | Type | Description |
|---|---|---|
| `infrastructure` | int | Infrastructure cible : `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
| `is_service` | bool | Traitement persistant (service long-running) |
| `open_source` | bool | Code source ouvert |
| `license` | string | Licence (`MIT`, `Apache-2.0`, `GPL-3.0`) |
| `maturity` | string | Maturité (`prototype`, `beta`, `production`) |
| `access_protocol` | string | Protocole d'accès |
| `country` | string | Code pays ISO |
| `location.latitude` | float64 | Latitude |
| `location.longitude` | float64 | Longitude |
| `access.container.image` | string | Image du conteneur |
| `access.container.command` | string | Commande de démarrage |
| `access.container.args` | string | Arguments de la commande |
```plantuml
Processing(p1, "NDVI Calc") ' infrastructure: 0, open_source: true, license: MIT, maturity: production, access.container.image: myrepo/ndvi:1.2
```
---
### `Storage(var, "nom")` Stockage
Ressource de stockage. Produit une instance live (`LiveStorage`) à l'import.
| Clé | Type | Description |
|---|---|---|
| `storage_type` | int | Type de stockage (enum) |
| `source` | string | URL / endpoint du stockage |
| `path` | string | Chemin ou bucket dans le stockage |
| `local` | bool | Stockage local |
| `security_level` | string | Niveau de sécurité |
| `size` | float64 | Taille allouée en GB |
| `encryption` | bool | Chiffrement activé |
| `redundancy` | string | Politique de redondance |
| `throughput` | string | Débit cible |
| `access_protocol` | string | Protocole (`s3`, `nfs`, `smb`) |
| `country` | string | Code pays ISO |
| `location.latitude` | float64 | Latitude |
| `location.longitude` | float64 | Longitude |
```plantuml
Storage(s1, "Minio OVH") ' source: http://minio.example.com:9000, path: /bucket/data, access_protocol: s3, encryption: true, size: 500, country: FR
```
---
### `ComputeUnit(var, "nom")` Unité de calcul
Ressource de calcul (datacenter, cluster). Produit une instance live (`LiveDatacenter`)
à l'import.
| Clé | Type | Description |
|---|---|---|
| `architecture` | string | Architecture CPU (`x86_64`, `arm64`) |
| `infrastructure` | int | `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
| `source` | string | URL de l'API du datacenter |
| `security_level` | string | Niveau de sécurité |
| `annual_co2_emissions` | float64 | Émissions CO annuelles (kg) |
| `access_protocol` | string | Protocole d'accès |
| `country` | string | Code pays ISO |
| `location.latitude` | float64 | Latitude |
| `location.longitude` | float64 | Longitude |
```plantuml
ComputeUnit(c1, "Datacenter Rennes") ' source: https://api.dc-rennes.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68, security_level: high
```
---
### `WorkflowEvent(var, "nom")` Événement déclencheur de workflow
Crée directement un `NativeTool` de type `WORKFLOW_EVENT` (Kind = 0).
Représente le point de départ d'un workflow.
> **Pas d'instance. Pas de commentaire.**
> Le nom du `NativeTool` est forcé à `WORKFLOW_EVENT` à l'import.
```plantuml
WorkflowEvent(e1, "Start")
```
---
## Liens
Les commentaires sur les liens qualifient la connexion entre deux ressources
(typiquement entre un traitement et un stockage).
### Syntaxe
```plantuml
source --> destination ' clé: valeur
source <-- destination ' clé: valeur
source -- destination ' clé: valeur (non directionnel)
```
### Attributs disponibles
| Clé | Type | Description |
|---|---|---|
| `storage_link_infos.write` | bool | `true` = écriture, `false` = lecture |
| `storage_link_infos.source` | string | Chemin source dans le lien |
| `storage_link_infos.destination` | string | Chemin destination dans le lien |
| `storage_link_infos.filename` | string | Nom du fichier échangé |
```plantuml
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: output.tif
d1 --> p1
```
---
## Exemple complet
```plantuml
@startuml
!include opencloud.puml
WorkflowEvent(e1, "Start")
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
Processing(p1, "NDVI") ' infrastructure: 0, open_source: true, license: MIT, access.container.image: myrepo/ndvi:1.2
Storage(s1, "Minio résultats") ' source: http://minio.example.com:9000, path: /results, access_protocol: s3, encryption: true, size: 500, country: FR
ComputeUnit(c1, "DC Rennes") ' source: https://api.dc.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68
e1 --> p1
d1 --> p1
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: ndvi.tif
s1 --> c1
@enduml
```
---
## Récapitulatif des types de ressources
| Mot-clé PlantUML | Type Go | Instance | Live | Commentaire |
|---|---|---|---|---|
| `Data` | `DataResource` | `DataInstance` | non | oui |
| `Processing` | `ProcessingResource` | `ProcessingInstance` | non | oui |
| `Storage` | `StorageResource` | `StorageResourceInstance` | oui `LiveStorage` | oui |
| `ComputeUnit` | `ComputeResource` | `ComputeResourceInstance` | oui `LiveDatacenter` | oui |
| `WorkflowEvent` | `NativeTool` (Kind=WORKFLOW_EVENT) | aucune | non | non |

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"slices"
"strings" "strings"
"runtime/debug" "runtime/debug"
@@ -34,6 +35,7 @@ import (
"github.com/beego/beego/v2/server/web/filter/cors" "github.com/beego/beego/v2/server/web/filter/cors"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/goraz/onion" "github.com/goraz/onion"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@@ -61,10 +63,15 @@ const (
LIVE_STORAGE = tools.LIVE_STORAGE LIVE_STORAGE = tools.LIVE_STORAGE
PURCHASE_RESOURCE = tools.PURCHASE_RESOURCE PURCHASE_RESOURCE = tools.PURCHASE_RESOURCE
NATIVE_TOOL = tools.NATIVE_TOOL NATIVE_TOOL = tools.NATIVE_TOOL
EXECUTION_VERIFICATION = tools.EXECUTION_VERIFICATION
) )
func GetMySelf() (string, error) { func GetMySelf() (*peer.Peer, error) {
return utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true})) pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
if pp == nil {
return nil, errors.New("peer not found")
}
return pp.(*peer.Peer), err
} }
func IsMySelf(peerID string) (bool, string) { func IsMySelf(peerID string) (bool, string) {
@@ -92,7 +99,7 @@ func GenerateNodeID() (string, error) {
// will turn into standards api hostnames // will turn into standards api hostnames
func (d LibDataEnum) API() string { func (d LibDataEnum) API() string {
return tools.DefaultAPI[d] return tools.Str[d]
} }
// will turn into standards name // will turn into standards name
@@ -151,6 +158,14 @@ func InitDaemon(appName string) {
o.GetIntDefault("API_PORT", 8080), o.GetIntDefault("API_PORT", 8080),
o.GetStringDefault("PUBLIC_KEY_PATH", "./pem/public.pem"), o.GetStringDefault("PUBLIC_KEY_PATH", "./pem/public.pem"),
o.GetStringDefault("PRIVATE_KEY_PATH", "./pem/private.pem"), o.GetStringDefault("PRIVATE_KEY_PATH", "./pem/private.pem"),
o.GetStringDefault("INTERNAL_CATALOG_API", "oc-catalog"),
o.GetStringDefault("INTERNAL_SHARED_API", "oc-shared"),
o.GetStringDefault("INTERNAL_WORKFLOW_API", "oc-workflow"),
o.GetStringDefault("INTERNAL_WORKSPACE_API", "oc-workspace"),
o.GetStringDefault("INTERNAL_PEER_API", "oc-peer"),
o.GetStringDefault("INTERNAL_DATACENTER_API", "oc-datacenter"),
o.GetStringDefault("INTERNAL_SCHEDULER_API", "oc-scheduler"),
) )
// Beego init // Beego init
beego.BConfig.AppName = appName beego.BConfig.AppName = appName
@@ -238,8 +253,12 @@ func GetLogger() zerolog.Logger {
* @param logLevel string * @param logLevel string
* @return *Config * @return *Config
*/ */
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int, pkpath string, pppath string) *config.Config { func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string,
cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath) port int, pppath string, pkpath string,
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *config.Config {
cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath, internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI, internalSchedulerAPI)
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack()))) tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
@@ -290,6 +309,15 @@ func NewRequest(collection LibDataEnum, user string, peerID string, groups []str
return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller} return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller}
} }
func NewRequestInfoAdmin(collection LibDataEnum, user string, groups []string, caller *tools.HTTPCaller) *Request {
p, err := GetMySelf()
peerID := ""
if p != nil && err == nil {
peerID = p.GetID()
}
return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller, admin: true}
}
func NewRequestAdmin(collection LibDataEnum, caller *tools.HTTPCaller) *Request { func NewRequestAdmin(collection LibDataEnum, caller *tools.HTTPCaller) *Request {
return &Request{Collection: collection, Caller: caller, admin: true} return &Request{Collection: collection, Caller: caller, admin: true}
} }
@@ -414,7 +442,7 @@ func (r *Request) UpdateOne(set map[string]interface{}, id string) (data LibData
PeerID: r.PeerID, PeerID: r.PeerID,
Groups: r.Groups, Groups: r.Groups,
Admin: r.admin, Admin: r.admin,
}).UpdateOne(model.Deserialize(set, model), id) }).UpdateOne(set, id)
if err != nil { if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()} data = LibData{Data: d, Code: code, Err: err.Error()}
return return
@@ -621,18 +649,7 @@ func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
return nil return nil
} }
// ============== ADMIRALTY ============== // ------------- Loading resources ----------GetAccessor
// Returns a concatenation of the peerId and namespace in order for
// kubernetes ressources to have a unique name, under 63 characters
// and yet identify which peer they are created for
func GetConcatenatedName(peerId string, namespace string) string {
s := strings.Split(namespace, "-")[:2]
n := s[0] + "-" + s[1]
return peerId + "-" + n
}
// ------------- Loading resources ----------
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) { func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
@@ -640,7 +657,7 @@ func LoadOneStorage(storageId string, user string, peerID string, groups []strin
if res.Code != 200 { if res.Code != 200 {
l := GetLogger() l := GetLogger()
l.Error().Msg("Error while loading storage ressource " + storageId) l.Error().Msg("Error while loading storage ressource " + storageId)
return nil, fmt.Errorf(res.Err) return nil, errors.New(res.Err)
} }
return res.ToStorageResource(), nil return res.ToStorageResource(), nil
@@ -652,7 +669,7 @@ func LoadOneComputing(computingId string, user string, peerID string, groups []s
if res.Code != 200 { if res.Code != 200 {
l := GetLogger() l := GetLogger()
l.Error().Msg("Error while loading computing ressource " + computingId) l.Error().Msg("Error while loading computing ressource " + computingId)
return nil, fmt.Errorf(res.Err) return nil, errors.New(res.Err)
} }
return res.ToComputeResource(), nil return res.ToComputeResource(), nil
@@ -664,7 +681,7 @@ func LoadOneProcessing(processingId string, user string, peerID string, groups [
if res.Code != 200 { if res.Code != 200 {
l := GetLogger() l := GetLogger()
l.Error().Msg("Error while loading processing ressource " + processingId) l.Error().Msg("Error while loading processing ressource " + processingId)
return nil, fmt.Errorf(res.Err) return nil, errors.New(res.Err)
} }
return res.ToProcessingResource(), nil return res.ToProcessingResource(), nil
@@ -676,8 +693,81 @@ func LoadOneData(dataId string, user string, peerID string, groups []string) (*r
if res.Code != 200 { if res.Code != 200 {
l := GetLogger() l := GetLogger()
l.Error().Msg("Error while loading data ressource " + dataId) l.Error().Msg("Error while loading data ressource " + dataId)
return nil, fmt.Errorf(res.Err) return nil, errors.New(res.Err)
} }
return res.ToDataResource(), nil return res.ToDataResource(), nil
} }
// verify signature...
func InitNATSDecentralizedEmitter(authorizedDT ...tools.DataType) {
tools.NewNATSCaller().ListenNats(map[tools.NATSMethod]func(tools.NATSResponse){
tools.CREATE_RESOURCE: func(resp tools.NATSResponse) {
if resp.FromApp == config.GetAppName() || !slices.Contains(authorizedDT, resp.Datatype) {
return
}
p := map[string]interface{}{}
if err := json.Unmarshal(resp.Payload, &p); err == nil {
if err := verify(resp.Payload); err != nil {
return // don't trust anyone... only friends and foes are privilege
}
access := NewRequestAdmin(LibDataEnum(resp.Datatype), nil)
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false); len(data.Data) > 0 {
delete(p, "id")
access.UpdateOne(p, data.Data[0].GetID())
} else {
access.StoreOne(p)
}
}
},
tools.REMOVE_RESOURCE: func(resp tools.NATSResponse) {
if resp.FromApp == config.GetAppName() || !slices.Contains(authorizedDT, resp.Datatype) {
return
}
if err := verify(resp.Payload); err != nil {
return // don't trust anyone... only friends and foes are privilege
}
p := map[string]interface{}{}
access := NewRequestAdmin(LibDataEnum(resp.Datatype), nil)
err := json.Unmarshal(resp.Payload, &p)
if err == nil {
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false); len(data.Data) > 0 {
access.DeleteOne(fmt.Sprintf("%v", p[resp.SearchAttr]))
}
}
},
})
}
func verify(payload []byte) error {
var obj utils.AbstractObject
if err := json.Unmarshal(payload, &obj); err == nil {
obj.Unsign()
origin := NewRequestAdmin(LibDataEnum(PEER), nil).LoadOne(obj.GetCreatorID())
if origin.Data == nil || origin.Data.(*peer.Peer).Relation != peer.PARTNER {
return errors.New("don't know personnaly this guy") // don't trust anyone... only friends and foes are privilege
}
data, err := base64.StdEncoding.DecodeString(origin.Data.(*peer.Peer).PublicKey)
if err != nil {
return err
}
pk, err := crypto.UnmarshalPublicKey(data)
if err != nil {
return err
}
b, err := json.Marshal(obj)
if err != nil {
return err
}
if ok, err := pk.Verify(b, obj.GetSignature()); err != nil {
return err
} else if !ok {
return errors.New("signature is not corresponding to public key")
} else {
return nil
}
} else {
return err
}
}

50
go.mod
View File

@@ -1,29 +1,58 @@
module cloud.o-forge.io/core/oc-lib module cloud.o-forge.io/core/oc-lib
go 1.24.6 go 1.25.0
require ( require (
github.com/beego/beego/v2 v2.3.1 github.com/beego/beego/v2 v2.3.8
github.com/go-playground/validator/v10 v10.22.0 github.com/go-playground/validator/v10 v10.22.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/goraz/onion v0.1.3 github.com/goraz/onion v0.1.3
github.com/nats-io/nats.go v1.37.0 github.com/nats-io/nats.go v1.37.0
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
k8s.io/apimachinery v0.35.1
) )
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/time v0.9.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
) )
require ( require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
go.mongodb.org/mongo-driver v1.16.0 go.mongodb.org/mongo-driver v1.16.0
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.38.0 // indirect
) )
require ( require (
@@ -37,7 +66,6 @@ require (
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/libp2p/go-libp2p/core v0.43.0-rc2 github.com/libp2p/go-libp2p/core v0.43.0-rc2
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -52,10 +80,12 @@ 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-20240424034433-3c2c7870ae76 // indirect github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect
golang.org/x/crypto v0.39.0 // indirect golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.1
k8s.io/client-go v0.35.1
) )

173
go.sum
View File

@@ -1,6 +1,8 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beego/beego/v2 v2.3.1 h1:7MUKMpJYzOXtCUsTEoXOxsDV/UcHw6CPbaWMlthVNsc= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/beego/beego/v2 v2.3.1/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc=
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg=
github.com/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=
@@ -10,17 +12,34 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 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.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
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.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -29,12 +48,18 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
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.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/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=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -44,36 +69,74 @@ github.com/goraz/onion v0.1.3/go.mod h1:XEmz1XoBz+wxTgWB8NwuvRm4RAu3vKxvrmYtzK+X
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
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/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q= github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
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/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/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=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
@@ -81,6 +144,10 @@ github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDm
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/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/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -93,8 +160,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -105,12 +172,23 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
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/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -122,27 +200,33 @@ github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dh
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.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
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-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-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-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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -153,33 +237,60 @@ 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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.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.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.9.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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 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.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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=
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -59,13 +59,12 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
// A bit unsafe since we don't know what could be stored in the event // A bit unsafe since we don't know what could be stored in the event
// but we can't access this object once passed to the multilevel writter // but we can't access this object once passed to the multilevel writter
for k,v := range(event){ for k, v := range event {
if k != "level" && k != "time" && k != "message" { if k != "level" && k != "time" && k != "message" {
labels[k] = v.(string) labels[k] = fmt.Sprintf("%v", v)
} }
} }
// Format the timestamp in nanoseconds // Format the timestamp in nanoseconds
timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond)) timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond))

View File

@@ -2,12 +2,12 @@ package bill
import ( import (
"encoding/json" "encoding/json"
"fmt"
"sync" "sync"
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/order" "cloud.o-forge.io/core/oc-lib/models/order"
"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/resources" "cloud.o-forge.io/core/oc-lib/models/resources"
@@ -49,6 +49,7 @@ func DraftFirstBill(order *order.Order, request *tools.APIRequest) (*Bill, error
peers[p.DestPeerID] = []*PeerItemOrder{} peers[p.DestPeerID] = []*PeerItemOrder{}
} }
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{ peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
ResourceType: p.ResourceType,
Purchase: p, Purchase: p,
Item: p.PricedItem, Item: p.PricedItem,
Quantity: 1, Quantity: 1,
@@ -70,6 +71,8 @@ func DraftFirstBill(order *order.Order, request *tools.APIRequest) (*Bill, error
peers[b.DestPeerID] = []*PeerItemOrder{} peers[b.DestPeerID] = []*PeerItemOrder{}
} }
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{ peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
ResourceType: b.ResourceType,
Quantity: 1,
Item: b.PricedItem, Item: b.PricedItem,
}) })
} }
@@ -136,6 +139,22 @@ type PeerOrder struct {
Total float64 `json:"total,omitempty" bson:"total,omitempty"` Total float64 `json:"total,omitempty" bson:"total,omitempty"`
} }
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
switch dt {
case tools.PROCESSING_RESOURCE:
return &resources.PricedProcessingResource{}
case tools.STORAGE_RESOURCE:
return &resources.PricedStorageResource{}
case tools.DATA_RESOURCE:
return &resources.PricedDataResource{}
case tools.COMPUTE_RESOURCE:
return &resources.PricedComputeResource{}
case tools.WORKFLOW_RESOURCE:
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
}
return nil
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) { func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING d.Status = enum.PENDING
@@ -145,7 +164,7 @@ func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID { if d.Status == enum.PAID {
for _, b := range d.Items { for _, b := range d.Items {
var priced *resources.PricedResource priced := PricedByType(b.ResourceType)
bb, _ := json.Marshal(b.Item) bb, _ := json.Marshal(b.Item)
json.Unmarshal(bb, priced) json.Unmarshal(bb, priced)
if !priced.IsPurchasable() { if !priced.IsPurchasable() {
@@ -179,6 +198,7 @@ func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
} }
type PeerItemOrder struct { type PeerItemOrder struct {
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"` Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"` Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"` Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
@@ -190,11 +210,10 @@ func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
return 0, nil return 0, nil
} }
/////////// ///////////
var priced *resources.PricedResource priced := PricedByType(d.ResourceType)
b, _ := json.Marshal(d.Item) b, _ := json.Marshal(d.Item)
err := json.Unmarshal(b, priced) err := json.Unmarshal(b, priced)
if err != nil { if err != nil {
fmt.Println(err)
return 0, err return 0, err
} }
accessor := purchase_resource.NewAccessor(request) accessor := purchase_resource.NewAccessor(request)

View File

@@ -1,63 +1,23 @@
package bill package bill
import ( import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type billMongoAccessor struct { type billMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Bill] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the billMongoAccessor // New creates a new instance of the billMongoAccessor
func NewAccessor(request *tools.APIRequest) *billMongoAccessor { func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
return &billMongoAccessor{ return &billMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Bill]{
Logger: logs.CreateLogger(tools.LIVE_DATACENTER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.BILL.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.LIVE_DATACENTER, Type: tools.BILL,
New: func() *Bill { return &Bill{} },
}, },
} }
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *billMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *billMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
// should verify if a source is existing...
return utils.GenericUpdateOne(set, id, a, &Bill{})
}
func (a *billMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Bill), a)
}
func (a *billMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Bill), a)
}
func (a *billMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Bill](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *billMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Bill](a.getExec(), isDraft, a)
}
func (a *billMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Bill](filters, search, (&Bill{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *billMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

@@ -0,0 +1,95 @@
package bill_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/bill"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ---- Bill model ----
func TestBill_StoreDraftDefault(t *testing.T) {
b := &bill.Bill{}
b.StoreDraftDefault()
assert.True(t, b.IsDraft)
}
func TestBill_CanDelete_Draft(t *testing.T) {
b := &bill.Bill{}
b.IsDraft = true
assert.True(t, b.CanDelete())
}
func TestBill_CanDelete_NonDraft(t *testing.T) {
b := &bill.Bill{}
b.IsDraft = false
assert.False(t, b.CanDelete())
}
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING}
b.IsDraft = false
set := &bill.Bill{Status: enum.PAID}
ok, returned := b.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.PAID, returned.(*bill.Bill).Status)
}
func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING}
b.IsDraft = false
set := &bill.Bill{Status: enum.PENDING}
ok, _ := b.CanUpdate(set)
assert.False(t, ok)
}
func TestBill_CanUpdate_Draft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING}
b.IsDraft = true
set := &bill.Bill{Status: enum.PAID}
ok, _ := b.CanUpdate(set)
assert.True(t, ok)
}
func TestBill_GetAccessor(t *testing.T) {
b := &bill.Bill{}
acc := b.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestBill_GetAccessor_NilRequest(t *testing.T) {
b := &bill.Bill{}
acc := b.GetAccessor(nil)
assert.NotNil(t, acc)
}
// ---- GenerateBill ----
func TestGenerateBill_Basic(t *testing.T) {
o := &order.Order{
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
}
req := &tools.APIRequest{PeerID: "peer-abc"}
b, err := bill.GenerateBill(o, req)
require.NoError(t, err)
assert.NotNil(t, b)
assert.Equal(t, "order-uuid-1", b.OrderID)
assert.Equal(t, enum.PENDING, b.Status)
assert.False(t, b.IsDraft)
assert.Contains(t, b.Name, "peer-abc")
}
// ---- SumUpBill ----
func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
b := &bill.Bill{Total: 0}
result, err := b.SumUpBill(nil)
require.NoError(t, err)
assert.Equal(t, 0.0, result.Total)
}

View File

@@ -3,12 +3,10 @@ package booking
import ( import (
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "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/common/models"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
/* /*
@@ -25,7 +23,7 @@ type Booking struct {
DestPeerID string `json:"dest_peer_id,omitempty" bson:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer DestPeerID string `json:"dest_peer_id,omitempty" bson:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer
WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"` ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`
State enum.BookingStatus `json:"state,omitempty" bson:"state,omitempty" validate:"required"` // State is the state of the booking State enum.BookingStatus `json:"state" bson:"state"` // State is the state of the booking
ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking
@@ -34,6 +32,11 @@ type Booking struct {
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty" validate:"required"` // ResourceType is the type of the resource ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty" validate:"required"` // ResourceType is the type of the resource
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty" validate:"required"` // could be a Compute or a Storage ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty" validate:"required"` // could be a Compute or a Storage
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
// Authorization: identifies who created this draft and the Check session it belongs to.
// Used to verify UPDATE and DELETE orders from remote schedulers.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
} }
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume { func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
@@ -63,40 +66,15 @@ func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResu
return m return m
} }
// CheckBooking checks if a booking is possible on a specific compute resource
func (wfa *Booking) Check(id string, start time.Time, end *time.Time, parrallelAllowed int) (bool, error) {
// check if
if end == nil {
// if no end... then Book like a savage
e := start.Add(time.Hour)
end = &e
}
accessor := NewAccessor(nil)
res, code, err := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{ // check if there is a booking on the same compute resource by filtering on the compute_resource_id, the state and the execution date
"resource_id": {{Operator: dbs.EQUAL.String(), Value: id}},
"state": {{Operator: dbs.EQUAL.String(), Value: enum.DRAFT.EnumIndex()}},
"expected_start_date": {
{Operator: dbs.LTE.String(), Value: primitive.NewDateTimeFromTime(*end)},
{Operator: dbs.GTE.String(), Value: primitive.NewDateTimeFromTime(start)},
},
},
}, "", wfa.IsDraft)
if code != 200 {
return false, err
}
return len(res) <= parrallelAllowed, nil
}
func (d *Booking) GetDelayForLaunch() time.Duration { func (d *Booking) GetDelayForLaunch() time.Duration {
return d.RealStartDate.Sub(d.ExpectedStartDate) return d.RealStartDate.Sub(d.ExpectedStartDate)
} }
func (d *Booking) GetDelayForFinishing() time.Duration { func (d *Booking) GetDelayForFinishing() time.Duration {
if d.ExpectedEndDate == nil { if d.ExpectedEndDate == nil || d.RealEndDate == nil {
return time.Duration(0) return time.Duration(0)
} }
return d.RealEndDate.Sub(d.ExpectedStartDate) return d.RealEndDate.Sub(*d.ExpectedEndDate)
} }
func (d *Booking) GetUsualDuration() time.Duration { func (d *Booking) GetUsualDuration() time.Duration {
@@ -123,18 +101,25 @@ func (d *Booking) VerifyAuth(callName string, request *tools.APIRequest) bool {
} }
func (r *Booking) StoreDraftDefault() { func (r *Booking) StoreDraftDefault() {
r.IsDraft = false r.IsDraft = true
r.State = enum.DRAFT
} }
func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) { func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.State != set.(*Booking).State || r.RealStartDate != set.(*Booking).RealStartDate || r.RealEndDate != set.(*Booking).RealEndDate { incoming := set.(*Booking)
return true, &Booking{ if !r.IsDraft && r.State != incoming.State || r.RealStartDate != incoming.RealStartDate || r.RealEndDate != incoming.RealEndDate {
State: set.(*Booking).State, patch := &Booking{
RealStartDate: set.(*Booking).RealStartDate, State: incoming.State,
RealEndDate: set.(*Booking).RealEndDate, RealStartDate: incoming.RealStartDate,
} // only state can be updated RealEndDate: incoming.RealEndDate,
}
// Auto-set RealStartDate when transitioning to STARTED and not already set
if r.State != enum.STARTED && incoming.State == enum.STARTED && patch.RealStartDate == nil {
now := time.Now()
patch.RealStartDate = &now
}
return true, patch
} }
// TODO : HERE WE CAN HANDLE THE CASE WHERE THE BOOKING IS DELAYED OR EXCEEDING OR ending sooner
return r.IsDraft, set return r.IsDraft, set
} }

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs/mongo"
"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/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
@@ -12,16 +12,17 @@ import (
) )
type BookingMongoAccessor struct { type BookingMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Booking] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the BookingMongoAccessor // New creates a new instance of the BookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor { func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
return &BookingMongoAccessor{ return &BookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Booking]{
Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.BOOKING, Type: tools.BOOKING,
New: func() *Booking { return &Booking{} },
}, },
} }
} }
@@ -29,32 +30,25 @@ func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (a *BookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { func (a *BookingMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a) if set["state"] == nil {
}
func (a *BookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if set.(*Booking).State == 0 {
return nil, 400, errors.New("state is required") return nil, 400, errors.New("state is required")
} }
realSet := &Booking{State: set.(*Booking).State} set = map[string]interface{}{
return utils.GenericUpdateOne(realSet, id, a, &Booking{}) "state": set["state"],
} }
return utils.GenericUpdateOne(set, id, a)
func (a *BookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *BookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
} }
func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now() now := time.Now()
now = now.Add(time.Second * -60) now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
return utils.GenericDeleteOne(d.GetID(), a) // Direct raw delete to avoid infinite recursion:
// GenericDeleteOne calls a.LoadOne which would re-enter this callback.
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
return nil, 410, errors.New("draft booking expired and deleted")
} }
if (d.(*Booking).ExpectedEndDate) == nil { if (d.(*Booking).ExpectedEndDate) == nil {
d.(*Booking).State = enum.FORGOTTEN d.(*Booking).State = enum.FORGOTTEN
@@ -67,20 +61,13 @@ func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
}, a) }, a)
} }
func (a *BookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { func (a *BookingMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return utils.GenericLoadAll[*Booking](a.getExec(), isDraft, a)
}
func (a *BookingMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Booking](filters, search, (&Booking{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *BookingMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now() now := time.Now()
now = now.Add(time.Second * -60) now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
utils.GenericDeleteOne(d.GetID(), a) // Direct raw delete to avoid infinite recursion (same as LoadOne callback).
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
return nil return nil
} }
if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) {

View File

@@ -0,0 +1,498 @@
package planner
import (
"encoding/json"
"sort"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"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/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
// InstanceCapacity holds the maximum available resources of a single resource instance.
type InstanceCapacity struct {
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
}
// ResourceRequest describes the resource amounts needed for a prospective booking.
// A nil map or nil pointer for a dimension means "use the full instance capacity" for that dimension.
type ResourceRequest struct {
CPUCores map[string]float64 // model -> cores needed (nil = max)
GPUMemGB map[string]float64 // model -> memory GB needed (nil = max)
RAMGB *float64 // GB needed (nil = max)
StorageGB *float64 // GB needed (nil = max)
}
// PlannerSlot represents a single booking occupying a resource instance during a time window.
// Usage maps each resource dimension (cpu_<model>, gpu_<model>, ram, storage) to
// its percentage of consumption relative to the instance's maximum capacity (0100).
type PlannerSlot struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
InstanceID string `json:"instance_id,omitempty"` // instance targeted by this booking
BookingID string `json:"booking_id,omitempty"` // empty in shallow mode
Usage map[string]float64 `json:"usage,omitempty"` // dimension -> % of max (0-100)
}
// PlannerITF is the interface used by Planify to check resource availability.
// *Planner satisfies this interface.
type PlannerITF interface {
NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time
}
// Planner is a volatile (non-persisted) object that organises bookings by resource.
// Only ComputeResource and StorageResource bookings appear in the schedule.
type Planner struct {
GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
}
// Generate builds a full Planner from all active bookings.
// Each slot includes the booking ID, the instance ID, and the usage percentage of every resource dimension.
func Generate(request *tools.APIRequest) (*Planner, error) {
return generate(request, false)
}
// GenerateShallow builds a Planner from all active bookings without booking IDs.
func GenerateShallow(request *tools.APIRequest) (*Planner, error) {
return generate(request, true)
}
func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
accessor := booking.NewAccessor(request)
// Include both confirmed (IsDraft=false) and draft (IsDraft=true) bookings
// so the planner reflects the full picture: first-come first-served on all
// pending reservations regardless of confirmation state.
confirmed, code, err := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
},
}, "*", false)
if code != 200 || err != nil {
return nil, err
}
drafts, _, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
},
}, "*", true)
bookings := append(confirmed, drafts...)
p := &Planner{
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
}
for _, b := range bookings {
bk := b.(*booking.Booking)
// Skip terminal bookings — they no longer occupy capacity.
switch bk.State {
case enum.SUCCESS, enum.FAILURE, enum.FORGOTTEN, enum.CANCELLED:
continue
}
// Only compute and storage resources are eligible
if bk.ResourceType != tools.COMPUTE_RESOURCE && bk.ResourceType != tools.STORAGE_RESOURCE {
continue
}
end := bk.ExpectedEndDate
if end == nil {
e := bk.ExpectedStartDate.UTC().Add(5 * time.Minute)
end = &e
}
instanceID, usage, cap := extractSlotData(bk, request)
if instanceID == "" {
instanceID = bk.InstanceID
}
if cap != nil && instanceID != "" {
if p.Capacities[bk.ResourceID] == nil {
p.Capacities[bk.ResourceID] = map[string]*InstanceCapacity{}
}
p.Capacities[bk.ResourceID][instanceID] = cap
}
slot := &PlannerSlot{
Start: bk.ExpectedStartDate,
End: *end,
InstanceID: instanceID,
Usage: usage,
}
if !shallow {
slot.BookingID = bk.GetID()
}
p.Schedule[bk.ResourceID] = append(p.Schedule[bk.ResourceID], slot)
}
return p, nil
}
// Check reports whether the requested time window has enough remaining capacity
// on the specified instance of the given resource.
//
// req describes the amounts needed; nil fields default to the full instance capacity.
// If req itself is nil, the full capacity of every dimension is assumed.
// If end is nil, a 1-hour window from start is assumed.
//
// A slot that overlaps the requested window is acceptable if, for every requested
// dimension, existing usage + requested usage ≤ 100 %.
// Slots targeting other instances are ignored.
// If no capacity is known for this instance (never booked), it is fully available.
func (p *Planner) Check(resourceID string, instanceID string, req *ResourceRequest, start time.Time, end *time.Time) bool {
if end == nil {
e := start.Add(5 * time.Minute)
end = &e
}
cap := p.instanceCapacity(resourceID, instanceID)
reqPct := toPercentages(req, cap)
slots, ok := p.Schedule[resourceID]
if !ok {
return true
}
for _, slot := range slots {
// Only consider slots on the same instance
if slot.InstanceID != instanceID {
continue
}
// Only consider overlapping slots
if !slot.Start.Before(*end) || !slot.End.After(start) {
continue
}
// If capacity is unknown (reqPct empty), any overlap blocks the slot.
if len(reqPct) == 0 {
return false
}
// Combined usage must not exceed 100 % for any requested dimension
for dim, needed := range reqPct {
if slot.Usage[dim]+needed >= 100.0 {
return false
}
}
}
return true
}
// instanceCapacity returns the stored max capacity for a resource/instance pair.
// Returns an empty (but non-nil) capacity when the instance has never been booked.
func (p *Planner) instanceCapacity(resourceID, instanceID string) *InstanceCapacity {
if instances, ok := p.Capacities[resourceID]; ok {
if c, ok := instances[instanceID]; ok {
return c
}
}
return &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
}
}
// toPercentages converts a ResourceRequest into a map of dimension -> percentage-of-max.
// nil fields in req (or nil req) are treated as requesting the full capacity (100 %).
func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float64 {
pct := map[string]float64{}
if req == nil {
for model := range cap.CPUCores {
pct["cpu_"+model] = 100.0
}
for model := range cap.GPUMemGB {
pct["gpu_"+model] = 100.0
}
if cap.RAMGB > 0 {
pct["ram"] = 100.0
}
if cap.StorageGB > 0 {
pct["storage"] = 100.0
}
return pct
}
if req.CPUCores == nil {
for model, maxCores := range cap.CPUCores {
if maxCores > 0 {
pct["cpu_"+model] = 100.0
}
}
} else {
for model, needed := range req.CPUCores {
if maxCores, ok := cap.CPUCores[model]; ok && maxCores > 0 {
pct["cpu_"+model] = (needed / maxCores) * 100.0
}
}
}
if req.GPUMemGB == nil {
for model, maxMem := range cap.GPUMemGB {
if maxMem > 0 {
pct["gpu_"+model] = 100.0
}
}
} else {
for model, needed := range req.GPUMemGB {
if maxMem, ok := cap.GPUMemGB[model]; ok && maxMem > 0 {
pct["gpu_"+model] = (needed / maxMem) * 100.0
}
}
}
if req.RAMGB == nil {
if cap.RAMGB > 0 {
pct["ram"] = 100.0
}
} else if cap.RAMGB > 0 {
pct["ram"] = (*req.RAMGB / cap.RAMGB) * 100.0
}
if req.StorageGB == nil {
if cap.StorageGB > 0 {
pct["storage"] = 100.0
}
} else if cap.StorageGB > 0 {
pct["storage"] = (*req.StorageGB / cap.StorageGB) * 100.0
}
return pct
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
// extractSlotData parses the booking's PricedItem, loads the corresponding resource,
// and returns the instance ID, usage percentages, and instance capacity in a single pass.
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
usage = map[string]float64{}
if len(bk.PricedItem) == 0 {
return
}
b, err := json.Marshal(bk.PricedItem)
if err != nil {
return
}
switch bk.ResourceType {
case tools.COMPUTE_RESOURCE:
instanceID, usage, cap = extractComputeSlot(b, bk.ResourceID, request)
case tools.STORAGE_RESOURCE:
instanceID, usage, cap = extractStorageSlot(b, bk.ResourceID, request)
}
return
}
// extractComputeSlot extracts the instance ID, usage percentages, and max capacity for a compute booking.
// Keys in usage: "cpu_<model>", "gpu_<model>", "ram".
func extractComputeSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
usage = map[string]float64{}
var priced resources.PricedComputeResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
return
}
res, _, err := (&resources.ComputeResource{}).GetAccessor(request).LoadOne(resourceID)
if err != nil {
return
}
compute := res.(*resources.ComputeResource)
instance := findComputeInstance(compute, priced.InstancesRefs)
if instance == nil {
return
}
instanceID = instance.GetID()
// Build the instance's maximum capacity
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
RAMGB: totalRAM(instance),
}
for model := range instance.CPUs {
cap.CPUCores[model] = totalCPUCores(instance, model)
}
for model := range instance.GPUs {
cap.GPUMemGB[model] = totalGPUMemory(instance, model)
}
// Compute usage as a percentage of the instance's maximum capacity
for model, usedCores := range priced.CPUsLocated {
if maxCores := cap.CPUCores[model]; maxCores > 0 {
usage["cpu_"+model] = (usedCores / maxCores) * 100.0
}
}
for model, usedMem := range priced.GPUsLocated {
if maxMem := cap.GPUMemGB[model]; maxMem > 0 {
usage["gpu_"+model] = (usedMem / maxMem) * 100.0
}
}
if cap.RAMGB > 0 && priced.RAMLocated > 0 {
usage["ram"] = (priced.RAMLocated / cap.RAMGB) * 100.0
}
return
}
// extractStorageSlot extracts the instance ID, usage percentages, and max capacity for a storage booking.
// Key in usage: "storage".
func extractStorageSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
usage = map[string]float64{}
var priced resources.PricedStorageResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
return
}
res, _, err := (&resources.StorageResource{}).GetAccessor(request).LoadOne(resourceID)
if err != nil {
return
}
storage := res.(*resources.StorageResource)
instance := findStorageInstance(storage, priced.InstancesRefs)
if instance == nil {
return
}
instanceID = instance.GetID()
maxStorage := float64(instance.SizeGB)
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
StorageGB: maxStorage,
}
if maxStorage > 0 && priced.UsageStorageGB > 0 {
usage["storage"] = (priced.UsageStorageGB / maxStorage) * 100.0
}
return
}
// findComputeInstance returns the instance referenced by the priced item's InstancesRefs,
// falling back to the first available instance.
func findComputeInstance(compute *resources.ComputeResource, refs map[string]string) *resources.ComputeResourceInstance {
for _, inst := range compute.Instances {
if _, ok := refs[inst.GetID()]; ok {
return inst
}
}
if len(compute.Instances) > 0 {
return compute.Instances[0]
}
return nil
}
// findStorageInstance returns the instance referenced by the priced item's InstancesRefs,
// falling back to the first available instance.
func findStorageInstance(storage *resources.StorageResource, refs map[string]string) *resources.StorageResourceInstance {
for _, inst := range storage.Instances {
if _, ok := refs[inst.GetID()]; ok {
return inst
}
}
if len(storage.Instances) > 0 {
return storage.Instances[0]
}
return nil
}
// totalCPUCores returns the total number of cores for a given CPU model across all nodes.
// It multiplies the per-chip core count (from the instance's CPU spec) by the total
// number of chips of that model across all nodes (chip_count × node.Quantity).
// Falls back to the spec's core count if no nodes are defined.
func totalCPUCores(instance *resources.ComputeResourceInstance, model string) float64 {
spec, ok := instance.CPUs[model]
if !ok || spec == nil || spec.Cores == 0 {
return 0
}
if len(instance.Nodes) == 0 {
return float64(spec.Cores)
}
totalChips := int64(0)
for _, node := range instance.Nodes {
if chipCount, ok := node.CPUs[model]; ok {
totalChips += chipCount * max(node.Quantity, 1)
}
}
if totalChips == 0 {
return float64(spec.Cores)
}
return float64(totalChips * int64(spec.Cores))
}
// totalGPUMemory returns the total GPU memory (GB) for a given model across all nodes.
// Falls back to the spec's memory if no nodes are defined.
func totalGPUMemory(instance *resources.ComputeResourceInstance, model string) float64 {
spec, ok := instance.GPUs[model]
if !ok || spec == nil || spec.MemoryGb == 0 {
return 0
}
if len(instance.Nodes) == 0 {
return spec.MemoryGb
}
totalUnits := int64(0)
for _, node := range instance.Nodes {
if unitCount, ok := node.GPUs[model]; ok {
totalUnits += unitCount * max(node.Quantity, 1)
}
}
if totalUnits == 0 {
return spec.MemoryGb
}
return float64(totalUnits) * spec.MemoryGb
}
// totalRAM returns the total RAM (GB) across all nodes of a compute instance.
func totalRAM(instance *resources.ComputeResourceInstance) float64 {
total := float64(0)
for _, node := range instance.Nodes {
if node.RAM != nil && node.RAM.SizeGb > 0 {
total += node.RAM.SizeGb * float64(max(node.Quantity, 1))
}
}
return total
}
// NextAvailableStart returns the earliest time >= start when resourceID/instanceID has a
// free window of duration d. Slots are scanned in order so a single linear pass suffices.
// If the planner has no slots for this resource/instance, start is returned unchanged.
func (p *Planner) NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time {
slots := p.Schedule[resourceID]
if len(slots) == 0 {
return start
}
// Collect and sort slots for this instance by start time.
relevant := make([]*PlannerSlot, 0, len(slots))
for _, s := range slots {
if s.InstanceID == instanceID {
relevant = append(relevant, s)
}
}
sort.Slice(relevant, func(i, j int) bool { return relevant[i].Start.Before(relevant[j].Start) })
end := start.Add(d)
for _, slot := range relevant {
if !slot.Start.Before(end) {
break // all remaining slots start after our window — done
}
if slot.End.After(start) {
// conflict: push start to after this slot
start = slot.End
end = start.Add(d)
}
}
return start
}

View File

@@ -13,8 +13,8 @@ import (
) )
func TestBooking_GetDurations(t *testing.T) { func TestBooking_GetDurations(t *testing.T) {
start := time.Now().Add(-2 * time.Hour) start := time.Now().Add(-10 * time.Minute)
end := start.Add(1 * time.Hour) end := start.Add(5 * time.Minute)
realStart := start.Add(30 * time.Minute) realStart := start.Add(30 * time.Minute)
realEnd := realStart.Add(90 * time.Minute) realEnd := realStart.Add(90 * time.Minute)

View File

@@ -91,10 +91,6 @@ func (d *CollaborativeArea) GetAccessor(request *tools.APIRequest) utils.Accesso
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request) // Create a new instance of the accessor
} }
func (d *CollaborativeArea) Trim() *CollaborativeArea {
return d
}
func (d *CollaborativeArea) StoreDraftDefault() { func (d *CollaborativeArea) StoreDraftDefault() {
d.AllowedPeersGroup = map[string][]string{ d.AllowedPeersGroup = map[string][]string{
d.CreatorID: {"*"}, d.CreatorID: {"*"},

View File

@@ -17,7 +17,7 @@ import (
// SharedWorkspace is a struct that represents a collaborative area // SharedWorkspace is a struct that represents a collaborative area
type collaborativeAreaMongoAccessor struct { type collaborativeAreaMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*CollaborativeArea] // AbstractAccessor contains the basic fields of an accessor (model, caller)
workspaceAccessor utils.Accessor workspaceAccessor utils.Accessor
workflowAccessor utils.Accessor workflowAccessor utils.Accessor
@@ -27,10 +27,11 @@ type collaborativeAreaMongoAccessor struct {
func NewAccessor(request *tools.APIRequest) *collaborativeAreaMongoAccessor { func NewAccessor(request *tools.APIRequest) *collaborativeAreaMongoAccessor {
return &collaborativeAreaMongoAccessor{ return &collaborativeAreaMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*CollaborativeArea]{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.COLLABORATIVE_AREA, Type: tools.COLLABORATIVE_AREA,
New: func() *CollaborativeArea { return &CollaborativeArea{} },
}, },
workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request), workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request),
workflowAccessor: (&w.Workflow{}).GetAccessor(request), workflowAccessor: (&w.Workflow{}).GetAccessor(request),
@@ -52,8 +53,8 @@ func (a *collaborativeAreaMongoAccessor) DeleteOne(id string) (utils.DBObject, i
} }
// UpdateOne updates a collaborative area in the database, given its ID and the new data, it automatically share to peers if the workspace is shared // UpdateOne updates a collaborative area in the database, given its ID and the new data, it automatically share to peers if the workspace is shared
func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func (a *collaborativeAreaMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
res, code, err := utils.GenericUpdateOne(set.(*CollaborativeArea).Trim(), id, a, &CollaborativeArea{}) res, code, err := utils.GenericUpdateOne(set, id, a)
// a.deleteToPeer(res.(*CollaborativeArea)) // delete the collaborative area on the peer // a.deleteToPeer(res.(*CollaborativeArea)) // delete the collaborative area on the peer
a.sharedWorkflow(res.(*CollaborativeArea), id) // replace all shared workflows a.sharedWorkflow(res.(*CollaborativeArea), id) // replace all shared workflows
a.sharedWorkspace(res.(*CollaborativeArea), id) // replace all collaborative areas (not shared worspace obj but workspace one) a.sharedWorkspace(res.(*CollaborativeArea), id) // replace all collaborative areas (not shared worspace obj but workspace one)
@@ -63,19 +64,19 @@ func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string
// StoreOne stores a collaborative area in the database, it automatically share to peers if the workspace is shared // StoreOne stores a collaborative area in the database, it automatically share to peers if the workspace is shared
func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
id, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{ pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true, Admin: true,
})) // get the local peer })) // get the local peer
if err != nil { if err != nil || pp == nil {
return data, 404, err return data, 404, err
} }
data.(*CollaborativeArea).Clear(id) // set the creator data.(*CollaborativeArea).Clear(pp.GetID()) // set the creator
// retrieve or proper peer // retrieve or proper peer
if data.(*CollaborativeArea).CollaborativeAreaRule != nil { if data.(*CollaborativeArea).CollaborativeAreaRule != nil {
data.(*CollaborativeArea).CollaborativeAreaRule = &CollaborativeAreaRule{} data.(*CollaborativeArea).CollaborativeAreaRule = &CollaborativeAreaRule{}
} }
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = id data.(*CollaborativeArea).CollaborativeAreaRule.Creator = pp.GetID()
d, code, err := utils.GenericStoreOne(data.(*CollaborativeArea).Trim(), a) d, code, err := utils.GenericStoreOne(data, a)
if code == 200 { if code == 200 {
a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows
a.sharedWorkspace(d.(*CollaborativeArea), d.GetID()) // create all collaborative areas a.sharedWorkspace(d.(*CollaborativeArea), d.GetID()) // create all collaborative areas
@@ -84,11 +85,6 @@ func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DB
return data, code, err return data, code, err
} }
// CopyOne copies a CollaborativeArea in the database
func (a *collaborativeAreaMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return a.StoreOne(data)
}
func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils.Accessor) []T { func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils.Accessor) []T {
var new []T var new []T
res, code, _ := a.Search(&dbs.Filters{ res, code, _ := a.Search(&dbs.Filters{
@@ -96,7 +92,6 @@ func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils
"abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}}, "abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}},
}, },
}, "", isDrafted) }, "", isDrafted)
fmt.Println(res, arr, isDrafted, a)
if code == 200 { if code == 200 {
for _, r := range res { for _, r := range res {
new = append(new, r.(T)) new = append(new, r.(T))
@@ -130,23 +125,10 @@ func (a *collaborativeAreaMongoAccessor) enrich(sharedWorkspace *CollaborativeAr
return sharedWorkspace return sharedWorkspace
} }
func (a *collaborativeAreaMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *collaborativeAreaMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return utils.GenericLoadOne[*CollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) { return func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), false, a.Request), 200, nil return a.enrich(d.(*CollaborativeArea), isDraft, a.Request)
}, a)
} }
func (a *collaborativeAreaMongoAccessor) LoadAll(isDrafted bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*CollaborativeArea](func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), isDrafted, a.Request)
}, isDrafted, a)
}
func (a *collaborativeAreaMongoAccessor) Search(filters *dbs.Filters, search string, isDrafted bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*CollaborativeArea](filters, search, (&CollaborativeArea{}).GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), isDrafted, a.Request)
}, isDrafted, a)
} }
/* /*
@@ -158,7 +140,9 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
eld := eldest.(*CollaborativeArea) eld := eldest.(*CollaborativeArea)
if eld.Workspaces != nil { // update all your workspaces in the eldest by replacing shared ref by an empty string if eld.Workspaces != nil { // update all your workspaces in the eldest by replacing shared ref by an empty string
for _, v := range eld.Workspaces { for _, v := range eld.Workspaces {
a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: ""}, v) a.workspaceAccessor.UpdateOne(map[string]interface{}{
"shared": "",
}, v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
continue continue
} }
@@ -174,7 +158,10 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
} }
if shared.Workspaces != nil { if shared.Workspaces != nil {
for _, v := range shared.Workspaces { // update all the collaborative areas for _, v := range shared.Workspaces { // update all the collaborative areas
workspace, code, _ := a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: shared.UUID}, v) // add the shared ref to workspace workspace, code, _ := a.workspaceAccessor.UpdateOne(
map[string]interface{}{
"shared": shared.UUID,
}, v) // add the shared ref to workspace
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
continue continue
} }
@@ -214,7 +201,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
} // kick the shared reference in your old shared workflow } // kick the shared reference in your old shared workflow
n := &w.Workflow{} n := &w.Workflow{}
n.Shared = new n.Shared = new
a.workflowAccessor.UpdateOne(n, v) a.workflowAccessor.UpdateOne(n.Serialize(n), v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
continue continue
} }
@@ -236,7 +223,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
s := data.(*w.Workflow) s := data.(*w.Workflow)
if !slices.Contains(s.Shared, id) { if !slices.Contains(s.Shared, id) {
s.Shared = append(s.Shared, id) s.Shared = append(s.Shared, id)
workflow, code, _ := a.workflowAccessor.UpdateOne(s, v) workflow, code, _ := a.workflowAccessor.UpdateOne(s.Serialize(s), v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
continue continue
} }
@@ -259,6 +246,8 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
// because you have no reference to the remote shared workflow // because you have no reference to the remote shared workflow
} }
// TODO it's a Shared API Problem with OC-DISCOVERY
// sharedWorkspace is a function that shares the collaborative area to the peers // sharedWorkspace is a function that shares the collaborative area to the peers
func (a *collaborativeAreaMongoAccessor) deleteToPeer(shared *CollaborativeArea) { func (a *collaborativeAreaMongoAccessor) deleteToPeer(shared *CollaborativeArea) {
a.contactPeer(shared, tools.POST) a.contactPeer(shared, tools.POST)

View File

@@ -1,62 +1,23 @@
package rule package rule
import ( import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type ruleMongoAccessor struct { type ruleMongoAccessor struct {
utils.AbstractAccessor utils.AbstractAccessor[*Rule]
} }
// New creates a new instance of the ruleMongoAccessor // New creates a new instance of the ruleMongoAccessor
func NewAccessor(request *tools.APIRequest) *ruleMongoAccessor { func NewAccessor(request *tools.APIRequest) *ruleMongoAccessor {
return &ruleMongoAccessor{ return &ruleMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Rule]{
Logger: logs.CreateLogger(tools.RULE.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.RULE.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.RULE, Type: tools.RULE,
New: func() *Rule { return &Rule{} },
}, },
} }
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *ruleMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *ruleMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &Rule{})
}
func (a *ruleMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *ruleMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *ruleMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Rule](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *ruleMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Rule](a.getExec(), isDraft, a)
}
func (a *ruleMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Rule](filters, search, (&Rule{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *ruleMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

@@ -1,56 +1,22 @@
package shallow_collaborative_area package shallow_collaborative_area
import ( import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type shallowSharedWorkspaceMongoAccessor struct { type shallowSharedWorkspaceMongoAccessor struct {
utils.AbstractAccessor utils.AbstractAccessor[*ShallowCollaborativeArea]
} }
func NewAccessor(request *tools.APIRequest) *shallowSharedWorkspaceMongoAccessor { func NewAccessor(request *tools.APIRequest) *shallowSharedWorkspaceMongoAccessor {
return &shallowSharedWorkspaceMongoAccessor{ return &shallowSharedWorkspaceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*ShallowCollaborativeArea]{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request, // Set the caller Request: request, // Set the caller
Type: tools.COLLABORATIVE_AREA, Type: tools.COLLABORATIVE_AREA,
New: func() *ShallowCollaborativeArea { return &ShallowCollaborativeArea{} },
}, },
} }
} }
func (a *shallowSharedWorkspaceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set.(*ShallowCollaborativeArea), id, a, &ShallowCollaborativeArea{})
}
func (a *shallowSharedWorkspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*ShallowCollaborativeArea), a)
}
func (a *shallowSharedWorkspaceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return a.StoreOne(data)
}
func (a *shallowSharedWorkspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*ShallowCollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*ShallowCollaborativeArea](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*ShallowCollaborativeArea](filters, search, (&ShallowCollaborativeArea{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, a)
}

View File

@@ -7,36 +7,36 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 { func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
near := float64(10000000000) // set a high value near := float64(-1) // unset sentinel
for _, items := range planned { // loop through the planned items for _, items := range planned { // loop through the planned items
for _, priced := range items { // loop through the priced items for _, priced := range items { // loop through the priced items
if priced.GetLocationStart() == nil { // if the start is nil, if priced.GetLocationStart() == nil { // if the start is nil,
continue // skip the iteration continue // skip the iteration
} }
newS := priced.GetLocationStart() // get the start newS := priced.GetLocationStart() // get the start
if newS.Sub(start).Seconds() < near { // if the difference between the start and the new start is less than the nearest start diff := newS.Sub(start).Seconds() // get the difference
near = newS.Sub(start).Seconds() if near < 0 || diff < near { // if the difference is less than the nearest start
near = diff
} }
} }
} }
if near < 0 {
return 0 // no items found, start at the given start time
}
return near return near
} }
func GetPlannerLongestTime(end *time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 { // GetPlannerLongestTime returns the sum of all processing durations (conservative estimate).
if end == nil { // Returns -1 if any processing is a service (open-ended).
return -1 func GetPlannerLongestTime(planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
}
longestTime := float64(0) longestTime := float64(0)
for _, priced := range planned[tools.PROCESSING_RESOURCE] { for _, priced := range planned[tools.PROCESSING_RESOURCE] {
if priced.GetLocationEnd() == nil { d := priced.GetExplicitDurationInS()
continue if d < 0 {
return -1 // service present: booking is open-ended
} }
newS := priced.GetLocationEnd() longestTime += d
if end == nil && longestTime < newS.Sub(*end).Seconds() {
longestTime = newS.Sub(*end).Seconds()
}
// get the nearest start from start var
} }
return longestTime return longestTime
} }

View File

@@ -9,6 +9,7 @@ import (
type PricedItemITF interface { type PricedItemITF interface {
GetID() string GetID() string
GetInstanceID() string
GetType() tools.DataType GetType() tools.DataType
IsPurchasable() bool IsPurchasable() bool
IsBooked() bool IsBooked() bool

View File

@@ -59,8 +59,8 @@ func GetDefaultPricingProfile() PricingProfileITF {
Pricing: PricingStrategy[TimePricingStrategy]{ Pricing: PricingStrategy[TimePricingStrategy]{
Price: 0, Price: 0,
Currency: "EUR", Currency: "EUR",
BuyingStrategy: PERMANENT, BuyingStrategy: SUBSCRIPTION,
TimePricingStrategy: ONCE, TimePricingStrategy: PER_SECOND,
}, },
} }
} }

View File

@@ -41,14 +41,14 @@ type BuyingStrategy int
// should except... on // should except... on
const ( const (
PERMANENT BuyingStrategy = iota // is a permanent buying ( predictible ) SUBSCRIPTION BuyingStrategy = iota // is a permanent buying ( predictible )
UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible ) UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible )
SUBSCRIPTION // a defined subscription ( predictible ) PERMANENT // a defined subscription ( predictible )
// PAY_PER_USE // per request. ( unpredictible ) // PAY_PER_USE // per request. ( unpredictible )
) )
func (t BuyingStrategy) String() string { func (t BuyingStrategy) String() string {
return [...]string{"PERMANENT", "UNDEFINED_SUBSCRIPTION", "SUBSCRIPTION"}[t] return [...]string{"SUBSCRIPTION", "UNDEFINED_SUBSCRIPTION", "PERMANENT"}[t]
} }
func (t BuyingStrategy) IsBillingStrategyAllowed(bs BillingStrategy) (BillingStrategy, bool) { func (t BuyingStrategy) IsBillingStrategyAllowed(bs BillingStrategy) (BillingStrategy, bool) {
@@ -65,7 +65,7 @@ func (t BuyingStrategy) IsBillingStrategyAllowed(bs BillingStrategy) (BillingStr
} }
func BuyingStrategyList() []BuyingStrategy { func BuyingStrategyList() []BuyingStrategy {
return []BuyingStrategy{PERMANENT, UNDEFINED_SUBSCRIPTION, SUBSCRIPTION} return []BuyingStrategy{SUBSCRIPTION, UNDEFINED_SUBSCRIPTION, PERMANENT}
} }
type Strategy interface { type Strategy interface {
@@ -85,10 +85,6 @@ const (
PER_MONTH PER_MONTH
) )
func IsTimeStrategy(i int) bool {
return len(TimePricingStrategyList()) < i
}
func (t TimePricingStrategy) String() string { func (t TimePricingStrategy) String() string {
return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t] return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t]
} }
@@ -116,7 +112,7 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
fromAverageDuration := after.Sub(now).Seconds() fromAverageDuration := after.Sub(now).Seconds()
var tEnd time.Time var tEnd time.Time
if end == nil { if end == nil {
tEnd = start.Add(1 * time.Hour) tEnd = start.Add(5 * time.Minute)
} else { } else {
tEnd = *end tEnd = *end
} }
@@ -164,7 +160,8 @@ type PricingStrategy[T Strategy] struct {
} }
func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time, variations []*PricingVariation) (float64, error) { func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time, variations []*PricingVariation) (float64, error) {
if p.BuyingStrategy == SUBSCRIPTION { switch p.BuyingStrategy {
case SUBSCRIPTION:
price, err := BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end) price, err := BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -178,7 +175,7 @@ func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration
return p.Price, nil return p.Price, nil
} else if p.BuyingStrategy == PERMANENT { case PERMANENT:
if variations != nil { if variations != nil {
price := p.Price price := p.Price
for _, v := range variations { for _, v := range variations {

View File

@@ -15,9 +15,9 @@ func (d DummyStrategy) GetStrategy() string { return "DUMMY" }
func (d DummyStrategy) GetStrategyValue() int { return int(d) } func (d DummyStrategy) GetStrategyValue() int { return int(d) }
func TestBuyingStrategy_String(t *testing.T) { func TestBuyingStrategy_String(t *testing.T) {
assert.Equal(t, "UNLIMITED", pricing.PERMANENT.String()) assert.Equal(t, "PERMANENT", pricing.PERMANENT.String())
assert.Equal(t, "UNDEFINED_SUBSCRIPTION", pricing.UNDEFINED_SUBSCRIPTION.String())
assert.Equal(t, "SUBSCRIPTION", pricing.SUBSCRIPTION.String()) assert.Equal(t, "SUBSCRIPTION", pricing.SUBSCRIPTION.String())
//assert.Equal(t, "PAY PER USE", pricing.PAY_PER_USE.String())
} }
func TestBuyingStrategyList(t *testing.T) { func TestBuyingStrategyList(t *testing.T) {
@@ -63,7 +63,7 @@ func Test_getAverageTimeInSecond_WithoutEnd(t *testing.T) {
func TestBookingEstimation(t *testing.T) { func TestBookingEstimation(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(2 * time.Hour) end := start.Add(10 * time.Minute)
strategies := map[pricing.TimePricingStrategy]float64{ strategies := map[pricing.TimePricingStrategy]float64{
pricing.ONCE: 50, pricing.ONCE: 50,
pricing.PER_HOUR: 10, pricing.PER_HOUR: 10,
@@ -102,7 +102,7 @@ func TestPricingStrategy_Getters(t *testing.T) {
func TestPricingStrategy_GetPriceHT(t *testing.T) { func TestPricingStrategy_GetPriceHT(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(1 * time.Hour) end := start.Add(5 * time.Minute)
// SUBSCRIPTION case // SUBSCRIPTION case
ps := pricing.PricingStrategy[DummyStrategy]{ ps := pricing.PricingStrategy[DummyStrategy]{
@@ -121,8 +121,8 @@ func TestPricingStrategy_GetPriceHT(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 5.0, p) assert.Equal(t, 5.0, p)
// PAY_PER_USE case // UNDEFINED_SUBSCRIPTION case: price * quantity
//ps.BuyingStrategy = pricing.PAY_PER_USE ps.BuyingStrategy = pricing.UNDEFINED_SUBSCRIPTION
p, err = ps.GetPriceHT(3, 0, start, &end, nil) p, err = ps.GetPriceHT(3, 0, start, &end, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 15.0, p) assert.Equal(t, 15.0, p)

View File

@@ -0,0 +1,31 @@
package execution_verification
import (
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* ExecutionVerification is a struct that represents a list of workflow executions
* Warning: No user can write (del, post, put) a workflow execution, it is only used by the system
* workflows generate their own executions
*/
type ExecutionVerification struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
WorkflowID string `json:"workflow_id" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
Payload string `json:"payload" bson:"payload,omitempty"`
IsVerified bool `json:"is_verified" bson:"is_verified,omitempty"`
Validate bool `json:"validate" bson:"validate,omitempty"`
}
func (r *ExecutionVerification) StoreDraftDefault() {
r.IsDraft = false // TODO: TEMPORARY
}
func (d *ExecutionVerification) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *ExecutionVerification) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}

View File

@@ -0,0 +1,38 @@
package execution_verification
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type ExecutionVerificationMongoAccessor struct {
utils.AbstractAccessor[*ExecutionVerification]
shallow bool
}
func NewAccessor(request *tools.APIRequest) *ExecutionVerificationMongoAccessor {
return &ExecutionVerificationMongoAccessor{
shallow: false,
AbstractAccessor: utils.AbstractAccessor[*ExecutionVerification]{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request,
Type: tools.WORKFLOW_EXECUTION,
New: func() *ExecutionVerification { return &ExecutionVerification{} },
NotImplemented: []string{"DeleteOne", "StoreOne", "CopyOne"},
},
}
}
func (wfa *ExecutionVerificationMongoAccessor) StoreOne(set utils.DBObject) (utils.DBObject, int, error) {
set.(*ExecutionVerification).IsVerified = false
return utils.GenericStoreOne(set, wfa)
}
func (wfa *ExecutionVerificationMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
set = map[string]interface{}{
"is_verified": true,
"validate": set["validate"],
}
return utils.GenericUpdateOne(set, id, wfa)
}

View File

@@ -63,7 +63,7 @@ func (r *AbstractLive) GetResourceType() tools.DataType {
} }
func (r *AbstractLive) StoreDraftDefault() { func (r *AbstractLive) StoreDraftDefault() {
r.IsDraft = true r.IsDraft = false
} }
func (r *AbstractLive) CanDelete() bool { func (r *AbstractLive) CanDelete() bool {

View File

@@ -4,23 +4,31 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type computeUnitsMongoAccessor[T LiveInterface] struct { type liveMongoAccessor[T LiveInterface] struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[LiveInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the computeUnitsMongoAccessor // New creates a new instance of the computeUnitsMongoAccessor
func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *computeUnitsMongoAccessor[T] { func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *liveMongoAccessor[T] {
return &computeUnitsMongoAccessor[T]{ return &liveMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[LiveInterface]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request, Request: request,
Type: t, Type: t,
New: func() LiveInterface {
switch t {
case tools.LIVE_DATACENTER:
return &LiveDatacenter{}
case tools.LIVE_STORAGE:
return &LiveStorage{}
}
return &LiveDatacenter{}
},
}, },
} }
} }
@@ -28,28 +36,15 @@ func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (a *computeUnitsMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) { func (a *liveMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *computeUnitsMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
// should verify if a source is existing...
return utils.GenericUpdateOne(set, id, a, &LiveDatacenter{})
}
func (a *computeUnitsMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*LiveDatacenter), a)
}
func (a *computeUnitsMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
// is a publisher... that become a resources. // is a publisher... that become a resources.
if data.IsDrafted() { if data.IsDrafted() {
return nil, 422, errors.New("can't publish a drafted compute units") return nil, 422, errors.New("can't publish a drafted compute units")
} }
live := data.(T) live := data.(T)
if live.GetMonitorPath() == "" || live.GetID() != "" { /*if live.GetMonitorPath() == "" || live.GetID() != "" {
return nil, 422, errors.New("publishing is only allowed is it can be monitored and be accessible") return nil, 422, errors.New("publishing is only allowed is it can be monitored and be accessible")
} }*/
if res, code, err := a.LoadOne(live.GetID()); err != nil { if res, code, err := a.LoadOne(live.GetID()); err != nil {
return nil, code, err return nil, code, err
} else { } else {
@@ -62,7 +57,6 @@ func (a *computeUnitsMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObj
if len(live.GetResourcesID()) > 0 { if len(live.GetResourcesID()) > 0 {
for _, r := range live.GetResourcesID() { for _, r := range live.GetResourcesID() {
// TODO dependent of a existing resource
res, code, err := resAccess.LoadOne(r) res, code, err := resAccess.LoadOne(r)
if err == nil { if err == nil {
return nil, code, err return nil, code, err
@@ -71,7 +65,7 @@ func (a *computeUnitsMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObj
b, _ := json.Marshal(res) b, _ := json.Marshal(res)
json.Unmarshal(b, existingResource) json.Unmarshal(b, existingResource)
live.SetResourceInstance(existingResource, instance) live.SetResourceInstance(existingResource, instance)
resAccess.UpdateOne(existingResource, existingResource.GetID()) resAccess.UpdateOne(existingResource.Serialize(existingResource), existingResource.GetID())
} }
if live.GetID() != "" { if live.GetID() != "" {
return a.LoadOne(live.GetID()) return a.LoadOne(live.GetID())
@@ -83,35 +77,15 @@ func (a *computeUnitsMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObj
b, _ := json.Marshal(live) b, _ := json.Marshal(live)
json.Unmarshal(b, &r) json.Unmarshal(b, &r)
live.SetResourceInstance(r, instance) live.SetResourceInstance(r, instance)
res, code, err := resAccess.StoreOne(r) res, code, err := utils.GenericStoreOne(r, resAccess)
if err != nil { if err != nil {
return nil, code, err return nil, code, err
} }
live.SetResourcesID(res.GetID()) live.SetResourcesID(res.GetID())
if live.GetID() != "" { if live.GetID() != "" {
return a.UpdateOne(live, live.GetID()) return a.UpdateOne(live.Serialize(live), live.GetID())
} else { } else {
return a.StoreOne(live) return a.StoreOne(live)
} }
} }
} }
func (a *computeUnitsMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *computeUnitsMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](a.getExec(), isDraft, a)
}
func (a *computeUnitsMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*LiveDatacenter](filters, search, (&LiveDatacenter{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *computeUnitsMongoAccessor[T]) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

@@ -0,0 +1,144 @@
package live_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- AbstractLive (via LiveDatacenter embedding) ----
func TestAbstractLive_StoreDraftDefault(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.StoreDraftDefault()
assert.True(t, dc.IsDraft)
}
func TestAbstractLive_CanDelete_Draft(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.IsDraft = true
assert.True(t, dc.CanDelete())
}
func TestAbstractLive_CanDelete_NonDraft(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.IsDraft = false
assert.False(t, dc.CanDelete())
}
func TestAbstractLive_GetMonitorPath(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.MonitorPath = "/metrics"
assert.Equal(t, "/metrics", dc.GetMonitorPath())
}
func TestAbstractLive_GetResourcesID_Empty(t *testing.T) {
dc := &live.LiveDatacenter{}
assert.Empty(t, dc.GetResourcesID())
}
func TestAbstractLive_SetResourcesID_Append(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.SetResourcesID("res-1")
assert.Equal(t, []string{"res-1"}, dc.GetResourcesID())
}
func TestAbstractLive_SetResourcesID_NoDuplication(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.SetResourcesID("res-1")
dc.SetResourcesID("res-1") // second call should not duplicate
assert.Len(t, dc.GetResourcesID(), 1)
}
func TestAbstractLive_SetResourcesID_MultipleDistinct(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.SetResourcesID("res-1")
dc.SetResourcesID("res-2")
assert.Len(t, dc.GetResourcesID(), 2)
}
func TestAbstractLive_GetResourceType(t *testing.T) {
dc := &live.LiveDatacenter{}
assert.Equal(t, tools.INVALID, dc.GetResourceType())
}
// ---- LiveDatacenter ----
func TestLiveDatacenter_GetAccessor(t *testing.T) {
dc := &live.LiveDatacenter{}
acc := dc.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestLiveDatacenter_GetAccessor_NilRequest(t *testing.T) {
dc := &live.LiveDatacenter{}
acc := dc.GetAccessor(nil)
assert.NotNil(t, acc)
}
func TestLiveDatacenter_GetResource(t *testing.T) {
dc := &live.LiveDatacenter{}
res := dc.GetResource()
assert.NotNil(t, res)
}
func TestLiveDatacenter_GetResourceInstance(t *testing.T) {
dc := &live.LiveDatacenter{}
inst := dc.GetResourceInstance()
assert.NotNil(t, inst)
}
func TestLiveDatacenter_IDAndName(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.AbstractLive.AbstractObject = utils.AbstractObject{UUID: "dc-id", Name: "dc-name"}
assert.Equal(t, "dc-id", dc.GetID())
assert.Equal(t, "dc-name", dc.GetName())
}
// ---- LiveStorage ----
func TestLiveStorage_StoreDraftDefault(t *testing.T) {
s := &live.LiveStorage{}
s.StoreDraftDefault()
assert.True(t, s.IsDraft)
}
func TestLiveStorage_CanDelete_Draft(t *testing.T) {
s := &live.LiveStorage{}
s.IsDraft = true
assert.True(t, s.CanDelete())
}
func TestLiveStorage_CanDelete_NonDraft(t *testing.T) {
s := &live.LiveStorage{}
s.IsDraft = false
assert.False(t, s.CanDelete())
}
func TestLiveStorage_GetAccessor(t *testing.T) {
s := &live.LiveStorage{}
acc := s.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestLiveStorage_GetResource(t *testing.T) {
s := &live.LiveStorage{}
res := s.GetResource()
assert.NotNil(t, res)
}
func TestLiveStorage_GetResourceInstance(t *testing.T) {
s := &live.LiveStorage{}
inst := s.GetResourceInstance()
assert.NotNil(t, inst)
}
func TestLiveStorage_SetResourcesID_NoDuplication(t *testing.T) {
s := &live.LiveStorage{}
s.SetResourcesID("storage-1")
s.SetResourcesID("storage-1")
assert.Len(t, s.GetResourcesID(), 1)
}

View File

@@ -3,6 +3,7 @@ package models
import ( import (
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/bill" "cloud.o-forge.io/core/oc-lib/models/bill"
"cloud.o-forge.io/core/oc-lib/models/execution_verification"
"cloud.o-forge.io/core/oc-lib/models/live" "cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/order" "cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource" "cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
@@ -44,15 +45,21 @@ var ModelsCatalog = map[string]func() utils.DBObject{
tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} }, tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} },
tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} }, tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
tools.BILL.String(): func() utils.DBObject { return &bill.Bill{} }, tools.BILL.String(): func() utils.DBObject { return &bill.Bill{} },
tools.EXECUTION_VERIFICATION.String(): func() utils.DBObject { return &execution_verification.ExecutionVerification{} },
} }
// Model returns the model object based on the model type // Model returns the model object based on the model type
func Model(model int) utils.DBObject { func Model(model int) utils.DBObject {
log := logs.GetLogger() log := logs.GetLogger()
if _, ok := ModelsCatalog[tools.FromInt(model)]; ok { if model < 0 || model >= len(tools.Str) {
return ModelsCatalog[tools.FromInt(model)]() log.Error().Msg("Can't find model: index out of range")
return nil
} }
log.Error().Msg("Can't find model " + tools.FromInt(model) + ".") key := tools.FromInt(model)
if _, ok := ModelsCatalog[key]; ok {
return ModelsCatalog[key]()
}
log.Error().Msg("Can't find model " + key + ".")
return nil return nil
} }

View File

@@ -1,64 +1,24 @@
package order package order
import ( import (
"errors"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type orderMongoAccessor struct { type orderMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Order] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the orderMongoAccessor // New creates a new instance of the orderMongoAccessor
func NewAccessor(request *tools.APIRequest) *orderMongoAccessor { func NewAccessor(request *tools.APIRequest) *orderMongoAccessor {
return &orderMongoAccessor{ return &orderMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Order]{
Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.ORDER, Type: tools.ORDER,
New: func() *Order { return &Order{} },
NotImplemented: []string{"CopyOne"},
}, },
} }
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *orderMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *orderMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &Order{})
}
func (a *orderMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data,a)
}
func (a *orderMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("Not implemented")
}
func (a *orderMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Order](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *orderMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Order](a.getExec(), isDraft, a)
}
func (a *orderMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Order](filters, search, (&Order{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *orderMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

@@ -1 +1,92 @@
package tests package order_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- Order model ----
func TestOrder_StoreDraftDefault(t *testing.T) {
o := &order.Order{}
o.StoreDraftDefault()
assert.True(t, o.IsDraft)
}
func TestOrder_CanDelete_Draft(t *testing.T) {
o := &order.Order{}
o.IsDraft = true
assert.True(t, o.CanDelete())
}
func TestOrder_CanDelete_NonDraft(t *testing.T) {
o := &order.Order{}
o.IsDraft = false
assert.False(t, o.CanDelete())
}
func TestOrder_CanUpdate_StatusChange_NonDraft(t *testing.T) {
o := &order.Order{Status: enum.PENDING}
o.IsDraft = false
set := &order.Order{Status: enum.PAID}
ok, returned := o.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.PAID, returned.(*order.Order).Status)
}
func TestOrder_CanUpdate_SameStatus_NonDraft(t *testing.T) {
o := &order.Order{Status: enum.PENDING}
o.IsDraft = false
set := &order.Order{Status: enum.PENDING}
ok, _ := o.CanUpdate(set)
// !r.IsDraft && r.Status == set.Status → first branch false → returns r.IsDraft = false
assert.False(t, ok)
}
func TestOrder_CanUpdate_Draft(t *testing.T) {
o := &order.Order{Status: enum.PENDING}
o.IsDraft = true
set := &order.Order{Status: enum.PAID}
ok, _ := o.CanUpdate(set)
// !r.IsDraft = false → first branch false → returns r.IsDraft = true
assert.True(t, ok)
}
func TestOrder_Quantity(t *testing.T) {
o := &order.Order{
Purchases: []*purchase_resource.PurchaseResource{{}, {}},
}
// Quantity = len(Purchases) + len(Purchases) (note: there is a bug in source: uses Purchases twice)
assert.Equal(t, 4, o.Quantity())
}
func TestOrder_Quantity_Empty(t *testing.T) {
o := &order.Order{}
assert.Equal(t, 0, o.Quantity())
}
func TestOrder_SetName(t *testing.T) {
o := &order.Order{}
o.UUID = "order-uuid"
o.SetName("ignored")
// Name is generated from UUID, not from the argument
assert.Contains(t, o.Name, "order-uuid")
assert.Contains(t, o.Name, "_order_")
}
func TestOrder_GetAccessor(t *testing.T) {
o := &order.Order{}
acc := o.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestOrder_GetAccessor_NilRequest(t *testing.T) {
o := &order.Order{}
acc := o.GetAccessor(nil)
assert.NotNil(t, acc)
}

View File

@@ -8,22 +8,6 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
// now write a go enum for the state partner with self, blacklist, partner
type PeerState int
const (
OFFLINE PeerState = iota
ONLINE
)
func (m PeerState) String() string {
return [...]string{"NONE", "SELF", "PARTNER", "BLACKLIST"}[m]
}
func (m PeerState) EnumIndex() int {
return int(m)
}
type PeerRelation int type PeerRelation int
const ( const (
@@ -69,7 +53,6 @@ type Peer struct {
NATSAddress string `json:"nats_address" bson:"nats_address" validate:"required"` NATSAddress string `json:"nats_address" bson:"nats_address" validate:"required"`
WalletAddress string `json:"wallet_address" bson:"wallet_address" validate:"required"` // WalletAddress is the wallet address of the peer WalletAddress string `json:"wallet_address" bson:"wallet_address" validate:"required"` // WalletAddress is the wallet address of the peer
PublicKey string `json:"public_key" bson:"public_key" validate:"required"` // PublicKey is the public key of the peer PublicKey string `json:"public_key" bson:"public_key" validate:"required"` // PublicKey is the public key of the peer
State PeerState `json:"state" bson:"state" default:"0"`
Relation PeerRelation `json:"relation" bson:"relation" default:"0"` Relation PeerRelation `json:"relation" bson:"relation" default:"0"`
ServicesState map[string]int `json:"services_state,omitempty" bson:"services_state,omitempty"` ServicesState map[string]int `json:"services_state,omitempty" bson:"services_state,omitempty"`
FailedExecution []PeerExecution `json:"failed_execution" bson:"failed_execution"` // FailedExecution is the list of failed executions, to be retried FailedExecution []PeerExecution `json:"failed_execution" bson:"failed_execution"` // FailedExecution is the list of failed executions, to be retried

View File

@@ -29,7 +29,7 @@ type PeerCache struct {
// urlFormat formats the URL of the peer with the data type API function // urlFormat formats the URL of the peer with the data type API function
func urlFormat(hostUrl string, dt tools.DataType) string { func urlFormat(hostUrl string, dt tools.DataType) string {
return hostUrl + "/" + strings.ReplaceAll(dt.API(), "oc-", "") return hostUrl + "/" + strings.ReplaceAll(dt.String(), "oc-", "")
} }
// checkPeerStatus checks the status of a peer // checkPeerStatus checks the status of a peer
@@ -43,7 +43,7 @@ func CheckPeerStatus(peerID string, appName string) (*Peer, bool) {
url := urlFormat(res.(*Peer).APIUrl, tools.PEER) + "/status" // Format the URL url := urlFormat(res.(*Peer).APIUrl, tools.PEER) + "/status" // Format the URL
state, services := api.CheckRemotePeer(url) state, services := api.CheckRemotePeer(url)
res.(*Peer).ServicesState = services // Update the services states of the peer res.(*Peer).ServicesState = services // Update the services states of the peer
access.UpdateOne(res, peerID) // Update the peer in the db access.UpdateOne(res.Serialize(res), peerID) // Update the peer in the db
return res.(*Peer), state != tools.DEAD && services[appName] == 0 // Return the peer and its status return res.(*Peer), state != tools.DEAD && services[appName] == 0 // Return the peer and its status
} }
@@ -61,7 +61,7 @@ func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
url := "" url := ""
// Check the status of the peer // Check the status of the peer
if mypeer, ok := CheckPeerStatus(peerID, dt.API()); !ok && mypeer != nil { if mypeer, ok := CheckPeerStatus(peerID, dt.String()); !ok && mypeer != nil {
// If the peer is not reachable, add the execution to the failed executions list // If the peer is not reachable, add the execution to the failed executions list
pexec := &PeerExecution{ pexec := &PeerExecution{
Method: method.String(), Method: method.String(),
@@ -71,7 +71,7 @@ func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
DataID: dataID, DataID: dataID,
} }
mypeer.AddExecution(*pexec) mypeer.AddExecution(*pexec)
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db
return map[string]interface{}{}, errors.New("peer is " + peerID + " not reachable") return map[string]interface{}{}, errors.New("peer is " + peerID + " not reachable")
} else { } else {
if mypeer == nil { if mypeer == nil {
@@ -81,7 +81,7 @@ func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
url = urlFormat((mypeer.APIUrl), dt) + path // Format the URL url = urlFormat((mypeer.APIUrl), dt) + path // Format the URL
tmp := mypeer.FailedExecution // Get the failed executions list tmp := mypeer.FailedExecution // Get the failed executions list
mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db
for _, v := range tmp { // Retry the failed executions for _, v := range tmp { // Retry the failed executions
go p.Exec(v.Url, tools.ToMethod(v.Method), v.Body, caller) go p.Exec(v.Url, tools.ToMethod(v.Method), v.Body, caller)
} }

View File

@@ -1,7 +1,6 @@
package peer package peer
import ( import (
"fmt"
"strconv" "strconv"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
@@ -11,7 +10,7 @@ import (
) )
type peerMongoAccessor struct { type peerMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Peer] // AbstractAccessor contains the basic fields of an accessor (model, caller)
OverrideAuth bool OverrideAuth bool
} }
@@ -19,9 +18,10 @@ type peerMongoAccessor struct {
func NewShallowAccessor() *peerMongoAccessor { func NewShallowAccessor() *peerMongoAccessor {
return &peerMongoAccessor{ return &peerMongoAccessor{
OverrideAuth: true, OverrideAuth: true,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Peer]{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Type: tools.PEER, Type: tools.PEER,
New: func() *Peer { return &Peer{} },
}, },
} }
} }
@@ -29,10 +29,11 @@ func NewShallowAccessor() *peerMongoAccessor {
func NewAccessor(request *tools.APIRequest) *peerMongoAccessor { func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
return &peerMongoAccessor{ return &peerMongoAccessor{
OverrideAuth: false, OverrideAuth: false,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Peer]{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.PEER, Type: tools.PEER,
New: func() *Peer { return &Peer{} },
}, },
} }
} }
@@ -44,42 +45,7 @@ func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool {
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (a *peerMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
func (wfa *peerMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, wfa)
}
func (wfa *peerMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set.(*Peer), id, wfa, &Peer{})
}
func (wfa *peerMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Peer), wfa)
}
func (wfa *peerMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, wfa)
}
func (dca *peerMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Peer](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, dca)
}
func (wfa *peerMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Peer](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (wfa *peerMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Peer](filters, search, wfa.GetDefaultFilter(search),
func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (a *peerMongoAccessor) GetDefaultFilter(search string) *dbs.Filters {
if i, err := strconv.Atoi(search); err == nil { if i, err := strconv.Atoi(search); err == nil {
m := map[string][]dbs.Filter{ // search by name if no filters are provided m := map[string][]dbs.Filter{ // search by name if no filters are provided
"relation": {{Operator: dbs.EQUAL.String(), Value: i}}, "relation": {{Operator: dbs.EQUAL.String(), Value: i}},
@@ -87,7 +53,6 @@ func (a *peerMongoAccessor) GetDefaultFilter(search string) *dbs.Filters {
if i == PARTNER.EnumIndex() { if i == PARTNER.EnumIndex() {
m["verify"] = []dbs.Filter{{Operator: dbs.EQUAL.String(), Value: false}} m["verify"] = []dbs.Filter{{Operator: dbs.EQUAL.String(), Value: false}}
} }
fmt.Println(m)
return &dbs.Filters{ return &dbs.Filters{
Or: m, Or: m,
} }
@@ -96,9 +61,6 @@ func (a *peerMongoAccessor) GetDefaultFilter(search string) *dbs.Filters {
search = "" search = ""
} }
return &dbs.Filters{ return &dbs.Filters{
And: map[string][]dbs.Filter{ // search by name if no filters are provided
"state": {{Operator: dbs.EQUAL.String(), Value: ONLINE.EnumIndex()}},
},
Or: map[string][]dbs.Filter{ // search by name if no filters are provided Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"url": {{Operator: dbs.LIKE.String(), Value: search}}, "url": {{Operator: dbs.LIKE.String(), Value: search}},

View File

@@ -1,100 +0,0 @@
package peer_test
import (
"encoding/json"
"testing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockHTTPCaller mocks tools.HTTPCaller
type MockHTTPCaller struct {
mock.Mock
URLS map[tools.DataType]map[tools.METHOD]string
}
func (c *MockHTTPCaller) GetUrls() map[tools.DataType]map[tools.METHOD]string {
return c.URLS
}
func (m *MockHTTPCaller) CallPost(url, token string, body interface{}, types ...string) ([]byte, error) {
args := m.Called(url, token, body)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPCaller) CallGet(url, token string, types ...string) ([]byte, error) {
args := m.Called(url, token)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPCaller) CallDelete(url, token string) ([]byte, error) {
args := m.Called(url, token)
return args.Get(0).([]byte), args.Error(1)
}
func TestLaunchPeerExecution_PeerNotReachable(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{
URLS: map[tools.DataType]map[tools.METHOD]string{
tools.PEER: {
tools.POST: "/execute/:id",
},
},
}
exec, err := cache.LaunchPeerExecution("peer-id", "data-id", tools.PEER, tools.POST, map[string]string{"a": "b"}, caller)
assert.Nil(t, exec)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not reachable")
}
func TestExecSuccess(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
response := map[string]interface{}{"result": "ok"}
data, _ := json.Marshal(response)
caller.On("CallPost", url, "", mock.Anything).Return(data, nil)
_, err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.NoError(t, err)
caller.AssertExpectations(t)
}
func TestExecReturnsErrorField(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
response := map[string]interface{}{"error": "something failed"}
data, _ := json.Marshal(response)
caller.On("CallPost", url, "", mock.Anything).Return(data, nil)
_, err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.Error(t, err)
assert.Equal(t, "something failed", err.Error())
}
func TestExecInvalidJSON(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
caller.On("CallPost", url, "", mock.Anything).Return([]byte("{invalid json}"), nil)
_, err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid character")
}
type mockAccessor struct {
loadOne func(string) (interface{}, int, error)
updateOne func(interface{}, string) error
}
func (m *mockAccessor) LoadOne(id string) (interface{}, int, error) {
return m.loadOne(id)
}
func (m *mockAccessor) UpdateOne(i interface{}, id string) error {
return m.updateOne(i, id)
}

View File

@@ -3,127 +3,107 @@ package peer_test
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"cloud.o-forge.io/core/oc-lib/dbs"
"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/tools"
"github.com/stretchr/testify/assert"
) )
type MockAccessor struct { // ---- PeerRelation ----
mock.Mock
utils.AbstractAccessor func TestPeerRelation_String(t *testing.T) {
assert.Equal(t, "UNKNOWN", peer.NONE.String())
assert.Equal(t, "SELF", peer.SELF.String())
assert.Equal(t, "PARTNER", peer.PARTNER.String())
assert.Equal(t, "BLACKLIST", peer.BLACKLIST.String())
} }
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) { func TestPeerRelation_Path(t *testing.T) {
args := m.Called(id) assert.Equal(t, "unknown", peer.NONE.Path())
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) assert.Equal(t, "self", peer.SELF.Path())
assert.Equal(t, "partner", peer.PARTNER.Path())
assert.Equal(t, "blacklist", peer.BLACKLIST.Path())
} }
func (m *MockAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func TestPeerRelation_EnumIndex(t *testing.T) {
args := m.Called(set, id) assert.Equal(t, 0, peer.NONE.EnumIndex())
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) assert.Equal(t, 1, peer.SELF.EnumIndex())
assert.Equal(t, 2, peer.PARTNER.EnumIndex())
assert.Equal(t, 3, peer.BLACKLIST.EnumIndex())
assert.Equal(t, 4, peer.PENDING_PARTNER.EnumIndex())
} }
func (m *MockAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func TestGetRelationPath(t *testing.T) {
args := m.Called(data) assert.Equal(t, 1, peer.GetRelationPath("self"))
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) assert.Equal(t, 2, peer.GetRelationPath("partner"))
assert.Equal(t, 3, peer.GetRelationPath("blacklist"))
assert.Equal(t, -1, peer.GetRelationPath("nonexistent"))
} }
func (m *MockAccessor) LoadOne(id string) (utils.DBObject, int, error) { // ---- Peer model ----
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) func TestPeer_VerifyAuth(t *testing.T) {
p := &peer.Peer{}
assert.True(t, p.VerifyAuth("get", nil))
assert.True(t, p.VerifyAuth("delete", &tools.APIRequest{}))
} }
func (m *MockAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { func TestPeer_CanDelete(t *testing.T) {
args := m.Called(isDraft) p := &peer.Peer{}
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2) assert.False(t, p.CanDelete())
} }
func (m *MockAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { func TestPeer_GetAccessor(t *testing.T) {
args := m.Called(filters, search, isDraft) p := &peer.Peer{}
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2) req := &tools.APIRequest{}
acc := p.GetAccessor(req)
assert.NotNil(t, acc)
} }
func newTestPeer() *peer.Peer { func TestPeer_AddExecution_Deduplication(t *testing.T) {
return &peer.Peer{ p := &peer.Peer{}
NATSAddress: "", exec := peer.PeerExecution{Method: "POST", Url: "http://peer/data", Body: "body1"}
StreamAddress: "127.0.0.1",
APIUrl: "http://localhost", p.AddExecution(exec)
WalletAddress: "0x123", assert.Len(t, p.FailedExecution, 1)
PublicKey: "pubkey",
Relation: peer.SELF, // Second add of same execution should not duplicate
} p.AddExecution(exec)
assert.Len(t, p.FailedExecution, 1)
// Different execution should be added
exec2 := peer.PeerExecution{Method: "GET", Url: "http://peer/data", Body: nil}
p.AddExecution(exec2)
assert.Len(t, p.FailedExecution, 2)
} }
func TestDeleteOne_UsingMock(t *testing.T) { func TestPeer_RemoveExecution(t *testing.T) {
mockAcc := new(MockAccessor) p := &peer.Peer{}
mockAcc.On("DeleteOne", "id").Return(newTestPeer(), 200, nil) exec1 := peer.PeerExecution{Method: "POST", Url: "http://peer/a", Body: nil}
exec2 := peer.PeerExecution{Method: "DELETE", Url: "http://peer/b", Body: nil}
obj, code, err := mockAcc.DeleteOne("id") p.AddExecution(exec1)
assert.NoError(t, err) p.AddExecution(exec2)
assert.Equal(t, 200, code) assert.Len(t, p.FailedExecution, 2)
assert.NotNil(t, obj)
mockAcc.AssertExpectations(t) p.RemoveExecution(exec1)
assert.Len(t, p.FailedExecution, 1)
assert.Equal(t, exec2, p.FailedExecution[0])
} }
func TestUpdateOne_UsingMock(t *testing.T) { func TestPeer_RemoveExecution_NotFound(t *testing.T) {
mockAcc := new(MockAccessor) p := &peer.Peer{}
peerObj := newTestPeer() exec := peer.PeerExecution{Method: "POST", Url: "http://peer/x", Body: nil}
mockAcc.On("UpdateOne", peerObj, "id").Return(peerObj, 200, nil) p.AddExecution(exec)
obj, code, err := mockAcc.UpdateOne(peerObj, "id") other := peer.PeerExecution{Method: "DELETE", Url: "http://other/x", Body: nil}
assert.NoError(t, err) p.RemoveExecution(other)
assert.Equal(t, 200, code) assert.Len(t, p.FailedExecution, 1) // unchanged
assert.Equal(t, peerObj, obj)
mockAcc.AssertExpectations(t)
} }
func TestStoreOne_UsingMock(t *testing.T) { func TestPeer_RemoveExecution_Empty(t *testing.T) {
mockAcc := new(MockAccessor) p := &peer.Peer{}
peerObj := newTestPeer() // Should not panic on empty list
mockAcc.On("StoreOne", peerObj).Return(peerObj, 200, nil) exec := peer.PeerExecution{Method: "GET", Url: "http://peer/x", Body: nil}
p.RemoveExecution(exec)
obj, code, err := mockAcc.StoreOne(peerObj) assert.Empty(t, p.FailedExecution)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, peerObj, obj)
mockAcc.AssertExpectations(t)
}
func TestLoadOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("LoadOne", "test-id").Return(newTestPeer(), 200, nil)
obj, code, err := mockAcc.LoadOne("test-id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, obj)
mockAcc.AssertExpectations(t)
}
func TestLoadAll_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
expected := []utils.ShallowDBObject{newTestPeer()}
mockAcc.On("LoadAll", false).Return(expected, 200, nil)
objs, code, err := mockAcc.LoadAll(false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, expected, objs)
mockAcc.AssertExpectations(t)
}
func TestSearch_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
filters := &dbs.Filters{}
expected := []utils.ShallowDBObject{newTestPeer()}
mockAcc.On("Search", filters, "test", false).Return(expected, 200, nil)
objs, code, err := mockAcc.Search(filters, "test", false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, expected, objs)
mockAcc.AssertExpectations(t)
} }

View File

@@ -2,6 +2,7 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
@@ -35,11 +36,11 @@ func (abs *ComputeResource) ConvertToPricedResource(t tools.DataType, selectedIn
if t != tools.COMPUTE_RESOURCE { if t != tools.COMPUTE_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Compute") return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Compute")
} }
p, err := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, request) p, err := ConvertToPricedResource[*ComputeResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
priced := p.(*PricedResource) priced := p.(*PricedResource[*ComputeResourcePricingProfile])
return &PricedComputeResource{ return &PricedComputeResource{
PricedResource: *priced, PricedResource: *priced,
}, nil }, nil
@@ -95,7 +96,8 @@ type ComputeResourcePricingProfile struct {
} }
func (p *ComputeResourcePricingProfile) IsPurchasable() bool { func (p *ComputeResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION fmt.Println("Buying", p.Pricing.BuyingStrategy)
return p.Pricing.BuyingStrategy == pricing.PERMANENT
} }
func (p *ComputeResourcePricingProfile) GetPurchase() pricing.BuyingStrategy { func (p *ComputeResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
@@ -120,7 +122,10 @@ func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explici
return 0, errors.New("params must be set") return 0, errors.New("params must be set")
} }
pp := float64(0) pp := float64(0)
model := params[1] model := ""
if len(params) > 1 {
model = params[1]
}
if strings.Contains(params[0], "cpus") && len(params) > 1 { if strings.Contains(params[0], "cpus") && len(params) > 1 {
if _, ok := p.CPUsPrices[model]; ok { if _, ok := p.CPUsPrices[model]; ok {
p.Pricing.Price = p.CPUsPrices[model] p.Pricing.Price = p.CPUsPrices[model]
@@ -156,18 +161,35 @@ func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explici
} }
type PricedComputeResource struct { type PricedComputeResource struct {
PricedResource PricedResource[*ComputeResourcePricingProfile]
CPUsLocated map[string]float64 `json:"cpus_in_use" bson:"cpus_in_use"` // CPUsInUse is the list of CPUs in use CPUsLocated map[string]float64 `json:"cpus_in_use" bson:"cpus_in_use"` // CPUsInUse is the list of CPUs in use
GPUsLocated map[string]float64 `json:"gpus_in_use" bson:"gpus_in_use"` // GPUsInUse is the list of GPUs in use GPUsLocated map[string]float64 `json:"gpus_in_use" bson:"gpus_in_use"` // GPUsInUse is the list of GPUs in use
RAMLocated float64 `json:"ram_in_use" bson:"ram_in_use"` // RAMInUse is the RAM in use RAMLocated float64 `json:"ram_in_use" bson:"ram_in_use"` // RAMInUse is the RAM in use
} }
func (r *PricedComputeResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ComputeResourcePricingProfile{}
}
}
func (r *PricedComputeResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedComputeResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedComputeResource) GetType() tools.DataType { func (r *PricedComputeResource) GetType() tools.DataType {
return tools.COMPUTE_RESOURCE return tools.COMPUTE_RESOURCE
} }
func (r *PricedComputeResource) GetPriceHT() (float64, error) { func (r *PricedComputeResource) GetPriceHT() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil { if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{} r.BookingConfiguration = &BookingConfiguration{}
} }
@@ -176,12 +198,9 @@ func (r *PricedComputeResource) GetPriceHT() (float64, error) {
r.BookingConfiguration.UsageStart = &now r.BookingConfiguration.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour)) add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add r.BookingConfiguration.UsageEnd = &add
} }
if r.SelectedPricing == nil {
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID)
}
pricing := r.SelectedPricing pricing := r.SelectedPricing
price := float64(0) price := float64(0)
for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} { for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} {

View File

@@ -2,7 +2,6 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/models" "cloud.o-forge.io/core/oc-lib/models/common/models"
@@ -42,11 +41,11 @@ func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInsta
if t != tools.DATA_RESOURCE { if t != tools.DATA_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data") return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
} }
p, err := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, request) p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
priced := p.(*PricedResource) priced := p.(*PricedResource[*DataResourcePricingProfile])
return &PricedDataResource{ return &PricedDataResource{
PricedResource: *priced, PricedResource: *priced,
}, nil }, nil
@@ -152,7 +151,7 @@ func (p *DataResourcePricingProfile) GetOverrideStrategyValue() int {
} }
func (p *DataResourcePricingProfile) IsPurchasable() bool { func (p *DataResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION return p.Pricing.BuyingStrategy == pricing.PERMANENT
} }
func (p *DataResourcePricingProfile) IsBooked() bool { func (p *DataResourcePricingProfile) IsBooked() bool {
@@ -161,30 +160,43 @@ func (p *DataResourcePricingProfile) IsBooked() bool {
} }
type PricedDataResource struct { type PricedDataResource struct {
PricedResource PricedResource[*DataResourcePricingProfile]
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"` UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
} }
func (r *PricedDataResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &DataResourcePricingProfile{}
}
}
func (r *PricedDataResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedDataResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedDataResource) GetType() tools.DataType { func (r *PricedDataResource) GetType() tools.DataType {
return tools.DATA_RESOURCE return tools.DATA_RESOURCE
} }
func (r *PricedDataResource) GetPriceHT() (float64, error) { func (r *PricedDataResource) GetPriceHT() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil { if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{} r.BookingConfiguration = &BookingConfiguration{}
} }
fmt.Println("GetPriceHT", r.BookingConfiguration.UsageStart, r.BookingConfiguration.UsageEnd)
now := time.Now() now := time.Now()
if r.BookingConfiguration.UsageStart == nil { if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now r.BookingConfiguration.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour)) add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add r.BookingConfiguration.UsageEnd = &add
} }
if r.SelectedPricing == nil {
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID)
}
pricing := r.SelectedPricing pricing := r.SelectedPricing
var err error var err error
amountOfData := float64(1) amountOfData := float64(1)

View File

@@ -8,19 +8,20 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type PricedResourceITF interface {
pricing.PricedItemITF
}
type ResourceInterface interface { type ResourceInterface interface {
utils.DBObject utils.DBObject
Trim()
FilterPeer(peerID string) *dbs.Filters FilterPeer(peerID string) *dbs.Filters
GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation
ConvertToPricedResource(t tools.DataType, a *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, b *int, request *tools.APIRequest) (pricing.PricedItemITF, error) ConvertToPricedResource(t tools.DataType, a *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, b *int, request *tools.APIRequest) (pricing.PricedItemITF, error)
GetType() string GetType() string
GetSelectedInstance(selected *int) ResourceInstanceITF
ClearEnv() utils.DBObject ClearEnv() utils.DBObject
SetAllowedInstances(request *tools.APIRequest) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF
AddInstances(instance ResourceInstanceITF) AddInstances(instance ResourceInstanceITF)
RefineResourceByPartnership(peerID string) ResourceInterface GetSelectedInstance(index *int) ResourceInstanceITF
GetSignature() []byte
} }
type ResourceInstanceITF interface { type ResourceInstanceITF interface {
@@ -29,17 +30,19 @@ type ResourceInstanceITF interface {
GetName() string GetName() string
StoreDraftDefault() StoreDraftDefault()
ClearEnv() ClearEnv()
FilterInstance(peerID string)
GetProfile(peerID string, partnershipIndex *int, buying *int, strategy *int) pricing.PricingProfileITF GetProfile(peerID string, partnershipIndex *int, buying *int, strategy *int) pricing.PricingProfileITF
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string)
ClearPeerGroups() ClearPeerGroups()
RefineResourceByPartnership(peerID string) (ResourceInstanceITF, bool) GetAverageDurationS() float64
UpdateAverageDuration(actualS float64)
} }
type ResourcePartnerITF interface { type ResourcePartnerITF interface {
RefineResourceByPartnership(peerID string) (ResourcePartnerITF, bool)
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() map[string][]string GetPeerGroups() map[string][]string
ClearPeerGroups() ClearPeerGroups()
GetProfile(buying *int, strategy *int) pricing.PricingProfileITF GetProfile(buying *int, strategy *int) pricing.PricingProfileITF
FilterPartnership(peerID string)
} }

View File

@@ -37,15 +37,13 @@ func (d *NativeTool) ClearEnv() utils.DBObject {
return d return d
} }
func (d *NativeTool) Trim() { func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */
}
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest) {
/* EMPTY */ /* EMPTY */
return []ResourceInstanceITF{}
} }
func (w *NativeTool) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) { func (w *NativeTool) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
return &PricedResource{ return &PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{
Name: w.Name, Name: w.Name,
Logo: w.Logo, Logo: w.Logo,
ResourceID: w.UUID, ResourceID: w.UUID,
@@ -55,8 +53,8 @@ func (w *NativeTool) ConvertToPricedResource(t tools.DataType, selectedInstance
}, nil }, nil
} }
func (abs *NativeTool) RefineResourceByPartnership(peerID string) ResourceInterface { func (r *NativeTool) GetSelectedInstance(selected *int) ResourceInstanceITF {
return abs return nil
} }
func InitNative() { func InitNative() {

View File

@@ -11,6 +11,9 @@ import (
type WorkflowEventParams struct { type WorkflowEventParams struct {
WorkflowResourceID string `json:"workflow_execution_id" bson:"workflow_execution_id" validate:"required"` WorkflowResourceID string `json:"workflow_execution_id" bson:"workflow_execution_id" validate:"required"`
ManualCheck bool `json:"manual_check" bson:"manual_check"`
Input string `json:"input" bson:"input"`
Payload string `json:"payload" bson:"payload"`
BookingMode *booking.BookingMode `json:"booking_mode" bson:"booking_mode"` BookingMode *booking.BookingMode `json:"booking_mode" bson:"booking_mode"`
} }

View File

@@ -2,7 +2,6 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/booking" "cloud.o-forge.io/core/oc-lib/models/booking"
@@ -17,125 +16,135 @@ type BookingConfiguration struct {
Mode booking.BookingMode `json:"mode,omitempty" bson:"mode,omitempty"` Mode booking.BookingMode `json:"mode,omitempty" bson:"mode,omitempty"`
} }
type PricedResource struct { type PricedResource[T pricing.PricingProfileITF] struct {
Name string `json:"name,omitempty" bson:"name,omitempty"` Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"` Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"` InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
SelectedPricing pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"` SelectedPricing T `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"` Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
BookingConfiguration *BookingConfiguration `json:"booking_configuration,omitempty" bson:"booking_configuration,omitempty"` BookingConfiguration *BookingConfiguration `json:"booking_configuration,omitempty" bson:"booking_configuration,omitempty"`
Variations []*pricing.PricingVariation `json:"pricing_variations" bson:"pricing_variations"` Variations []*pricing.PricingVariation `json:"pricing_variations" bson:"pricing_variations"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"` CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"` ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
InstanceID string `json:"instance_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"` ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
} }
func (abs *PricedResource) GetQuantity() int { func (abs *PricedResource[T]) GetQuantity() int {
return abs.Quantity return abs.Quantity
} }
func (abs *PricedResource) AddQuantity(amount int) { func (abs *PricedResource[T]) AddQuantity(amount int) {
abs.Quantity += amount abs.Quantity += amount
} }
func (abs *PricedResource) SelectPricing() pricing.PricingProfileITF { func (abs *PricedResource[T]) SelectPricing() pricing.PricingProfileITF {
return abs.SelectedPricing return abs.SelectedPricing
} }
func (abs *PricedResource) GetID() string { func (abs *PricedResource[T]) GetID() string {
return abs.ResourceID return abs.ResourceID
} }
func (abs *PricedResource) GetType() tools.DataType { func (abs *PricedResource[T]) GetInstanceID() string {
return abs.InstanceID
}
func (abs *PricedResource[T]) GetType() tools.DataType {
return abs.ResourceType return abs.ResourceType
} }
func (abs *PricedResource) GetCreatorID() string { func (abs *PricedResource[T]) GetCreatorID() string {
return abs.CreatorID return abs.CreatorID
} }
func (abs *PricedResource) IsPurchasable() bool { // IsPurchasable and IsBooked fall back to false when SelectedPricing is a nil interface.
if abs.SelectedPricing == nil { // Concrete types (PricedComputeResource, etc.) override these and guarantee non-nil pricing.
func (abs *PricedResource[T]) IsPurchasable() bool {
if any(abs.SelectedPricing) == nil {
return false return false
} }
return (abs.SelectedPricing).IsPurchasable() return abs.SelectedPricing.IsPurchasable()
} }
func (abs *PricedResource) IsBooked() bool { func (abs *PricedResource[T]) IsBooked() bool {
return true // For dev purposes, prevent that DB objects that don't have a Pricing are considered as not booked if any(abs.SelectedPricing) == nil {
if abs.SelectedPricing == nil {
return false return false
} }
return (abs.SelectedPricing).IsBooked() return abs.SelectedPricing.IsBooked()
} }
func (abs *PricedResource) GetLocationEnd() *time.Time { func (abs *PricedResource[T]) GetLocationEnd() *time.Time {
if abs.BookingConfiguration == nil { if abs.BookingConfiguration == nil {
return nil return nil
} }
return abs.BookingConfiguration.UsageEnd return abs.BookingConfiguration.UsageEnd
} }
func (abs *PricedResource) GetLocationStart() *time.Time { func (abs *PricedResource[T]) GetLocationStart() *time.Time {
if abs.BookingConfiguration == nil { if abs.BookingConfiguration == nil {
return nil return nil
} }
return abs.BookingConfiguration.UsageStart return abs.BookingConfiguration.UsageStart
} }
func (abs *PricedResource) SetLocationStart(start time.Time) { func (abs *PricedResource[T]) SetLocationStart(start time.Time) {
if abs.BookingConfiguration == nil { if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{} abs.BookingConfiguration = &BookingConfiguration{}
} }
abs.BookingConfiguration.UsageStart = &start abs.BookingConfiguration.UsageStart = &start
} }
func (abs *PricedResource) SetLocationEnd(end time.Time) { func (abs *PricedResource[T]) SetLocationEnd(end time.Time) {
if abs.BookingConfiguration == nil { if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{} abs.BookingConfiguration = &BookingConfiguration{}
} }
abs.BookingConfiguration.UsageEnd = &end abs.BookingConfiguration.UsageEnd = &end
} }
func (abs *PricedResource) GetBookingMode() booking.BookingMode { func (abs *PricedResource[T]) GetBookingMode() booking.BookingMode {
if abs.BookingConfiguration == nil { if abs.BookingConfiguration == nil {
return booking.WHEN_POSSIBLE return booking.WHEN_POSSIBLE
} }
return abs.BookingConfiguration.Mode return abs.BookingConfiguration.Mode
} }
func (abs *PricedResource) GetExplicitDurationInS() float64 { func (abs *PricedResource[T]) GetExplicitDurationInS() float64 {
if abs.BookingConfiguration == nil { if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{} abs.BookingConfiguration = &BookingConfiguration{}
} }
if abs.BookingConfiguration.ExplicitBookingDurationS == 0 { if abs.BookingConfiguration.ExplicitBookingDurationS == 0 {
if abs.BookingConfiguration.UsageEnd == nil && abs.BookingConfiguration.UsageStart == nil { if abs.BookingConfiguration.UsageEnd == nil && abs.BookingConfiguration.UsageStart == nil {
return time.Duration(1 * time.Hour).Seconds() return (5 * time.Minute).Seconds()
} }
if abs.BookingConfiguration.UsageEnd == nil { if abs.BookingConfiguration.UsageEnd == nil {
add := abs.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour)) add := abs.BookingConfiguration.UsageStart.Add(5 * time.Minute)
abs.BookingConfiguration.UsageEnd = &add abs.BookingConfiguration.UsageEnd = &add
} }
return abs.BookingConfiguration.UsageEnd.Sub(*abs.BookingConfiguration.UsageStart).Seconds() d := abs.BookingConfiguration.UsageEnd.Sub(*abs.BookingConfiguration.UsageStart).Seconds()
if d <= 0 {
return (5 * time.Minute).Seconds()
}
return d
} }
return abs.BookingConfiguration.ExplicitBookingDurationS return abs.BookingConfiguration.ExplicitBookingDurationS
} }
func (r *PricedResource) GetPriceHT() (float64, error) { func (r *PricedResource[T]) GetPriceHT() (float64, error) {
now := time.Now() now := time.Now()
if r.BookingConfiguration == nil { if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{} r.BookingConfiguration = &BookingConfiguration{}
} }
fmt.Println("GetPriceHT", r.BookingConfiguration.UsageStart, r.BookingConfiguration.UsageEnd)
if r.BookingConfiguration.UsageStart == nil { if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now r.BookingConfiguration.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour)) add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add r.BookingConfiguration.UsageEnd = &add
} }
if r.SelectedPricing == nil { if any(r.SelectedPricing) == nil {
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID) return 0, errors.New("pricing profile must be set for resource " + r.ResourceID)
} }
pricing := r.SelectedPricing pricing := r.SelectedPricing
return pricing.GetPriceHT(1, 0, *r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations) return pricing.GetPriceHT(1, 0, *r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)

View File

@@ -1,6 +1,7 @@
package resources package resources
import ( import (
"errors"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
@@ -65,10 +66,31 @@ type ProcessingResourcePartnership struct {
} }
type PricedProcessingResource struct { type PricedProcessingResource struct {
PricedResource PricedResource[*ProcessingResourcePricingProfile]
IsService bool IsService bool
} }
func (r *PricedProcessingResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ProcessingResourcePricingProfile{}
}
}
func (r *PricedProcessingResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedProcessingResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedProcessingResource) GetPriceHT() (float64, error) {
r.ensurePricing()
return r.PricedResource.GetPriceHT()
}
func (r *PricedProcessingResource) GetType() tools.DataType { func (r *PricedProcessingResource) GetType() tools.DataType {
return tools.PROCESSING_RESOURCE return tools.PROCESSING_RESOURCE
} }
@@ -82,7 +104,7 @@ func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
if a.IsService { if a.IsService {
return -1 return -1
} }
return time.Duration(1 * time.Hour).Seconds() return (5 * time.Minute).Seconds()
} }
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds() return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
} }
@@ -93,12 +115,26 @@ func (d *ProcessingResource) GetAccessor(request *tools.APIRequest) utils.Access
return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request, func() utils.DBObject { return &ProcessingResource{} }) // Create a new instance of the accessor return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request, func() utils.DBObject { return &ProcessingResource{} }) // Create a new instance of the accessor
} }
func (abs *ProcessingResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.PROCESSING_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
}
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*DataResourcePricingProfile])
return &PricedDataResource{
PricedResource: *priced,
}, nil
}
type ProcessingResourcePricingProfile struct { type ProcessingResourcePricingProfile struct {
pricing.AccessPricingProfile[pricing.TimePricingStrategy] // AccessPricingProfile is the pricing profile of a data it means that we can access the data for an amount of time pricing.AccessPricingProfile[pricing.TimePricingStrategy] // AccessPricingProfile is the pricing profile of a data it means that we can access the data for an amount of time
} }
func (p *ProcessingResourcePricingProfile) IsPurchasable() bool { func (p *ProcessingResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION return p.Pricing.BuyingStrategy == pricing.PERMANENT
} }
func (p *ProcessingResourcePricingProfile) IsBooked() bool { func (p *ProcessingResourcePricingProfile) IsBooked() bool {

View File

@@ -11,10 +11,17 @@ type PurchaseResource struct {
utils.AbstractObject utils.AbstractObject
DestPeerID string `json:"dest_peer_id" bson:"dest_peer_id"` DestPeerID string `json:"dest_peer_id" bson:"dest_peer_id"`
PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty" validate:"required"` PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty" validate:"required"`
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"` EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"` ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"` ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"`
// Authorization: identifies who created this draft and the Check session it belongs to.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
} }
func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor {

View File

@@ -3,23 +3,23 @@ package purchase_resource
import ( import (
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type PurchaseResourceMongoAccessor struct { type PurchaseResourceMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*PurchaseResource] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the bookingMongoAccessor // New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor { func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
return &PurchaseResourceMongoAccessor{ return &PurchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*PurchaseResource]{
Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.PURCHASE_RESOURCE, Type: tools.PURCHASE_RESOURCE,
New: func() *PurchaseResource { return &PurchaseResource{} },
}, },
} }
} }
@@ -27,24 +27,8 @@ func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (a *PurchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *PurchaseResourceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &PurchaseResource{})
}
func (a *PurchaseResourceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *PurchaseResourceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*PurchaseResource](id, func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(id, a) utils.GenericDeleteOne(id, a)
return nil, 404, nil return nil, 404, nil
@@ -53,15 +37,7 @@ func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int,
}, a) }, a)
} }
func (a *PurchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { func (a *PurchaseResourceMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return utils.GenericLoadAll[*PurchaseResource](a.getExec(), isDraft, a)
}
func (a *PurchaseResourceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*PurchaseResource](filters, search, (&PurchaseResource{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *PurchaseResourceMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(d.GetID(), a) utils.GenericDeleteOne(d.GetID(), a)

View File

@@ -36,8 +36,8 @@ func TestCanUpdate(t *testing.T) {
func TestCanDelete(t *testing.T) { func TestCanDelete(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
past := now.Add(-1 * time.Hour) past := now.Add(-5 * time.Minute)
future := now.Add(1 * time.Hour) future := now.Add(5 * time.Minute)
t.Run("nil EndDate", func(t *testing.T) { t.Run("nil EndDate", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{} r := &purchase_resource.PurchaseResource{}

View File

@@ -1,7 +1,6 @@
package resources package resources
import ( import (
"crypto/sha256"
"encoding/json" "encoding/json"
"errors" "errors"
"slices" "slices"
@@ -21,32 +20,14 @@ import (
// AbstractResource is the struct containing all of the attributes commons to all ressources // AbstractResource is the struct containing all of the attributes commons to all ressources
type AbstractResource struct { type AbstractResource struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name) utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
Type string `json:"type,omitempty" bson:"type,omitempty"` // Type is the type of the resource Type string `json:"type,omitempty" bson:"type,omitempty"` // Type is the type of the resource
Logo string `json:"logo,omitempty" bson:"logo,omitempty" validate:"required"` // Logo is the logo of the resource Logo string `json:"logo,omitempty" bson:"logo,omitempty"` // Logo is the logo of the resource
Description string `json:"description,omitempty" bson:"description,omitempty"` // Description is the description of the resource Description string `json:"description,omitempty" bson:"description,omitempty"` // Description is the description of the resource
ShortDescription string `json:"short_description,omitempty" bson:"short_description,omitempty" validate:"required"` // ShortDescription is the short description of the resource ShortDescription string `json:"short_description,omitempty" bson:"short_description,omitempty"` // ShortDescription is the short description of the resource
Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource
UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"` UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"` AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"`
Signature []byte `bson:"signature,omitempty" json:"signature,omitempty"`
}
func (r *AbstractResource) Unsign() {
r.Signature = nil
}
func (r *AbstractResource) Sign() {
priv, err := tools.LoadKeyFromFilePrivate() // your node private key
if err != nil {
return
}
b, _ := json.Marshal(r)
hash := sha256.Sum256(b)
r.Signature, err = priv.Sign(hash[:])
}
func (abs *AbstractResource) GetSignature() []byte {
return abs.Signature
} }
func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters { func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
@@ -66,10 +47,6 @@ func (r *AbstractResource) GetBookingModes() map[booking.BookingMode]*pricing.Pr
return r.AllowedBookingModes return r.AllowedBookingModes
} }
func (r *AbstractResource) GetSelectedInstance(selected *int) ResourceInstanceITF {
return nil
}
func (r *AbstractResource) GetType() string { func (r *AbstractResource) GetType() string {
return tools.INVALID.String() return tools.INVALID.String()
} }
@@ -79,10 +56,7 @@ func (r *AbstractResource) StoreDraftDefault() {
} }
func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) { func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if r.IsDraft != set.IsDrafted() && set.IsDrafted() { return r.IsDraft, set
return true, set // only state can be updated
}
return r.IsDraft != set.IsDrafted() && set.IsDrafted(), set
} }
func (r *AbstractResource) CanDelete() bool { func (r *AbstractResource) CanDelete() bool {
@@ -91,64 +65,74 @@ func (r *AbstractResource) CanDelete() bool {
type AbstractInstanciatedResource[T ResourceInstanceITF] struct { type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
AbstractResource // AbstractResource contains the basic fields of an object (id, name) AbstractResource // AbstractResource contains the basic fields of an object (id, name)
Instances []T `json:"instances,omitempty" bson:"instances,omitempty"` // Bill is the bill of the resource // Bill is the bill of the resource
}
// PEERID found Instances []T `json:"instances,omitempty" bson:"instances,omitempty"`
func (abs *AbstractInstanciatedResource[T]) RefineResourceByPartnership(peerID string) ResourceInterface {
instances := []T{}
for _, i := range instances {
i, ok := i.RefineResourceByPartnership(peerID)
if ok {
instances = append(instances, i.(T))
}
}
abs.Instances = instances
return abs
} }
func (abs *AbstractInstanciatedResource[T]) AddInstances(instance ResourceInstanceITF) { func (abs *AbstractInstanciatedResource[T]) AddInstances(instance ResourceInstanceITF) {
abs.Instances = append(abs.Instances, instance.(T)) abs.Instances = append(abs.Instances, instance.(T))
} }
func (abs *AbstractInstanciatedResource[T]) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) { func ConvertToPricedResource[T pricing.PricingProfileITF](t tools.DataType,
selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int,
selectedBookingModeIndex *int, abs ResourceInterface, request *tools.APIRequest) (pricing.PricedItemITF, error) {
instances := map[string]string{} instances := map[string]string{}
profiles := []pricing.PricingProfileITF{}
for _, instance := range abs.Instances { // TODO why it crush before ?
instances[instance.GetID()] = instance.GetName()
profiles = instance.GetPricingsProfiles(request.PeerID, request.Groups)
}
var profile pricing.PricingProfileITF var profile pricing.PricingProfileITF
var inst ResourceInstanceITF
if t := abs.GetSelectedInstance(selectedInstance); t != nil { if t := abs.GetSelectedInstance(selectedInstance); t != nil {
inst = t
instances[t.GetID()] = t.GetName()
profile = t.GetProfile(request.PeerID, selectedPartnership, selectedBuyingStrategy, selectedStrategy) profile = t.GetProfile(request.PeerID, selectedPartnership, selectedBuyingStrategy, selectedStrategy)
} else {
for i, instance := range abs.SetAllowedInstances(request) { // TODO why it crush before ?
if i == 0 {
inst = instance
} }
if profile == nil { instances[instance.GetID()] = instance.GetName()
profiles := instance.GetPricingsProfiles(request.PeerID, request.Groups)
if len(profiles) > 0 { if len(profiles) > 0 {
profile = profiles[0] profile = profiles[0]
} else { break
if ok, _ := utils.IsMySelf(request.PeerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{ }
}
}
if profile == nil {
/*if ok, _ := utils.IsMySelf(request.PeerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true, Admin: true,
})); ok { })); ok {*/
profile = pricing.GetDefaultPricingProfile() profile = pricing.GetDefaultPricingProfile()
} else { /*} else {
return nil, errors.New("no pricing profile found") return nil, errors.New("no pricing profile found")
} }*/
}
} }
variations := []*pricing.PricingVariation{} variations := []*pricing.PricingVariation{}
if selectedBookingModeIndex != nil && abs.AllowedBookingModes[booking.BookingMode(*selectedBookingModeIndex)] != nil { if selectedBookingModeIndex != nil && abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)] != nil {
variations = append(variations, abs.AllowedBookingModes[booking.BookingMode(*selectedBookingModeIndex)]) variations = append(variations, abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)])
} }
return &PricedResource{ // Seed the booking configuration with the instance's historical average duration
Name: abs.Name, // so GetExplicitDurationInS() returns a realistic default out of the box.
Logo: abs.Logo, var bc *BookingConfiguration
ResourceID: abs.UUID, if inst != nil {
if avg := inst.GetAverageDurationS(); avg > 0 {
bc = &BookingConfiguration{ExplicitBookingDurationS: avg}
}
}
instanceID := ""
if inst != nil {
instanceID = inst.GetID()
}
selectedPricing, _ := profile.(T)
return &PricedResource[T]{
Name: abs.GetName(),
ResourceID: abs.GetID(),
InstanceID: instanceID,
ResourceType: t, ResourceType: t,
Quantity: 1, Quantity: 1,
InstancesRefs: instances, InstancesRefs: instances,
SelectedPricing: profile, SelectedPricing: selectedPricing,
Variations: variations, Variations: variations,
CreatorID: abs.CreatorID, CreatorID: abs.GetCreatorID(),
BookingConfiguration: bc,
}, nil }, nil
} }
@@ -169,31 +153,28 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
return nil return nil
} }
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest) { func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" { if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
return abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
} }
abs.Instances = VerifyAuthAction(abs.Instances, request) inst := []ResourceInstanceITF{}
for _, i := range abs.Instances {
inst = append(inst, i)
} }
func (d *AbstractInstanciatedResource[T]) Trim() { return inst
d.Type = d.GetType()
if ok, _ := utils.IsMySelf(d.CreatorID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); !ok {
for _, instance := range d.Instances {
instance.ClearPeerGroups()
}
}
} }
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(callName string, request *tools.APIRequest) bool { func (abs *AbstractInstanciatedResource[T]) VerifyAuth(callName string, request *tools.APIRequest) bool {
return len(VerifyAuthAction(abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(callName, request) return len(VerifyAuthAction(abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(callName, request)
} }
func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T { func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest, instanceID ...string) []T {
instances := []T{} instances := []T{}
for _, instance := range baseInstance { for _, instance := range baseInstance {
if len(instanceID) > 0 && !slices.Contains(instanceID, instance.GetID()) {
continue
}
_, peerGroups := instance.GetPeerGroups() _, peerGroups := instance.GetPeerGroups()
for _, peers := range peerGroups { for _, peers := range peerGroups {
if request == nil { if request == nil {
@@ -201,11 +182,14 @@ func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.AP
} }
if grps, ok := peers[request.PeerID]; ok || config.GetConfig().Whitelist { if grps, ok := peers[request.PeerID]; ok || config.GetConfig().Whitelist {
if (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) { if (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance) instances = append(instances, instance)
// TODO filter Partners + Profiles...
continue continue
} }
for _, grp := range grps { for _, grp := range grps {
if slices.Contains(request.Groups, grp) { if slices.Contains(request.Groups, grp) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance) instances = append(instances, instance)
} }
} }
@@ -230,6 +214,9 @@ type ResourceInstance[T ResourcePartnerITF] struct {
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"` Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"` Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
AverageDurationS float64 `json:"average_duration_s,omitempty" bson:"average_duration_s,omitempty"`
AverageDurationSamples int `json:"average_duration_samples,omitempty" bson:"average_duration_samples,omitempty"`
} }
// TODO should kicks all selection // TODO should kicks all selection
@@ -244,17 +231,15 @@ func NewInstance[T ResourcePartnerITF](name string) *ResourceInstance[T] {
} }
} }
func (abs *ResourceInstance[T]) RefineResourceByPartnership(peerID string) (ResourceInstanceITF, bool) { func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
okk := false partnerships := []T{}
partners := []T{} for _, p := range ri.Partnerships {
for _, p := range abs.Partnerships { if p.GetPeerGroups()[peerID] != nil {
partner, ok := p.RefineResourceByPartnership(peerID) p.FilterPartnership(peerID)
if ok { partnerships = append(partnerships, p)
partners = append(partners, partner.(T))
okk = true
} }
} }
return abs, okk ri.Partnerships = partnerships
} }
func (ri *ResourceInstance[T]) ClearEnv() { func (ri *ResourceInstance[T]) ClearEnv() {
@@ -299,15 +284,15 @@ func (ri *ResourceInstance[T]) GetPeerGroups() ([]ResourcePartnerITF, []map[stri
groups = append(groups, p.GetPeerGroups()) groups = append(groups, p.GetPeerGroups())
} }
if len(groups) == 0 { if len(groups) == 0 {
id, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{ pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true, Admin: true,
})) }))
if err != nil { if err != nil || pp == nil {
return partners, groups return partners, groups
} }
groups = []map[string][]string{ groups = []map[string][]string{
{ {
id: {"*"}, pp.GetID(): {"*"},
}, },
} }
// TODO make allow all only for self. // TODO make allow all only for self.
@@ -321,6 +306,17 @@ func (ri *ResourceInstance[T]) ClearPeerGroups() {
} }
} }
func (ri *ResourceInstance[T]) GetAverageDurationS() float64 {
return ri.AverageDurationS
}
func (ri *ResourceInstance[T]) UpdateAverageDuration(actualS float64) {
buffered := actualS * 1.20
n := float64(ri.AverageDurationSamples)
ri.AverageDurationS = (ri.AverageDurationS*n + buffered) / (n + 1)
ri.AverageDurationSamples++
}
type ResourcePartnerShip[T pricing.PricingProfileITF] struct { type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"` Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"`
PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"` PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
@@ -328,17 +324,14 @@ type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
// to upgrade pricing profiles. to be a map BuyingStrategy, map of Strategy // to upgrade pricing profiles. to be a map BuyingStrategy, map of Strategy
} }
func (ri *ResourcePartnerShip[T]) RefineResourceByPartnership(peerID string) (ResourcePartnerITF, bool) { func (ri *ResourcePartnerShip[T]) FilterPartnership(peerID string) {
ok := false if ri.PeerGroups[peerID] == nil {
peerGrp := map[string][]string{} ri.PeerGroups = map[string][]string{}
for k, v := range ri.PeerGroups { } else {
if k == peerID { ri.PeerGroups = map[string][]string{
peerGrp[k] = v peerID: ri.PeerGroups[peerID],
ok = true
} }
} }
ri.PeerGroups = peerGrp
return ri, ok
} }
func (ri *ResourcePartnerShip[T]) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF { func (ri *ResourcePartnerShip[T]) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
@@ -387,14 +380,14 @@ func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []st
func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string { func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string {
if len(rp.PeerGroups) == 0 { if len(rp.PeerGroups) == 0 {
id, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{ pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true, Admin: true,
})) }))
if err != nil { if err != nil || pp == nil {
return rp.PeerGroups return rp.PeerGroups
} }
return map[string][]string{ return map[string][]string{
id: {"*"}, pp.GetID(): {"*"},
} }
} }
return rp.PeerGroups return rp.PeerGroups

View File

@@ -11,7 +11,7 @@ import (
) )
type ResourceMongoAccessor[T ResourceInterface] struct { type ResourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject generateData func() utils.DBObject
} }
@@ -25,10 +25,27 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
return nil return nil
} }
return &ResourceMongoAccessor[T]{ return &ResourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[ResourceInterface]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request, Request: request,
Type: t, Type: t,
New: func() ResourceInterface {
switch t {
case tools.COMPUTE_RESOURCE:
return &ComputeResource{}
case tools.STORAGE_RESOURCE:
return &StorageResource{}
case tools.PROCESSING_RESOURCE:
return &ProcessingResource{}
case tools.WORKFLOW_RESOURCE:
return &WorkflowResource{}
case tools.DATA_RESOURCE:
return &DataResource{}
case tools.NATIVE_TOOL:
return &NativeTool{}
}
return nil
},
}, },
generateData: g, generateData: g,
} }
@@ -37,35 +54,23 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (dca *ResourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, dca)
}
func (dca *ResourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func (dca *ResourceMongoAccessor[T]) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE { if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't update a non existing computing units resource not reported onto compute units catalog") return nil, 404, errors.New("can't update a non existing computing units resource not reported onto compute units catalog")
} }
set.(T).Trim() return utils.GenericUpdateOne(set, id, dca)
if d, c, err := utils.GenericUpdateOne(set, id, dca, dca.generateData()); err != nil {
return d, c, err
} else {
d.Unsign()
d.Sign()
return utils.GenericUpdateOne(set, id, dca, dca.generateData())
} }
func (dca *ResourceMongoAccessor[T]) ShouldVerifyAuth() bool {
return false // TEMP : by pass
} }
func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE { if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog") return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
} }
data.(T).Trim() return utils.GenericStoreOne(data, dca)
d, c, err := utils.GenericStoreOne(data, dca)
if err != nil {
return d, c, err
}
return dca.UpdateOne(d, d.GetID())
} }
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
@@ -75,18 +80,8 @@ func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObjec
return dca.StoreOne(data) return dca.StoreOne(data)
} }
func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) {
d.(T).SetAllowedInstances(dca.Request)
return d, 200, nil
}, dca)
}
func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject { return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa)
d.(T).SetAllowedInstances(wfa.Request)
return d
}, isDraft, wfa)
} }
func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
@@ -96,14 +91,21 @@ func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string,
return d return d
}, isDraft, wfa) }, isDraft, wfa)
} }
return utils.GenericSearch[T](filters, search, wfa.getResourceFilter(search), return utils.GenericSearch[T](filters, search, wfa.GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject { func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request) d.(T).SetAllowedInstances(wfa.Request)
return d return d
}, isDraft, wfa) }, isDraft, wfa)
} }
func (abs *ResourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters { func (a *ResourceMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(a.Request)
return d
}
}
func (abs *ResourceMongoAccessor[T]) GetObjectFilters(search string) *dbs.Filters {
return &dbs.Filters{ return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided
"abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},

View File

@@ -2,7 +2,6 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
@@ -35,11 +34,11 @@ func (abs *StorageResource) ConvertToPricedResource(t tools.DataType, selectedIn
if t != tools.STORAGE_RESOURCE { if t != tools.STORAGE_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Storage") return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Storage")
} }
p, err := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, request) p, err := ConvertToPricedResource[*StorageResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
priced := p.(*PricedResource) priced := p.(*PricedResource[*StorageResourcePricingProfile])
return &PricedStorageResource{ return &PricedStorageResource{
PricedResource: *priced, PricedResource: *priced,
}, nil }, nil
@@ -170,7 +169,7 @@ type StorageResourcePricingProfile struct {
} }
func (p *StorageResourcePricingProfile) IsPurchasable() bool { func (p *StorageResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION return p.Pricing.BuyingStrategy == pricing.PERMANENT
} }
func (p *StorageResourcePricingProfile) IsBooked() bool { func (p *StorageResourcePricingProfile) IsBooked() bool {
@@ -181,30 +180,43 @@ func (p *StorageResourcePricingProfile) IsBooked() bool {
} }
type PricedStorageResource struct { type PricedStorageResource struct {
PricedResource PricedResource[*StorageResourcePricingProfile]
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"` UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
} }
func (r *PricedStorageResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &StorageResourcePricingProfile{}
}
}
func (r *PricedStorageResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedStorageResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedStorageResource) GetType() tools.DataType { func (r *PricedStorageResource) GetType() tools.DataType {
return tools.STORAGE_RESOURCE return tools.STORAGE_RESOURCE
} }
func (r *PricedStorageResource) GetPriceHT() (float64, error) { func (r *PricedStorageResource) GetPriceHT() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil { if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{} r.BookingConfiguration = &BookingConfiguration{}
} }
fmt.Println("GetPriceHT", r.BookingConfiguration.UsageStart, r.BookingConfiguration.UsageEnd)
now := time.Now() now := time.Now()
if r.BookingConfiguration.UsageStart == nil { if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now r.BookingConfiguration.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour)) add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add r.BookingConfiguration.UsageEnd = &add
} }
if r.SelectedPricing == nil {
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID)
}
pricing := r.SelectedPricing pricing := r.SelectedPricing
var err error var err error
amountOfData := float64(1) amountOfData := float64(1)

View File

@@ -37,7 +37,7 @@ func TestComputeResource_ConvertToPricedResource(t *testing.T) {
func TestComputeResourcePricingProfile_GetPriceHT_CPUs(t *testing.T) { func TestComputeResourcePricingProfile_GetPriceHT_CPUs(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(1 * time.Hour) end := start.Add(5 * time.Minute)
profile := resources.ComputeResourcePricingProfile{ profile := resources.ComputeResourcePricingProfile{
CPUsPrices: map[string]float64{"Xeon": 2.0}, CPUsPrices: map[string]float64{"Xeon": 2.0},
ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{ ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{
@@ -61,10 +61,18 @@ func TestComputeResourcePricingProfile_GetPriceHT_InvalidParams(t *testing.T) {
func TestPricedComputeResource_GetPriceHT(t *testing.T) { func TestPricedComputeResource_GetPriceHT(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(1 * time.Hour) end := start.Add(5 * time.Minute)
r := resources.PricedComputeResource{ r := resources.PricedComputeResource{
PricedResource: resources.PricedResource{ PricedResource: resources.PricedResource[*resources.ComputeResourcePricingProfile]{
ResourceID: "comp456", ResourceID: "comp456",
SelectedPricing: &resources.ComputeResourcePricingProfile{
CPUsPrices: map[string]float64{"Xeon": 2.0},
ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0},
},
},
},
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start, UsageStart: &start,
UsageEnd: &end, UsageEnd: &end,
@@ -72,8 +80,8 @@ func TestPricedComputeResource_GetPriceHT(t *testing.T) {
}, },
}, },
CPUsLocated: map[string]float64{"Xeon": 2}, CPUsLocated: map[string]float64{"Xeon": 2},
GPUsLocated: map[string]float64{"Tesla": 1}, GPUsLocated: map[string]float64{},
RAMLocated: 4, RAMLocated: 0,
} }
price, err := r.GetPriceHT() price, err := r.GetPriceHT()
@@ -83,7 +91,7 @@ func TestPricedComputeResource_GetPriceHT(t *testing.T) {
func TestPricedComputeResource_GetPriceHT_MissingProfile(t *testing.T) { func TestPricedComputeResource_GetPriceHT_MissingProfile(t *testing.T) {
r := resources.PricedComputeResource{ r := resources.PricedComputeResource{
PricedResource: resources.PricedResource{ PricedResource: resources.PricedResource[*resources.ComputeResourcePricingProfile]{
ResourceID: "comp789", ResourceID: "comp789",
}, },
} }

View File

@@ -76,13 +76,13 @@ func TestDataResourcePricingStrategy_GetQuantity(t *testing.T) {
func TestDataResourcePricingProfile_IsPurchased(t *testing.T) { func TestDataResourcePricingProfile_IsPurchased(t *testing.T) {
profile := &resources.DataResourcePricingProfile{} profile := &resources.DataResourcePricingProfile{}
profile.Pricing.BuyingStrategy = pricing.SUBSCRIPTION profile.Pricing.BuyingStrategy = pricing.PERMANENT
assert.True(t, profile.IsPurchasable()) assert.True(t, profile.IsPurchasable())
} }
func TestPricedDataResource_GetPriceHT(t *testing.T) { func TestPricedDataResource_GetPriceHT(t *testing.T) {
now := time.Now() now := time.Now()
later := now.Add(1 * time.Hour) later := now.Add(5 * time.Minute)
mockPrice := 42.0 mockPrice := 42.0
pricingProfile := &resources.DataResourcePricingProfile{AccessPricingProfile: pricing.AccessPricingProfile[resources.DataResourcePricingStrategy]{ pricingProfile := &resources.DataResourcePricingProfile{AccessPricingProfile: pricing.AccessPricingProfile[resources.DataResourcePricingStrategy]{
@@ -91,7 +91,8 @@ func TestPricedDataResource_GetPriceHT(t *testing.T) {
pricingProfile.Pricing.OverrideStrategy = resources.PER_GB_DOWNLOADED pricingProfile.Pricing.OverrideStrategy = resources.PER_GB_DOWNLOADED
r := &resources.PricedDataResource{ r := &resources.PricedDataResource{
PricedResource: resources.PricedResource{ PricedResource: resources.PricedResource[*resources.DataResourcePricingProfile]{
SelectedPricing: pricingProfile,
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &now, UsageStart: &now,
UsageEnd: &later, UsageEnd: &later,
@@ -106,7 +107,7 @@ func TestPricedDataResource_GetPriceHT(t *testing.T) {
func TestPricedDataResource_GetPriceHT_NoProfiles(t *testing.T) { func TestPricedDataResource_GetPriceHT_NoProfiles(t *testing.T) {
r := &resources.PricedDataResource{ r := &resources.PricedDataResource{
PricedResource: resources.PricedResource{ PricedResource: resources.PricedResource[*resources.DataResourcePricingProfile]{
ResourceID: "test-resource", ResourceID: "test-resource",
}, },
} }

View File

@@ -36,7 +36,7 @@ func (m *MockPricingProfile) GetPriceHT(amount float64, explicitDuration float64
// ---- Tests ---- // ---- Tests ----
func TestGetIDAndCreatorAndType(t *testing.T) { func TestGetIDAndCreatorAndType(t *testing.T) {
r := resources.PricedResource{ r := resources.PricedResource[pricing.PricingProfileITF]{
ResourceID: "res-123", ResourceID: "res-123",
CreatorID: "user-abc", CreatorID: "user-abc",
ResourceType: tools.DATA_RESOURCE, ResourceType: tools.DATA_RESOURCE,
@@ -48,23 +48,23 @@ func TestGetIDAndCreatorAndType(t *testing.T) {
func TestIsPurchased(t *testing.T) { func TestIsPurchased(t *testing.T) {
t.Run("nil selected pricing returns false", func(t *testing.T) { t.Run("nil selected pricing returns false", func(t *testing.T) {
r := &resources.PricedResource{} r := &resources.PricedResource[pricing.PricingProfileITF]{}
assert.False(t, r.IsPurchasable()) assert.False(t, r.IsPurchasable())
}) })
t.Run("returns true if pricing profile is purchased", func(t *testing.T) { t.Run("returns true if pricing profile is purchased", func(t *testing.T) {
mock := &MockPricingProfile{Purchased: true} mock := &MockPricingProfile{Purchased: true}
r := &resources.PricedResource{SelectedPricing: mock} r := &resources.PricedResource[pricing.PricingProfileITF]{SelectedPricing: mock}
assert.True(t, r.IsPurchasable()) assert.True(t, r.IsPurchasable())
}) })
} }
func TestGetAndSetLocationStartEnd(t *testing.T) { func TestGetAndSetLocationStartEnd(t *testing.T) {
r := &resources.PricedResource{} r := &resources.PricedResource[pricing.PricingProfileITF]{}
now := time.Now() now := time.Now()
r.SetLocationStart(now) r.SetLocationStart(now)
r.SetLocationEnd(now.Add(2 * time.Hour)) r.SetLocationEnd(now.Add(10 * time.Minute))
assert.Equal(t, now, *r.GetLocationStart()) assert.Equal(t, now, *r.GetLocationStart())
assert.Equal(t, now.Add(2*time.Hour), *r.GetLocationEnd()) assert.Equal(t, now.Add(2*time.Hour), *r.GetLocationEnd())
@@ -72,7 +72,7 @@ func TestGetAndSetLocationStartEnd(t *testing.T) {
func TestGetExplicitDurationInS(t *testing.T) { func TestGetExplicitDurationInS(t *testing.T) {
t.Run("uses explicit duration if set", func(t *testing.T) { t.Run("uses explicit duration if set", func(t *testing.T) {
r := &resources.PricedResource{BookingConfiguration: &resources.BookingConfiguration{ r := &resources.PricedResource[pricing.PricingProfileITF]{BookingConfiguration: &resources.BookingConfiguration{
ExplicitBookingDurationS: 3600, ExplicitBookingDurationS: 3600,
}, },
} }
@@ -81,8 +81,8 @@ func TestGetExplicitDurationInS(t *testing.T) {
t.Run("computes duration from start and end", func(t *testing.T) { t.Run("computes duration from start and end", func(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(2 * time.Hour) end := start.Add(10 * time.Minute)
r := &resources.PricedResource{ r := &resources.PricedResource[pricing.PricingProfileITF]{
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start, UsageEnd: &end, UsageStart: &start, UsageEnd: &end,
}, },
@@ -91,28 +91,24 @@ func TestGetExplicitDurationInS(t *testing.T) {
}) })
t.Run("defaults to 1 hour when times not set", func(t *testing.T) { t.Run("defaults to 1 hour when times not set", func(t *testing.T) {
r := &resources.PricedResource{} r := &resources.PricedResource[pricing.PricingProfileITF]{}
assert.InDelta(t, 3600.0, r.GetExplicitDurationInS(), 0.1) assert.InDelta(t, 3600.0, r.GetExplicitDurationInS(), 0.1)
}) })
} }
func TestGetPriceHT(t *testing.T) { func TestGetPriceHT(t *testing.T) {
t.Run("returns error if no pricing profile", func(t *testing.T) { t.Run("returns error if no pricing profile", func(t *testing.T) {
r := &resources.PricedResource{ResourceID: "no-profile"} r := &resources.PricedResource[pricing.PricingProfileITF]{ResourceID: "no-profile"}
price, err := r.GetPriceHT() price, err := r.GetPriceHT()
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set") assert.Contains(t, err.Error(), "pricing profile must be set")
assert.Equal(t, 0.0, price) assert.Equal(t, 0.0, price)
}) })
t.Run("uses first profile if selected is nil", func(t *testing.T) { t.Run("defaults BookingConfiguration when nil", func(t *testing.T) {
start := time.Now() mock := &MockPricingProfile{ReturnCost: 42.0}
end := start.Add(30 * time.Minute) r := &resources.PricedResource[pricing.PricingProfileITF]{
r := &resources.PricedResource{ SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
},
} }
price, err := r.GetPriceHT() price, err := r.GetPriceHT()
require.NoError(t, err) require.NoError(t, err)
@@ -121,9 +117,9 @@ func TestGetPriceHT(t *testing.T) {
t.Run("returns error if profile GetPriceHT fails", func(t *testing.T) { t.Run("returns error if profile GetPriceHT fails", func(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(1 * time.Hour) end := start.Add(5 * time.Minute)
mock := &MockPricingProfile{ReturnErr: true} mock := &MockPricingProfile{ReturnErr: true}
r := &resources.PricedResource{ r := &resources.PricedResource[pricing.PricingProfileITF]{
SelectedPricing: mock, SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start, UsageStart: &start,
@@ -137,9 +133,9 @@ func TestGetPriceHT(t *testing.T) {
t.Run("uses SelectedPricing if set", func(t *testing.T) { t.Run("uses SelectedPricing if set", func(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(1 * time.Hour) end := start.Add(5 * time.Minute)
mock := &MockPricingProfile{ReturnCost: 10.0} mock := &MockPricingProfile{ReturnCost: 10.0}
r := &resources.PricedResource{ r := &resources.PricedResource[pricing.PricingProfileITF]{
SelectedPricing: mock, SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start, UsageStart: &start,

View File

@@ -23,7 +23,7 @@ func TestPricedProcessingResource_GetType(t *testing.T) {
func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) { func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) {
now := time.Now() now := time.Now()
after := now.Add(2 * time.Hour) after := now.Add(10 * time.Minute)
tests := []struct { tests := []struct {
name string name string
@@ -40,30 +40,30 @@ func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) {
{ {
name: "Nil start time, non-service", name: "Nil start time, non-service",
input: PricedProcessingResource{ input: PricedProcessingResource{
PricedResource: PricedResource{ PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: nil, UsageStart: nil,
}, },
}, },
}, },
expected: float64((1 * time.Hour).Seconds()), expected: float64((5 * time.Minute).Seconds()),
}, },
{ {
name: "Duration computed from start and end", name: "Duration computed from start and end",
input: PricedProcessingResource{ input: PricedProcessingResource{
PricedResource: PricedResource{ PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &now, UsageStart: &now,
UsageEnd: &after, UsageEnd: &after,
}, },
}, },
}, },
expected: float64((2 * time.Hour).Seconds()), expected: float64((10 * time.Minute).Seconds()),
}, },
{ {
name: "Explicit duration takes precedence", name: "Explicit duration takes precedence",
input: PricedProcessingResource{ input: PricedProcessingResource{
PricedResource: PricedResource{ PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
BookingConfiguration: &resources.BookingConfiguration{ BookingConfiguration: &resources.BookingConfiguration{
ExplicitBookingDurationS: 1337, ExplicitBookingDurationS: 1337,
}, },
@@ -89,14 +89,14 @@ func TestProcessingResource_GetAccessor(t *testing.T) {
func TestProcessingResourcePricingProfile_GetPriceHT(t *testing.T) { func TestProcessingResourcePricingProfile_GetPriceHT(t *testing.T) {
start := time.Now() start := time.Now()
end := start.Add(2 * time.Hour) end := start.Add(10 * time.Minute)
mockPricing := pricing.AccessPricingProfile[pricing.TimePricingStrategy]{ mockPricing := pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{ Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{
Price: 100.0, Price: 100.0,
}, },
} }
profile := &ProcessingResourcePricingProfile{AccessPricingProfile: mockPricing} profile := &ProcessingResourcePricingProfile{AccessPricingProfile: mockPricing}
price, err := profile.GetPriceHT(0, 0, start, end, []*pricing.PricingVariation{}) price, err := profile.GetPriceHT(1, 0, start, end, []*pricing.PricingVariation{})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 100.0, price) assert.Equal(t, 100.0, price)
} }

View File

@@ -20,6 +20,7 @@ func (m *MockInstance) GetID() string { return m.ID }
func (m *MockInstance) GetName() string { return m.Name } func (m *MockInstance) GetName() string { return m.Name }
func (m *MockInstance) ClearEnv() {} func (m *MockInstance) ClearEnv() {}
func (m *MockInstance) ClearPeerGroups() {} func (m *MockInstance) ClearPeerGroups() {}
func (m *MockPartner) FilterPartnership(peerID string) {}
func (m *MockInstance) GetProfile(peerID string, a *int, b *int, c *int) pricing.PricingProfileITF { func (m *MockInstance) GetProfile(peerID string, a *int, b *int, c *int) pricing.PricingProfileITF {
return nil return nil
} }
@@ -36,10 +37,6 @@ type MockPartner struct {
groups map[string][]string groups map[string][]string
} }
func (abs *MockPartner) RefineResourceByPartnership(peerID string) (resources.ResourcePartnerITF, bool) {
return nil, false
}
func (m *MockPartner) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF { func (m *MockPartner) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
return nil return nil
} }
@@ -48,6 +45,7 @@ func (m *MockPartner) GetPeerGroups() map[string][]string {
return m.groups return m.groups
} }
func (m *MockPartner) ClearPeerGroups() {} func (m *MockPartner) ClearPeerGroups() {}
func (m *MockPartner) GetPricingsProfiles(string, []string) []pricing.PricingProfileITF { func (m *MockPartner) GetPricingsProfiles(string, []string) []pricing.PricingProfileITF {
return nil return nil
} }
@@ -83,8 +81,8 @@ func TestGetSelectedInstance_NoIndex(t *testing.T) {
} }
func TestCanUpdate_WhenOnlyStateDiffers(t *testing.T) { func TestCanUpdate_WhenOnlyStateDiffers(t *testing.T) {
resource := &resources.AbstractResource{AbstractObject: utils.AbstractObject{IsDraft: false}} resource := &resources.AbstractResource{AbstractObject: utils.AbstractObject{IsDraft: true}}
set := &MockDBObject{isDraft: true} set := &MockDBObject{isDraft: false}
canUpdate, updated := resource.CanUpdate(set) canUpdate, updated := resource.CanUpdate(set)
assert.True(t, canUpdate) assert.True(t, canUpdate)
assert.Equal(t, set, updated) assert.Equal(t, set, updated)
@@ -107,8 +105,12 @@ type FakeResource struct {
resources.AbstractInstanciatedResource[*MockInstance] resources.AbstractInstanciatedResource[*MockInstance]
} }
func (f *FakeResource) Trim() {} func (f *FakeResource) SetAllowedInstances(req *tools.APIRequest, instance_id ...string) []resources.ResourceInstanceITF {
func (f *FakeResource) SetAllowedInstances(*tools.APIRequest) {} return nil
}
func (f *FakeResource) ConvertToPricedResource(t tools.DataType, a *int, b *int, c *int, d *int, e *int, req *tools.APIRequest) (pricing.PricedItemITF, error) {
return nil, nil
}
func (f *FakeResource) VerifyAuth(string, *tools.APIRequest) bool { return true } func (f *FakeResource) VerifyAuth(string, *tools.APIRequest) bool { return true }
func TestNewAccessor_ReturnsValid(t *testing.T) { func TestNewAccessor_ReturnsValid(t *testing.T) {

View File

@@ -96,7 +96,7 @@ func TestStorageResourcePricingStrategy_GetQuantity_Invalid(t *testing.T) {
func TestPricedStorageResource_GetPriceHT_NoProfiles(t *testing.T) { func TestPricedStorageResource_GetPriceHT_NoProfiles(t *testing.T) {
res := &resources.PricedStorageResource{ res := &resources.PricedStorageResource{
PricedResource: resources.PricedResource{ PricedResource: resources.PricedResource[*resources.StorageResourcePricingProfile]{
ResourceID: "res-id", ResourceID: "res-id",
}, },
} }

View File

@@ -41,13 +41,6 @@ func TestWorkflowResource_ClearEnv(t *testing.T) {
w := &resources.WorkflowResource{} w := &resources.WorkflowResource{}
assert.Equal(t, w, w.ClearEnv()) assert.Equal(t, w, w.ClearEnv())
} }
func TestWorkflowResource_Trim(t *testing.T) {
w := &resources.WorkflowResource{}
w.Trim()
// nothing to assert; just test that it doesn't panic
}
func TestWorkflowResource_SetAllowedInstances(t *testing.T) { func TestWorkflowResource_SetAllowedInstances(t *testing.T) {
w := &resources.WorkflowResource{} w := &resources.WorkflowResource{}
w.SetAllowedInstances(&tools.APIRequest{}) w.SetAllowedInstances(&tools.APIRequest{})

View File

@@ -19,10 +19,6 @@ func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} }) return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
} }
func (abs *WorkflowResource) RefineResourceByPartnership(peerID string) ResourceInterface {
return abs
}
func (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) { func (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) {
} }
@@ -34,15 +30,17 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
return d return d
} }
func (d *WorkflowResource) Trim() { func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */ /* EMPTY */
return []ResourceInstanceITF{}
} }
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest) {
/* EMPTY */ func (r *WorkflowResource) GetSelectedInstance(selected *int) ResourceInstanceITF {
return nil
} }
func (w *WorkflowResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) { func (w *WorkflowResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
return &PricedResource{ return &PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{
Name: w.Name, Name: w.Name,
Logo: w.Logo, Logo: w.Logo,
ResourceID: w.UUID, ResourceID: w.UUID,

View File

@@ -1,17 +1,17 @@
package models package models
import ( import (
"strconv"
"testing" "testing"
"cloud.o-forge.io/core/oc-lib/models" "cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestModel_ReturnsValidInstances(t *testing.T) { func TestModel_ReturnsValidInstances(t *testing.T) {
for name, _ := range models.ModelsCatalog { for name := range models.ModelsCatalog {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
modelInt, _ := strconv.Atoi(name) modelInt := tools.FromString(name)
obj := models.Model(modelInt) obj := models.Model(modelInt)
assert.NotNil(t, obj, "Model() returned nil for valid model name %s", name) assert.NotNil(t, obj, "Model() returned nil for valid model name %s", name)
}) })

View File

@@ -1,7 +1,10 @@
package utils package utils
import ( import (
"crypto/sha256"
"encoding/json" "encoding/json"
"errors"
"slices"
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
@@ -37,20 +40,43 @@ type AbstractObject struct {
UpdaterID string `json:"updater_id,omitempty" bson:"updater_id,omitempty"` UpdaterID string `json:"updater_id,omitempty" bson:"updater_id,omitempty"`
UserUpdaterID string `json:"user_updater_id,omitempty" bson:"user_updater_id,omitempty"` UserUpdaterID string `json:"user_updater_id,omitempty" bson:"user_updater_id,omitempty"`
AccessMode AccessMode `json:"access_mode" bson:"access_mode" default:"0"` AccessMode AccessMode `json:"access_mode" bson:"access_mode" default:"0"`
Signature []byte `bson:"signature,omitempty" json:"signature,omitempty"`
} }
func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor { func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor {
return nil return nil
} }
func (r *AbstractObject) Unsign() {} func (r *AbstractObject) Unsign() {
r.Signature = nil
}
func (r *AbstractObject) Sign() {} func (r *AbstractObject) Sign() {
priv, err := tools.LoadKeyFromFilePrivate() // your node private key
if err != nil {
return
}
b, _ := json.Marshal(r.DeepCopy())
hash := sha256.Sum256(b)
r.Signature, err = priv.Sign(hash[:])
}
func (r *AbstractObject) SetID(id string) { func (r *AbstractObject) SetID(id string) {
r.UUID = id r.UUID = id
} }
func (r *AbstractObject) DeepCopy() *AbstractObject {
var obj AbstractObject
b, err := json.Marshal(r)
if err != nil {
return nil
}
if err := json.Unmarshal(b, &obj); err != nil {
return nil
}
return &obj
}
func (r *AbstractObject) SetName(name string) { func (r *AbstractObject) SetName(name string) {
r.Name = name r.Name = name
} }
@@ -82,6 +108,10 @@ func (ao AbstractObject) GetID() string {
return ao.UUID return ao.UUID
} }
func (ao AbstractObject) GetSignature() []byte {
return ao.Signature
}
// GetName implements ShallowDBObject. // GetName implements ShallowDBObject.
func (ao AbstractObject) GetName() string { func (ao AbstractObject) GetName() string {
return ao.Name return ao.Name
@@ -103,7 +133,7 @@ func (ao *AbstractObject) UpToDate(user string, peer string, create bool) {
} }
func (ao *AbstractObject) VerifyAuth(callName string, request *tools.APIRequest) bool { func (ao *AbstractObject) VerifyAuth(callName string, request *tools.APIRequest) bool {
return (ao.AccessMode == Public && callName == "get") || request.Admin || (request != nil && ao.CreatorID == request.PeerID && request.PeerID != "") return (ao.AccessMode == Public && callName == "get") || (request != nil && (request.Admin || (ao.CreatorID == request.PeerID && request.PeerID != "")))
} }
// TODO : check write per auth // TODO : check write per auth
@@ -137,50 +167,108 @@ func (dma *AbstractObject) Serialize(obj DBObject) map[string]interface{} {
return m return m
} }
type AbstractAccessor struct { type AbstractAccessor[T DBObject] struct {
Logger zerolog.Logger // Logger is the logger of the accessor, it's a specilized logger for the accessor Logger zerolog.Logger // Logger is the logger of the accessor, it's a specilized logger for the accessor
Type tools.DataType // Type is the data type of the accessor Type tools.DataType // Type is the data type of the accessor
Request *tools.APIRequest // Caller is the http caller of the accessor (optionnal) only need in a peer connection Request *tools.APIRequest // Caller is the http caller of the accessor (optionnal) only need in a peer connection
ResourceModelAccessor Accessor ResourceModelAccessor Accessor
New func() T
NotImplemented []string
} }
func (r *AbstractAccessor) ShouldVerifyAuth() bool { func (r *AbstractAccessor[T]) NewObj() DBObject {
return r.New()
}
func (r *AbstractAccessor[T]) ShouldVerifyAuth() bool {
return true return true
} }
func (r *AbstractAccessor) GetRequest() *tools.APIRequest { func (r *AbstractAccessor[T]) GetRequest() *tools.APIRequest {
return r.Request return r.Request
} }
func (dma *AbstractAccessor) GetUser() string { func (dma *AbstractAccessor[T]) GetUser() string {
if dma.Request == nil { if dma.Request == nil {
return "" return ""
} }
return dma.Request.Username return dma.Request.Username
} }
func (dma *AbstractAccessor) GetPeerID() string { func (dma *AbstractAccessor[T]) GetPeerID() string {
if dma.Request == nil { if dma.Request == nil {
return "" return ""
} }
return dma.Request.PeerID return dma.Request.PeerID
} }
func (dma *AbstractAccessor) GetGroups() []string { func (dma *AbstractAccessor[T]) GetGroups() []string {
if dma.Request == nil { if dma.Request == nil {
return []string{} return []string{}
} }
return dma.Request.Groups return dma.Request.Groups
} }
func (dma *AbstractAccessor) GetLogger() *zerolog.Logger { func (dma *AbstractAccessor[T]) GetLogger() *zerolog.Logger {
return &dma.Logger return &dma.Logger
} }
func (dma *AbstractAccessor) GetType() tools.DataType { func (dma *AbstractAccessor[T]) GetType() tools.DataType {
return dma.Type return dma.Type
} }
func (dma *AbstractAccessor) GetCaller() *tools.HTTPCaller { func (dma *AbstractAccessor[T]) GetCaller() *tools.HTTPCaller {
if dma.Request == nil { if dma.Request == nil {
return nil return nil
} }
return dma.Request.Caller return dma.Request.Caller
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *AbstractAccessor[T]) DeleteOne(id string) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "DeleteOne") {
return nil, 404, errors.New("not implemented")
}
return GenericDeleteOne(id, a)
}
func (a *AbstractAccessor[T]) UpdateOne(set map[string]interface{}, id string) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "UpdateOne") {
return nil, 404, errors.New("not implemented")
}
// should verify if a source is existing...
return GenericUpdateOne(set, id, a)
}
func (a *AbstractAccessor[T]) StoreOne(data DBObject) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "StoreOne") {
return nil, 404, errors.New("not implemented")
}
return GenericStoreOne(data.(T), a)
}
func (a *AbstractAccessor[T]) CopyOne(data DBObject) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "CopyOne") {
return nil, 404, errors.New("not implemented")
}
return GenericStoreOne(data.(T), a)
}
func (a *AbstractAccessor[T]) LoadOne(id string) (DBObject, int, error) {
return GenericLoadOne(id, a.New(), func(d DBObject) (DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *AbstractAccessor[T]) LoadAll(isDraft bool) ([]ShallowDBObject, int, error) {
return GenericLoadAll[T](a.GetExec(isDraft), isDraft, a)
}
func (a *AbstractAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]ShallowDBObject, int, error) {
return GenericSearch[T](filters, search, a.New().GetObjectFilters(search), a.GetExec(isDraft), isDraft, a)
}
func (a *AbstractAccessor[T]) GetExec(isDraft bool) func(DBObject) ShallowDBObject {
return func(d DBObject) ShallowDBObject {
return d
}
}

View File

@@ -1,6 +1,7 @@
package utils package utils
import ( import (
"encoding/json"
"errors" "errors"
"os" "os"
@@ -31,9 +32,10 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
if data.GetID() == "" { if data.GetID() == "" {
data.GenerateID() data.GenerateID()
} }
data.SetID(data.GetID())
data.StoreDraftDefault() data.StoreDraftDefault()
data.UpToDate(a.GetUser(), a.GetPeerID(), true) data.UpToDate(a.GetUser(), a.GetPeerID(), true)
data.Unsign()
data.Sign()
f := dbs.Filters{ f := dbs.Filters{
Or: map[string][]dbs.Filter{ Or: map[string][]dbs.Filter{
"abstractresource.abstractobject.name": {{ "abstractresource.abstractobject.name": {{
@@ -67,6 +69,12 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
// GenericLoadOne loads one object from the database (generic) // GenericLoadOne loads one object from the database (generic)
func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) { func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
res, code, err := a.LoadOne(id) res, code, err := a.LoadOne(id)
if err != nil {
return res, code, err
}
if res == nil {
return res, code, errors.New("not found")
}
if !res.CanDelete() { if !res.CanDelete() {
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String()) return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
} }
@@ -84,29 +92,46 @@ func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
return res, 200, nil return res, 200, nil
} }
// GenericLoadOne loads one object from the database (generic) func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, map[string]interface{}, int, error) {
// json expected in entry is a flatted object no need to respect the inheritance hierarchy
func GenericUpdateOne(set DBObject, id string, a Accessor, new DBObject) (DBObject, int, error) {
r, c, err := a.LoadOne(id) r, c, err := a.LoadOne(id)
if err != nil { if err != nil {
return nil, c, err return nil, nil, c, err
} }
ok, newSet := r.CanUpdate(set) obj := a.NewObj()
b, _ := json.Marshal(r)
json.Unmarshal(b, obj)
if !a.GetRequest().Admin {
var ok bool
ok, r = r.CanUpdate(obj)
if !ok { if !ok {
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String()) return nil, nil, 403, errors.New("you are not allowed to update :" + a.GetType().String())
} }
set = newSet
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
if a.ShouldVerifyAuth() && !r.VerifyAuth("update", a.GetRequest()) { if a.ShouldVerifyAuth() && !r.VerifyAuth("update", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String()) return nil, nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
} }
change := set.Serialize(set) // get the changes }
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
if a.GetPeerID() == r.GetCreatorID() {
r.Unsign()
r.Sign()
}
loaded := r.Serialize(r) // get the loaded object loaded := r.Serialize(r) // get the loaded object
for k, v := range change { // apply the changes, with a flatten method for k, v := range change { // apply the changes, with a flatten method
loaded[k] = v loaded[k] = v
} }
id, code, err := mongo.MONGOService.UpdateOne(new.Deserialize(loaded, new), id, a.GetType().String()) return r, loaded, 200, nil
}
// GenericLoadOne loads one object from the database (generic)
// json expected in entry is a flatted object no need to respect the inheritance hierarchy
func GenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, int, error) {
obj, loaded, c, err := ModelGenericUpdateOne(change, id, a)
if err != nil {
return nil, c, err
}
id, code, err := mongo.MONGOService.UpdateOne(obj.Deserialize(loaded, obj), id, a.GetType().String())
if err != nil { if err != nil {
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error()) a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
return nil, code, err return nil, code, err
@@ -114,13 +139,15 @@ func GenericUpdateOne(set DBObject, id string, a Accessor, new DBObject) (DBObje
return a.LoadOne(id) return a.LoadOne(id)
} }
func GenericLoadOne[T DBObject](id string, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) { func GenericLoadOne[T DBObject](id string, data T, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
var data T
res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String()) res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String())
if err != nil { if err != nil {
return nil, code, err return nil, code, err
} }
res_mongo.Decode(&data) if err = res_mongo.Decode(data); err != nil {
return nil, 400, err
}
if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) { if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String()) return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
} }
@@ -170,24 +197,24 @@ func GenericRawUpdateOne(set DBObject, id string, a Accessor) (DBObject, int, er
return a.LoadOne(id) return a.LoadOne(id)
} }
func GetMySelf(wfa Accessor) (string, error) { func GetMySelf(wfa Accessor) (ShallowDBObject, error) {
id, err := GenerateNodeID() datas, _, _ := wfa.Search(&dbs.Filters{
if err != nil { And: map[string][]dbs.Filter{
return "", err "relation": {{Operator: dbs.EQUAL.String(), Value: 1}},
},
}, "", false)
if len(datas) > 0 && datas[0] != nil {
return datas[0], nil
} }
datas, _, _ := wfa.Search(nil, id, false) return nil, errors.New("peer not found")
if len(datas) > 0 {
return datas[0].GetID(), nil
}
return "", errors.New("peer not found")
} }
func IsMySelf(peerID string, wfa Accessor) (bool, string) { func IsMySelf(peerID string, wfa Accessor) (bool, string) {
id, err := GetMySelf(wfa) pp, err := GetMySelf(wfa)
if err != nil { if err != nil || pp == nil {
return false, "" return false, ""
} }
return peerID == id, id return peerID == pp.GetID(), pp.GetID()
} }
func GenerateNodeID() (string, error) { func GenerateNodeID() (string, error) {

View File

@@ -34,10 +34,13 @@ type DBObject interface {
Deserialize(j map[string]interface{}, obj DBObject) DBObject Deserialize(j map[string]interface{}, obj DBObject) DBObject
Sign() Sign()
Unsign() Unsign()
GetSignature() []byte
GetObjectFilters(search string) *dbs.Filters
} }
// Accessor is an interface that defines the basic methods for an Accessor // Accessor is an interface that defines the basic methods for an Accessor
type Accessor interface { type Accessor interface {
NewObj() DBObject
GetUser() string GetUser() string
GetPeerID() string GetPeerID() string
GetGroups() []string GetGroups() []string
@@ -51,6 +54,7 @@ type Accessor interface {
CopyOne(data DBObject) (DBObject, int, error) CopyOne(data DBObject) (DBObject, int, error)
StoreOne(data DBObject) (DBObject, int, error) StoreOne(data DBObject) (DBObject, int, error)
LoadAll(isDraft bool) ([]ShallowDBObject, int, error) LoadAll(isDraft bool) ([]ShallowDBObject, int, error)
UpdateOne(set DBObject, id string) (DBObject, int, error) UpdateOne(set map[string]interface{}, id string) (DBObject, int, error)
Search(filters *dbs.Filters, search string, isDraft bool) ([]ShallowDBObject, int, error) Search(filters *dbs.Filters, search string, isDraft bool) ([]ShallowDBObject, int, error)
GetExec(isDraft bool) func(DBObject) ShallowDBObject
} }

View File

@@ -1,128 +1,263 @@
package models_test package utils_test
import ( import (
"testing" "testing"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGenerateID(t *testing.T) { // ---- AbstractObject ----
ao := &utils.AbstractObject{}
ao.GenerateID() func TestAbstractObject_GetID(t *testing.T) {
assert.NotEmpty(t, ao.UUID) obj := &utils.AbstractObject{UUID: "abc-123"}
_, err := uuid.Parse(ao.UUID) assert.Equal(t, "abc-123", obj.GetID())
assert.NoError(t, err)
} }
func TestStoreDraftDefault(t *testing.T) { func TestAbstractObject_GetName(t *testing.T) {
ao := &utils.AbstractObject{IsDraft: true} obj := &utils.AbstractObject{Name: "test-name"}
ao.StoreDraftDefault() assert.Equal(t, "test-name", obj.GetName())
assert.False(t, ao.IsDraft)
} }
func TestCanUpdate(t *testing.T) { func TestAbstractObject_GetCreatorID(t *testing.T) {
ao := &utils.AbstractObject{} obj := &utils.AbstractObject{CreatorID: "peer-xyz"}
res, set := ao.CanUpdate(nil) assert.Equal(t, "peer-xyz", obj.GetCreatorID())
assert.True(t, res)
assert.Nil(t, set)
} }
func TestCanDelete(t *testing.T) { func TestAbstractObject_SetID(t *testing.T) {
ao := &utils.AbstractObject{} obj := &utils.AbstractObject{}
assert.True(t, ao.CanDelete()) obj.SetID("new-id")
assert.Equal(t, "new-id", obj.UUID)
} }
func TestIsDrafted(t *testing.T) { func TestAbstractObject_SetName(t *testing.T) {
ao := &utils.AbstractObject{IsDraft: true} obj := &utils.AbstractObject{}
assert.True(t, ao.IsDrafted()) obj.SetName("hello")
assert.Equal(t, "hello", obj.Name)
} }
func TestGetID(t *testing.T) { func TestAbstractObject_GenerateID_WhenEmpty(t *testing.T) {
u := uuid.New().String() obj := &utils.AbstractObject{}
ao := &utils.AbstractObject{UUID: u} obj.GenerateID()
assert.Equal(t, u, ao.GetID()) assert.NotEmpty(t, obj.UUID)
} }
func TestGetName(t *testing.T) { func TestAbstractObject_GenerateID_KeepsExisting(t *testing.T) {
name := "MyObject" obj := &utils.AbstractObject{UUID: "existing-id"}
ao := &utils.AbstractObject{Name: name} obj.GenerateID()
assert.Equal(t, name, ao.GetName()) assert.Equal(t, "existing-id", obj.UUID)
} }
func TestGetCreatorID(t *testing.T) { func TestAbstractObject_StoreDraftDefault(t *testing.T) {
id := "creator-123" obj := &utils.AbstractObject{IsDraft: true}
ao := &utils.AbstractObject{CreatorID: id} obj.StoreDraftDefault()
assert.Equal(t, id, ao.GetCreatorID()) assert.False(t, obj.IsDraft)
} }
func TestUpToDate_CreateFalse(t *testing.T) { func TestAbstractObject_IsDrafted(t *testing.T) {
ao := &utils.AbstractObject{} obj := &utils.AbstractObject{IsDraft: true}
now := time.Now() assert.True(t, obj.IsDrafted())
time.Sleep(time.Millisecond) // ensure time difference
ao.UpToDate("user123", "peer456", false) obj.IsDraft = false
assert.WithinDuration(t, now, ao.UpdateDate, time.Second) assert.False(t, obj.IsDrafted())
assert.Equal(t, "peer456", ao.UpdaterID)
assert.Equal(t, "user123", ao.UserUpdaterID)
assert.True(t, ao.CreationDate.IsZero())
} }
func TestUpToDate_CreateTrue(t *testing.T) { func TestAbstractObject_CanDelete(t *testing.T) {
ao := &utils.AbstractObject{} obj := &utils.AbstractObject{}
now := time.Now() assert.True(t, obj.CanDelete())
time.Sleep(time.Millisecond)
ao.UpToDate("user123", "peer456", true)
assert.WithinDuration(t, now, ao.UpdateDate, time.Second)
assert.WithinDuration(t, now, ao.CreationDate, time.Second)
assert.Equal(t, "peer456", ao.UpdaterID)
assert.Equal(t, "peer456", ao.CreatorID)
assert.Equal(t, "user123", ao.UserUpdaterID)
assert.Equal(t, "user123", ao.UserCreatorID)
} }
func TestVerifyAuth(t *testing.T) { func TestAbstractObject_CanUpdate(t *testing.T) {
request := &tools.APIRequest{PeerID: "peer123"} obj := &utils.AbstractObject{UUID: "id-1"}
ao := &utils.AbstractObject{CreatorID: "peer123"} other := &utils.AbstractObject{UUID: "id-2"}
assert.True(t, ao.VerifyAuth("get", request)) ok, returned := obj.CanUpdate(other)
assert.True(t, ok)
ao = &utils.AbstractObject{AccessMode: utils.Public} assert.Equal(t, other, returned)
assert.True(t, ao.VerifyAuth("get", nil))
ao = &utils.AbstractObject{AccessMode: utils.Private, CreatorID: "peer123"}
request = &tools.APIRequest{PeerID: "wrong"}
assert.False(t, ao.VerifyAuth("get", request))
} }
func TestGetObjectFilters(t *testing.T) { func TestAbstractObject_Unsign(t *testing.T) {
ao := &utils.AbstractObject{} obj := &utils.AbstractObject{Signature: []byte("sig")}
f := ao.GetObjectFilters("*") obj.Unsign()
assert.Nil(t, obj.Signature)
}
func TestAbstractObject_GetSignature(t *testing.T) {
obj := &utils.AbstractObject{Signature: []byte("sig")}
assert.Equal(t, []byte("sig"), obj.GetSignature())
}
func TestAbstractObject_DeepCopy(t *testing.T) {
obj := &utils.AbstractObject{UUID: "id-1", Name: "original"}
copy := obj.DeepCopy()
assert.NotNil(t, copy)
assert.Equal(t, obj.UUID, copy.UUID)
assert.Equal(t, obj.Name, copy.Name)
// Mutating the copy should not affect the original
copy.Name = "modified"
assert.Equal(t, "original", obj.Name)
}
func TestAbstractObject_UpToDate_Create(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: ""}
obj.UpToDate("user1", "peer1", true)
assert.Equal(t, "peer1", obj.UpdaterID)
assert.Equal(t, "user1", obj.UserUpdaterID)
// CreatorID was empty so create branch is skipped
assert.Empty(t, obj.CreatorID)
}
func TestAbstractObject_UpToDate_CreateWithExistingCreator(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "existing-peer"}
obj.UpToDate("user1", "peer1", true)
assert.Equal(t, "peer1", obj.CreatorID)
assert.Equal(t, "user1", obj.UserCreatorID)
}
func TestAbstractObject_UpToDate_Update(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "original-peer"}
obj.UpToDate("user2", "peer2", false)
assert.Equal(t, "peer2", obj.UpdaterID)
assert.Equal(t, "original-peer", obj.CreatorID) // unchanged
}
// ---- VerifyAuth ----
func TestAbstractObject_VerifyAuth_NilRequest_GetPublic(t *testing.T) {
obj := &utils.AbstractObject{AccessMode: 1} // Public = 1
assert.True(t, obj.VerifyAuth("get", nil))
}
func TestAbstractObject_VerifyAuth_NilRequest_DeletePublic(t *testing.T) {
obj := &utils.AbstractObject{AccessMode: 1} // Public = 1
// non-"get" call with nil request → false
assert.False(t, obj.VerifyAuth("delete", nil))
}
func TestAbstractObject_VerifyAuth_NilRequest_Private(t *testing.T) {
obj := &utils.AbstractObject{AccessMode: 0} // Private
assert.False(t, obj.VerifyAuth("get", nil))
}
func TestAbstractObject_VerifyAuth_AdminRequest(t *testing.T) {
obj := &utils.AbstractObject{}
req := &tools.APIRequest{Admin: true}
assert.True(t, obj.VerifyAuth("get", req))
assert.True(t, obj.VerifyAuth("delete", req))
}
func TestAbstractObject_VerifyAuth_MatchingPeerID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "peer-abc"}
req := &tools.APIRequest{PeerID: "peer-abc"}
assert.True(t, obj.VerifyAuth("get", req))
}
func TestAbstractObject_VerifyAuth_MismatchedPeerID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "peer-abc"}
req := &tools.APIRequest{PeerID: "peer-xyz"}
assert.False(t, obj.VerifyAuth("get", req))
}
func TestAbstractObject_VerifyAuth_EmptyPeerID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: ""}
req := &tools.APIRequest{PeerID: ""}
// both empty → condition `ao.CreatorID == request.PeerID && request.PeerID != ""` is false
assert.False(t, obj.VerifyAuth("get", req))
}
// ---- GetObjectFilters ----
func TestAbstractObject_GetObjectFilters_Star(t *testing.T) {
obj := &utils.AbstractObject{}
f := obj.GetObjectFilters("*")
assert.NotNil(t, f) assert.NotNil(t, f)
assert.Contains(t, f.Or, "abstractobject.name")
assert.Equal(t, dbs.LIKE.String(), f.Or["abstractobject.name"][0].Operator)
} }
func TestDeserialize(t *testing.T) { func TestAbstractObject_GetObjectFilters_Search(t *testing.T) {
ao := &utils.AbstractObject{} obj := &utils.AbstractObject{}
input := map[string]interface{}{"name": "test", "id": uuid.New().String()} f := obj.GetObjectFilters("my-search")
res := ao.Deserialize(input, &utils.AbstractObject{}) assert.NotNil(t, f)
assert.NotNil(t, res)
} }
func TestSerialize(t *testing.T) { // ---- Serialize / Deserialize ----
ao := &utils.AbstractObject{Name: "test", UUID: uuid.New().String()}
m := ao.Serialize(ao) func TestAbstractObject_SerializeDeserialize(t *testing.T) {
assert.Equal(t, "test", m["name"]) obj := &utils.AbstractObject{UUID: "serial-id", Name: "serial-name"}
m := obj.Serialize(obj)
assert.NotNil(t, m)
dst := &utils.AbstractObject{}
result := obj.Deserialize(m, dst)
assert.NotNil(t, result)
assert.Equal(t, "serial-id", result.GetID())
} }
func TestAbstractAccessorMethods(t *testing.T) { // ---- GetAccessor ----
r := &utils.AbstractAccessor{Request: &tools.APIRequest{Username: "alice", PeerID: "peer1", Groups: []string{"dev"}}}
assert.True(t, r.ShouldVerifyAuth()) func TestAbstractObject_GetAccessor_ReturnsNil(t *testing.T) {
assert.Equal(t, "alice", r.GetUser()) obj := &utils.AbstractObject{}
assert.Equal(t, "peer1", r.GetPeerID()) acc := obj.GetAccessor(nil)
assert.Equal(t, []string{"dev"}, r.GetGroups()) assert.Nil(t, acc)
assert.Equal(t, r.Request.Caller, r.GetCaller()) }
// ---- AbstractAccessor ----
func TestAbstractAccessor_GetUser_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Equal(t, "", acc.GetUser())
}
func TestAbstractAccessor_GetUser_WithRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Request: &tools.APIRequest{Username: "alice"},
}
assert.Equal(t, "alice", acc.GetUser())
}
func TestAbstractAccessor_GetPeerID_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Equal(t, "", acc.GetPeerID())
}
func TestAbstractAccessor_GetPeerID_WithRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Request: &tools.APIRequest{PeerID: "peer-42"},
}
assert.Equal(t, "peer-42", acc.GetPeerID())
}
func TestAbstractAccessor_GetGroups_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Equal(t, []string{}, acc.GetGroups())
}
func TestAbstractAccessor_GetGroups_WithRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Request: &tools.APIRequest{Groups: []string{"g1", "g2"}},
}
assert.Equal(t, []string{"g1", "g2"}, acc.GetGroups())
}
func TestAbstractAccessor_ShouldVerifyAuth(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{}
assert.True(t, acc.ShouldVerifyAuth())
}
func TestAbstractAccessor_GetType(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Type: tools.WORKFLOW,
}
assert.Equal(t, tools.WORKFLOW, acc.GetType())
}
func TestAbstractAccessor_GetRequest(t *testing.T) {
req := &tools.APIRequest{Admin: true}
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: req}
assert.Equal(t, req, acc.GetRequest())
}
func TestAbstractAccessor_GetCaller_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Nil(t, acc.GetCaller())
} }

View File

@@ -1,168 +0,0 @@
package models_test
import (
"errors"
"testing"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// --- Mock Definitions ---
type MockDBObject struct {
mock.Mock
}
func (m *MockAccessor) GetLogger() *zerolog.Logger {
return nil
}
func (m *MockAccessor) GetGroups() []string {
return []string{}
}
func (m *MockAccessor) GetCaller() *tools.HTTPCaller {
return nil
}
func (m *MockDBObject) GenerateID() { m.Called() }
func (m *MockDBObject) StoreDraftDefault() { m.Called() }
func (m *MockDBObject) UpToDate(user, peer string, create bool) {
m.Called(user, peer, create)
}
func (m *MockDBObject) VerifyAuth(req *tools.APIRequest) bool {
args := m.Called(req)
return args.Bool(0)
}
func (m *MockDBObject) CanDelete() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockDBObject) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
args := m.Called(set)
return args.Bool(0), args.Get(1).(utils.DBObject)
}
func (m *MockDBObject) IsDrafted() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockDBObject) Serialize(obj utils.DBObject) map[string]interface{} {
args := m.Called(obj)
return args.Get(0).(map[string]interface{})
}
func (m *MockDBObject) Deserialize(mdata map[string]interface{}, obj utils.DBObject) utils.DBObject {
args := m.Called(mdata, obj)
return args.Get(0).(utils.DBObject)
}
func (m *MockDBObject) GetID() string {
args := m.Called()
return args.String(0)
}
func (m *MockDBObject) GetName() string {
args := m.Called()
return args.String(0)
}
type MockAccessor struct {
mock.Mock
}
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) ShouldVerifyAuth() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockAccessor) GetRequest() *tools.APIRequest {
args := m.Called()
return args.Get(0).(*tools.APIRequest)
}
func (m *MockAccessor) GetType() tools.DataType {
args := m.Called()
return args.Get(0).(tools.DataType)
}
func (m *MockAccessor) Search(filters *dbs.Filters, s string, d bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, s, d)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) GetUser() string {
args := m.Called()
return args.String(0)
}
func (m *MockAccessor) GetPeerID() string {
args := m.Called()
return args.String(0)
}
// --- Test Cases ---
func TestVerifyAccess_Authorized(t *testing.T) {
mockObj := new(MockDBObject)
mockAcc := new(MockAccessor)
req := &tools.APIRequest{PeerID: "peer"}
mockAcc.On("LoadOne", "123").Return(mockObj, 200, nil)
mockAcc.On("ShouldVerifyAuth").Return(true)
mockObj.On("VerifyAuth", req).Return(true)
mockAcc.On("GetRequest").Return(req)
err := utils.VerifyAccess(mockAcc, "123")
assert.NoError(t, err)
}
func TestVerifyAccess_Unauthorized(t *testing.T) {
mockObj := new(MockDBObject)
mockAcc := new(MockAccessor)
req := &tools.APIRequest{PeerID: "peer"}
mockAcc.On("LoadOne", "123").Return(mockObj, 200, nil)
mockAcc.On("ShouldVerifyAuth").Return(true)
mockObj.On("VerifyAuth", req).Return(false)
mockAcc.On("GetRequest").Return(req)
err := utils.VerifyAccess(mockAcc, "123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not allowed")
}
func TestVerifyAccess_LoadError(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("LoadOne", "bad-id").Return(nil, 404, errors.New("not found"))
err := utils.VerifyAccess(mockAcc, "bad-id")
assert.Error(t, err)
assert.Equal(t, "not found", err.Error())
}

View File

@@ -1,8 +1,6 @@
package graph package graph
import ( import (
"time"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
@@ -67,46 +65,32 @@ func (wf *Graph) IsWorkflow(item GraphItem) bool {
return item.Workflow != nil return item.Workflow != nil
} }
func (g *Graph) GetAverageTimeRelatedToProcessingActivity(start time.Time, processings []*resources.ProcessingResource, resource resources.ResourceInterface, func (g *Graph) GetAverageTimeRelatedToProcessingActivity(processings []*resources.ProcessingResource, resource resources.ResourceInterface,
f func(GraphItem) resources.ResourceInterface, instance int, partnership int, buying int, strategy int, bookingMode int, request *tools.APIRequest) (float64, float64, error) { f func(GraphItem) resources.ResourceInterface, instance int, partnership int, buying int, strategy int, bookingMode int, request *tools.APIRequest) (float64, float64, error) {
nearestStart := float64(10000000000)
oneIsInfinite := false oneIsInfinite := false
longestDuration := float64(0) longestDuration := float64(0)
for _, link := range g.Links { for _, link := range g.Links {
for _, processing := range processings { for _, processing := range processings {
var source string // source is the source of the link if !(link.Destination.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID()) &&
if link.Destination.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID() { // if the destination is the processing and the source is not a compute !(link.Source.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID()) {
source = link.Source.ID continue
} else if link.Source.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID() { // if the source is the processing and the destination is not a compute
source = link.Destination.ID
} }
priced, err := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, &instance, &partnership, &buying, &strategy, &bookingMode, request) priced, err := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
if source != "" { duration := priced.GetExplicitDurationInS()
if priced.GetLocationStart() != nil { if duration < 0 {
near := float64(priced.GetLocationStart().Sub(start).Seconds())
if near < nearestStart {
nearestStart = near
}
}
if priced.GetLocationEnd() != nil {
duration := float64(priced.GetLocationEnd().Sub(*priced.GetLocationStart()).Seconds())
if longestDuration < duration {
longestDuration = duration
}
} else {
oneIsInfinite = true oneIsInfinite = true
} } else if longestDuration < duration {
longestDuration = duration
} }
} }
} }
if oneIsInfinite { if oneIsInfinite {
return nearestStart, -1, nil return 0, -1, nil
} }
return nearestStart, longestDuration, nil return 0, longestDuration, nil
} }
/* /*
@@ -155,7 +139,7 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) { func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
if item, ok := g.Items[id]; ok { if item, ok := g.Items[id]; ok {
if item.Data != nil { if item.NativeTool != nil {
return tools.NATIVE_TOOL, item.NativeTool return tools.NATIVE_TOOL, item.NativeTool
} else if item.Data != nil { } else if item.Data != nil {
return tools.DATA_RESOURCE, item.Data return tools.DATA_RESOURCE, item.Data

282
models/workflow/plantuml.go Normal file
View File

@@ -0,0 +1,282 @@
package workflow
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
)
// ---------------------------------------------------------------------------
// PlantUML export
// ---------------------------------------------------------------------------
// plantUMLProcedures defines !procedure blocks for each resource type.
// Parameters use the $var/$name convention of PlantUML preprocessor v2.
// Calls are written WITHOUT inline comments (comment on the following line)
// to avoid the "assumed sequence diagram" syntax error.
const plantUMLProcedures = `!procedure Processing($var, $name)
component "$name" as $var <<Processing>>
!endprocedure
!procedure Data($var, $name)
file "$name" as $var <<Data>>
!endprocedure
!procedure Storage($var, $name)
database "$name" as $var <<Storage>>
!endprocedure
!procedure ComputeUnit($var, $name)
node "$name" as $var <<ComputeUnit>>
!endprocedure
!procedure WorkflowEvent($var, $name)
usecase "$name" as $var <<WorkflowEvent>>
!endprocedure
!procedure Workflow($var, $name)
frame "$name" as $var <<Workflow>>
!endprocedure
`
// ToPlantUML serializes the workflow graph to a valid, renderable PlantUML file
// that is also compatible with ExtractFromPlantUML (round-trip).
// Resource and instance attributes are written as human-readable comments:
//
// Processing(p1, "NDVI") ' access.container.image: myrepo/ndvi:1.2, infrastructure: 0
func (w *Workflow) ToPlantUML() string {
var sb strings.Builder
sb.WriteString("@startuml\n\n")
sb.WriteString(plantUMLProcedures)
sb.WriteByte('\n')
varNames := plantUMLVarNames(w.Graph.Items)
// --- resource declarations ---
for id, item := range w.Graph.Items {
if line := plantUMLItemLine(varNames[id], item); line != "" {
sb.WriteString(line + "\n")
}
}
sb.WriteByte('\n')
// --- links ---
for _, link := range w.Graph.Links {
src := varNames[link.Source.ID]
dst := varNames[link.Destination.ID]
if src == "" || dst == "" {
continue
}
sb.WriteString(fmt.Sprintf("%s --> %s\n", src, dst))
if comment := plantUMLLinkComment(link); comment != "" {
sb.WriteString("' " + comment + "\n")
}
}
sb.WriteString("\n@enduml\n")
return sb.String()
}
// plantUMLVarNames assigns short, deterministic variable names to each graph
// item (d1, d2, p1, s1, c1, e1, wf1 …).
func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
counters := map[string]int{}
varNames := map[string]string{}
// Sort IDs for deterministic output
ids := make([]string, 0, len(items))
for id := range items {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
prefix := plantUMLPrefix(items[id])
counters[prefix]++
varNames[id] = fmt.Sprintf("%s%d", prefix, counters[prefix])
}
return varNames
}
func plantUMLPrefix(item graph.GraphItem) string {
switch {
case item.NativeTool != nil:
return "e"
case item.Data != nil:
return "d"
case item.Processing != nil:
return "p"
case item.Storage != nil:
return "s"
case item.Compute != nil:
return "c"
case item.Workflow != nil:
return "wf"
}
return "u"
}
// plantUMLItemLine builds the PlantUML declaration line for one graph item.
func plantUMLItemLine(varName string, item graph.GraphItem) string {
switch {
case item.NativeTool != nil:
// WorkflowEvent has no instance and no configurable attributes.
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.NativeTool.GetName())
case item.Data != nil:
return plantUMLResourceLine("Data", varName, item.Data)
case item.Processing != nil:
return plantUMLResourceLine("Processing", varName, item.Processing)
case item.Storage != nil:
return plantUMLResourceLine("Storage", varName, item.Storage)
case item.Compute != nil:
return plantUMLResourceLine("ComputeUnit", varName, item.Compute)
case item.Workflow != nil:
return plantUMLResourceLine("Workflow", varName, item.Workflow)
}
return ""
}
func plantUMLResourceLine(macro, varName string, res resources.ResourceInterface) string {
decl := fmt.Sprintf("%s(%s, \"%s\")", macro, varName, res.GetName())
if comment := plantUMLResourceComment(res); comment != "" {
// Comment on the line AFTER the declaration. ExtractFromPlantUML uses
// look-ahead to merge it back. No inline comment = no !procedure conflict.
return decl + "\n' " + comment
}
return decl
}
// plantUMLResourceComment merges resource-level fields with the first instance
// fields (instance overrides resource) and formats them as human-readable pairs.
func plantUMLResourceComment(res resources.ResourceInterface) string {
m := plantUMLToFlatMap(res)
if inst := res.GetSelectedInstance(nil); inst != nil {
for k, v := range plantUMLToFlatMap(inst) {
m[k] = v
}
}
return plantUMLFlatMapToComment(m)
}
// plantUMLLinkComment serializes StorageLinkInfos (first entry) as flat
// human-readable pairs prefixed with "storage_link_infos.".
func plantUMLLinkComment(link graph.GraphLink) string {
if len(link.StorageLinkInfos) == 0 {
return ""
}
infoFlat := plantUMLToFlatMap(link.StorageLinkInfos[0])
prefixed := make(map[string]string, len(infoFlat))
for k, v := range infoFlat {
prefixed["storage_link_infos."+k] = v
}
return plantUMLFlatMapToComment(prefixed)
}
// ---------------------------------------------------------------------------
// Flat-map helpers (shared by import & export)
// ---------------------------------------------------------------------------
// plantUMLSkipFields lists JSON field names (root keys) that must never appear
// in human-readable comments. All names are the actual JSON tags, not Go field names.
var plantUMLSkipFields = map[string]bool{
// AbstractObject — identity & audit (json tags)
"id": true, "name": true, "is_draft": true, "access_mode": true, "signature": true,
"creator_id": true, "user_creator_id": true,
"creation_date": true, "update_date": true,
"updater_id": true, "user_updater_id": true,
// internal resource type identifier (AbstractResource.Type / GetType())
"type": true,
// relationships / pricing
"instances": true, "partnerships": true,
"allowed_booking_modes": true, "usage_restrictions": true,
// display / admin
"logo": true, "description": true, "short_description": true, "owners": true,
// runtime params
"env": true, "inputs": true, "outputs": true,
// NativeTool internals
"kind": true, "params": true,
}
// zeroTimeStr is the JSON representation of Go's zero time.Time value.
// encoding/json does not treat it as "empty" for omitempty, so we filter it explicitly.
const zeroTimeStr = "0001-01-01T00:00:00Z"
// plantUMLToFlatMap marshals v to JSON and flattens the resulting object into
// a map[string]string using dot notation for nested keys, skipping zero values
// and known meta fields.
func plantUMLToFlatMap(v interface{}) map[string]string {
b, err := json.Marshal(v)
if err != nil {
return nil
}
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return nil
}
result := map[string]string{}
plantUMLFlattenJSON(raw, "", result)
return result
}
// plantUMLFlattenJSON recursively walks a JSON object and writes scalar leaf
// values into result using dot-notation keys.
func plantUMLFlattenJSON(m map[string]interface{}, prefix string, result map[string]string) {
for k, v := range m {
fullKey := k
if prefix != "" {
fullKey = prefix + "." + k
}
// Skip fields whose root key is in the deny-list
if plantUMLSkipFields[strings.SplitN(fullKey, ".", 2)[0]] {
continue
}
switch val := v.(type) {
case map[string]interface{}:
plantUMLFlattenJSON(val, fullKey, result)
case []interface{}:
// Arrays are not representable in flat human-readable format; skip.
case float64:
if val != 0 {
if val == float64(int64(val)) {
result[fullKey] = strconv.FormatInt(int64(val), 10)
} else {
result[fullKey] = strconv.FormatFloat(val, 'f', -1, 64)
}
}
case bool:
if val {
result[fullKey] = "true"
}
case string:
if val != "" && val != zeroTimeStr {
result[fullKey] = val
}
}
}
}
// plantUMLFlatMapToComment converts a flat map to a sorted "key: value, …" string.
func plantUMLFlatMapToComment(m map[string]string) string {
if len(m) == 0 {
return ""
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, k+": "+m[k])
}
return strings.Join(parts, ", ")
}

View File

@@ -6,15 +6,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"strconv"
"strings" "strings"
"time" "time"
"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/booking/planner"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area" "cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/common" "cloud.o-forge.io/core/oc-lib/models/common"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/live"
"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/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/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow/graph" "cloud.o-forge.io/core/oc-lib/models/workflow/graph"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
@@ -42,6 +47,8 @@ type Workflow struct {
ScheduleActive bool `json:"schedule_active" bson:"schedule_active"` // ScheduleActive is a flag that indicates if the schedule is active, if not the workflow is not scheduled and no execution or booking will be set ScheduleActive bool `json:"schedule_active" bson:"schedule_active"` // ScheduleActive is a flag that indicates if the schedule is active, if not the workflow is not scheduled and no execution or booking will be set
// Schedule *WorkflowSchedule `bson:"schedule,omitempty" json:"schedule,omitempty"` // Schedule is the schedule of the workflow // Schedule *WorkflowSchedule `bson:"schedule,omitempty" json:"schedule,omitempty"` // Schedule is the schedule of the workflow
Shared []string `json:"shared,omitempty" bson:"shared,omitempty"` // Shared is the ID of the shared workflow // AbstractWorkflow contains the basic fields of a workflow Shared []string `json:"shared,omitempty" bson:"shared,omitempty"` // Shared is the ID of the shared workflow // AbstractWorkflow contains the basic fields of a workflow
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
} }
func (d *Workflow) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *Workflow) GetAccessor(request *tools.APIRequest) utils.Accessor {
@@ -134,81 +141,187 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A
}, },
} }
}, },
// WorkflowEvent creates a NativeTool of Kind=WORKFLOW_EVENT directly,
// without DB lookup. It has no user-defined instance.
"WorkflowEvent": func() resources.ResourceInterface {
return &resources.NativeTool{
Kind: int(native_tools.WORKFLOW_EVENT),
} }
graphVarName := map[string]*graph.GraphItem{} },
scanner := bufio.NewScanner(plantUML) }
graphVarName := map[string]graph.GraphItem{}
// Collect all lines first to support look-ahead (comment on the line after
// the declaration, as produced by ToPlantUML).
scanner := bufio.NewScanner(plantUML)
var lines []string
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() lines = append(lines, scanner.Text())
for n, new := range resourceCatalog {
if strings.Contains(line, n+"(") && !strings.Contains(line, "!procedure") { // should exclude declaration of type.
newRes := new()
varName, graphItem, err := d.extractResourcePlantUML(line, newRes, n, request.PeerID)
if err != nil {
return d, err
}
graphVarName[varName] = graphItem
continue
} else if strings.Contains(line, n+"-->") {
err := d.extractLink(line, graphVarName, "-->", false)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, n+"<--") {
err := d.extractLink(line, graphVarName, "<--", true)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, n+"--") {
err := d.extractLink(line, graphVarName, "--", false)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, n+"-") {
err := d.extractLink(line, graphVarName, "-", false)
if err != nil {
fmt.Println(err)
continue
}
}
}
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return d, err return d, err
} }
for i, line := range lines {
trimmed := strings.TrimSpace(line)
// Skip pure comment lines and PlantUML directives — they must never be
// parsed as resource declarations or links. Without this guard, a comment
// like "' source: http://my-server.com" would match the "-" link check.
if strings.HasPrefix(trimmed, "'") ||
strings.HasPrefix(trimmed, "!") ||
strings.HasPrefix(trimmed, "@") ||
trimmed == "" {
continue
}
// Build the parse line: if the current line has no inline comment and the
// next line is a pure comment, append it so parsers receive one combined line.
// Also handles the legacy inline-comment format unchanged.
parseLine := line
if !strings.Contains(line, "'") && i+1 < len(lines) {
if next := strings.TrimSpace(lines[i+1]); strings.HasPrefix(next, "'") {
parseLine = line + " " + next
}
}
for n, new := range resourceCatalog {
if strings.Contains(line, n+"(") && !strings.Contains(line, "!procedure") && !strings.Contains(line, "!define") { // exclude macro declarations
newRes := new()
newRes.SetID(uuid.New().String())
varName, graphItem, err := d.extractResourcePlantUML(parseLine, newRes, n, request.PeerID)
if err != nil {
return d, err
}
if graphItem != nil {
graphVarName[varName] = *graphItem
}
continue
} else if strings.Contains(line, "-->") {
err := d.extractLink(parseLine, graphVarName, "-->", false)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, "<--") {
err := d.extractLink(parseLine, graphVarName, "<--", true)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, "--") {
err := d.extractLink(parseLine, graphVarName, "--", false)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, "-") {
err := d.extractLink(parseLine, graphVarName, "-", false)
if err != nil {
fmt.Println(err)
continue
}
}
}
}
d.generateResource(d.GetResources(tools.DATA_RESOURCE), request) d.generateResource(d.GetResources(tools.DATA_RESOURCE), request)
d.generateResource(d.GetResources(tools.PROCESSING_RESOURCE), request) d.generateResource(d.GetResources(tools.PROCESSING_RESOURCE), request)
d.generateResource(d.GetResources(tools.STORAGE_RESOURCE), request) d.generateResource(d.GetResources(tools.STORAGE_RESOURCE), request)
d.generateResource(d.GetResources(tools.COMPUTE_RESOURCE), request) d.generateResource(d.GetResources(tools.COMPUTE_RESOURCE), request)
d.generateResource(d.GetResources(tools.WORKFLOW_RESOURCE), request) d.generateResource(d.GetResources(tools.WORKFLOW_RESOURCE), request)
d.Graph.Items = graphVarName
return d, nil return d, nil
} }
func (d *Workflow) generateResource(datas []resources.ResourceInterface, request *tools.APIRequest) error { func (d *Workflow) generateResource(datas []resources.ResourceInterface, request *tools.APIRequest) error {
for _, d := range datas { for _, d := range datas {
access := d.GetAccessor(request) if d.GetType() == tools.COMPUTE_RESOURCE.String() {
if _, code, err := access.LoadOne(d.GetID()); err != nil && code == 200 { access := live.NewAccessor[*live.LiveDatacenter](tools.LIVE_DATACENTER, request)
if b, err := json.Marshal(d); err == nil {
var liv live.LiveDatacenter
json.Unmarshal(b, &liv)
data, _, err := access.StoreOne(&liv)
if err == nil {
access.CopyOne(data)
}
}
continue
} else if d.GetType() == tools.STORAGE_RESOURCE.String() {
access := live.NewAccessor[*live.LiveStorage](tools.LIVE_STORAGE, request)
if b, err := json.Marshal(d); err == nil {
var liv live.LiveStorage
json.Unmarshal(b, &liv)
data, _, err := access.StoreOne(&liv)
if err == nil {
access.CopyOne(data)
}
}
continue continue
} }
access.StoreOne(d) d.GetAccessor(request).StoreOne(d)
} }
return nil return nil
} }
func (d *Workflow) extractLink(line string, graphVarName map[string]*graph.GraphItem, pattern string, reverse bool) error { // setNestedKey sets a value in a nested map using dot-notation path.
// "access.container.image" → m["access"]["container"]["image"] = value
func setNestedKey(m map[string]any, path string, value any) {
parts := strings.SplitN(path, ".", 2)
if len(parts) == 1 {
m[path] = value
return
}
key, rest := parts[0], parts[1]
if _, ok := m[key]; !ok {
m[key] = map[string]any{}
}
if sub, ok := m[key].(map[string]any); ok {
setNestedKey(sub, rest, value)
}
}
// parseHumanFriendlyAttrs converts a human-friendly comment into JSON bytes.
// Supports:
// - flat: "source: http://example.com, encryption: true, size: 500"
// - nested: "access.container.image: nginx, access.container.tag: latest"
// - raw JSON passthrough (backward-compat): '{"key": "value"}'
//
// Values are auto-typed: bool, float64, or string.
// Note: the first ':' in each pair is the key/value separator,
// so URLs like "http://..." are handled correctly.
func parseHumanFriendlyAttrs(comment string) []byte {
comment = strings.TrimSpace(comment)
if strings.HasPrefix(comment, "{") {
return []byte(comment)
}
m := map[string]any{}
for _, pair := range strings.Split(comment, ",") {
pair = strings.TrimSpace(pair)
parts := strings.SplitN(pair, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
var typed any
if b, err := strconv.ParseBool(val); err == nil {
typed = b
} else if n, err := strconv.ParseFloat(val, 64); err == nil {
typed = n
} else {
typed = val
}
setNestedKey(m, key, typed)
}
b, _ := json.Marshal(m)
return b
}
func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphItem, pattern string, reverse bool) error {
splitted := strings.Split(line, pattern) splitted := strings.Split(line, pattern)
if len(splitted) < 2 { if len(splitted) < 2 {
return errors.New("links elements not found") return errors.New("links elements not found")
} }
if graphVarName[splitted[0]] != nil {
return errors.New("links elements not found -> " + strings.Trim(splitted[0], " "))
}
if graphVarName[splitted[1]] != nil {
return errors.New("links elements not found -> " + strings.Trim(splitted[1], " "))
}
link := &graph.GraphLink{ link := &graph.GraphLink{
Source: graph.Position{ Source: graph.Position{
ID: graphVarName[splitted[0]].ID, ID: graphVarName[splitted[0]].ID,
@@ -227,11 +340,10 @@ func (d *Workflow) extractLink(line string, graphVarName map[string]*graph.Graph
link.Source = tmp link.Source = tmp
} }
splittedComments := strings.Split(line, "'") splittedComments := strings.Split(line, "'")
if len(splittedComments) <= 1 { if len(splittedComments) > 1 {
return errors.New("Can't deserialize Object, there's no commentary") comment := strings.ReplaceAll(splittedComments[1], "'", "")
json.Unmarshal(parseHumanFriendlyAttrs(comment), link)
} }
comment := strings.ReplaceAll(splittedComments[1], "'", "") // for now it's a json.
json.Unmarshal([]byte(comment), link)
d.Graph.Links = append(d.Graph.Links, *link) d.Graph.Links = append(d.Graph.Links, *link)
return nil return nil
} }
@@ -242,7 +354,7 @@ func (d *Workflow) extractResourcePlantUML(line string, resource resources.Resou
return "", nil, errors.New("Can't deserialize Object, there's no func") return "", nil, errors.New("Can't deserialize Object, there's no func")
} }
splittedParams := strings.Split(splittedFunc[1], ",") splittedParams := strings.Split(splittedFunc[1], ",")
if len(splittedFunc) <= 1 { if len(splittedParams) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no params") return "", nil, errors.New("Can't deserialize Object, there's no params")
} }
@@ -252,32 +364,40 @@ func (d *Workflow) extractResourcePlantUML(line string, resource resources.Resou
if len(splitted) <= 1 { if len(splitted) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no name") return "", nil, errors.New("Can't deserialize Object, there's no name")
} }
resource.SetName(splitted[1]) resource.SetName(strings.ReplaceAll(splitted[1], "\\n", " "))
splittedComments := strings.Split(line, "'") // Resources with instances get a default one seeded from the parent resource,
if len(splittedComments) <= 1 { // then overridden by any explicit comment attributes.
return "", nil, errors.New("Can't deserialize Object, there's no commentary") // Event (NativeTool) has no instance: getNewInstance returns nil and is skipped.
}
comment := strings.ReplaceAll(splittedComments[1], "'", "") // for now it's a json.
instance := d.getNewInstance(dataName, splitted[1], peerID) instance := d.getNewInstance(dataName, splitted[1], peerID)
if instance == nil { if instance != nil {
return "", nil, errors.New("No instance found.") if b, err := json.Marshal(resource); err == nil {
json.Unmarshal(b, instance)
}
splittedComments := strings.Split(line, "'")
if len(splittedComments) > 1 {
comment := strings.ReplaceAll(splittedComments[1], "'", "")
json.Unmarshal(parseHumanFriendlyAttrs(comment), instance)
} }
resource.AddInstances(instance) resource.AddInstances(instance)
}
json.Unmarshal([]byte(comment), instance) item := d.getNewGraphItem(dataName, resource)
// deserializer les instances... une instance doit par défaut avoir certaines valeurs d'accès. if item != nil {
graphID := uuid.New() d.Graph.Items[item.ID] = *item
}
return varName, item, nil
}
func (d *Workflow) getNewGraphItem(dataName string, resource resources.ResourceInterface) *graph.GraphItem {
if resource == nil {
return nil
}
graphItem := &graph.GraphItem{ graphItem := &graph.GraphItem{
ID: graphID.String(), ID: uuid.New().String(),
ItemResource: &resources.ItemResource{},
} }
graphItem = d.getNewGraphItem(dataName, graphItem, resource)
d.Graph.Items[graphID.String()] = *graphItem
return varName, graphItem, nil
}
func (d *Workflow) getNewGraphItem(dataName string, graphItem *graph.GraphItem, resource resources.ResourceInterface) *graph.GraphItem {
switch dataName { switch dataName {
case "Data": case "Data":
d.Datas = append(d.Datas, resource.GetID()) d.Datas = append(d.Datas, resource.GetID())
@@ -287,15 +407,13 @@ func (d *Workflow) getNewGraphItem(dataName string, graphItem *graph.GraphItem,
d.Processings = append(d.Processings, resource.GetID()) d.Processings = append(d.Processings, resource.GetID())
d.ProcessingResources = append(d.ProcessingResources, resource.(*resources.ProcessingResource)) d.ProcessingResources = append(d.ProcessingResources, resource.(*resources.ProcessingResource))
graphItem.Processing = resource.(*resources.ProcessingResource) graphItem.Processing = resource.(*resources.ProcessingResource)
case "Event": case "WorkflowEvent":
access := resources.NewAccessor[*resources.NativeTool](tools.NATIVE_TOOL, &tools.APIRequest{ // The resource is already a *NativeTool with Kind=WORKFLOW_EVENT set by the
Admin: true, // catalog factory. We use it directly without any DB lookup.
}, func() utils.DBObject { return &resources.NativeTool{} }) nt := resource.(*resources.NativeTool)
t, _, err := access.Search(nil, "WORKFLOW_EVENT", false) nt.Name = native_tools.WORKFLOW_EVENT.String()
if err == nil && len(t) > 0 { d.NativeTool = append(d.NativeTool, nt.GetID())
d.NativeTool = append(d.NativeTool, t[0].GetID()) graphItem.NativeTool = nt
graphItem.NativeTool = t[0].(*resources.NativeTool)
}
case "Storage": case "Storage":
d.Storages = append(d.Storages, resource.GetID()) d.Storages = append(d.Storages, resource.GetID())
d.StorageResources = append(d.StorageResources, resource.(*resources.StorageResource)) d.StorageResources = append(d.StorageResources, resource.(*resources.StorageResource))
@@ -305,7 +423,7 @@ func (d *Workflow) getNewGraphItem(dataName string, graphItem *graph.GraphItem,
d.ComputeResources = append(d.ComputeResources, resource.(*resources.ComputeResource)) d.ComputeResources = append(d.ComputeResources, resource.(*resources.ComputeResource))
graphItem.Compute = resource.(*resources.ComputeResource) graphItem.Compute = resource.(*resources.ComputeResource)
default: default:
return graphItem return nil
} }
return graphItem return graphItem
} }
@@ -390,11 +508,13 @@ func (w *Workflow) GetPricedItem(
for _, item := range w.Graph.Items { for _, item := range w.Graph.Items {
if f(item) { if f(item) {
dt, res := item.GetResource() dt, res := item.GetResource()
ord, err := res.ConvertToPricedResource(dt, &instance, &partnership, &buying, &strategy, &bookingMode, request) ord, err := res.ConvertToPricedResource(dt, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil { if err != nil {
return list_datas, err return list_datas, err
} }
list_datas[res.GetID()] = ord list_datas[res.GetID()] = ord
} }
} }
return list_datas, nil return list_datas, nil
@@ -445,6 +565,7 @@ func (ao *Workflow) VerifyAuth(callName string, request *tools.APIRequest) bool
return ao.AbstractObject.VerifyAuth(callName, request) || isAuthorized return ao.AbstractObject.VerifyAuth(callName, request) || isAuthorized
} }
// TODO : Check Booking... + Storage
/* /*
* CheckBooking is a function that checks the booking of the workflow on peers (even ourselves) * CheckBooking is a function that checks the booking of the workflow on peers (even ourselves)
*/ */
@@ -474,8 +595,36 @@ func (wfa *Workflow) CheckBooking(caller *tools.HTTPCaller) (bool, error) {
return true, nil return true, nil
} }
func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigItem, partnerships ConfigItem, buyings ConfigItem, strategies ConfigItem, bookingMode int, request *tools.APIRequest) (bool, float64, map[tools.DataType]map[string]pricing.PricedItemITF, *Workflow, error) { // preemptDelay is the minimum lead time granted before a preempted booking starts.
const preemptDelay = 30 * time.Second
// Planify computes the scheduled start/end for every resource in the workflow.
//
// bookingMode controls availability checking when p (a live planner snapshot) is provided:
// - PREEMPTED : start from now+preemptDelay regardless of existing load.
// - WHEN_POSSIBLE: start from max(now, start); if a slot conflicts, slide to the next free window.
// - PLANNED : use start as-is; return an error if the slot is not available.
//
// Passing p = nil skips all availability checks (useful for sub-workflow recursion).
func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigItem, partnerships ConfigItem, buyings ConfigItem, strategies ConfigItem, bookingMode int, p planner.PlannerITF, request *tools.APIRequest) (bool, float64, map[tools.DataType]map[string]pricing.PricedItemITF, *Workflow, error) {
// 1. Adjust global start based on booking mode.
now := time.Now()
switch booking.BookingMode(bookingMode) {
case booking.PREEMPTED:
if earliest := now.Add(preemptDelay); start.Before(earliest) {
start = earliest
}
case booking.WHEN_POSSIBLE:
if start.Before(now) {
start = now
}
// PLANNED: honour the caller's start date as-is.
}
priceds := map[tools.DataType]map[string]pricing.PricedItemITF{} priceds := map[tools.DataType]map[string]pricing.PricedItemITF{}
var err error
// 2. Plan processings first so we can derive the total workflow duration.
ps, priceds, err := plan[*resources.ProcessingResource](tools.PROCESSING_RESOURCE, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request, wf.Graph.IsProcessing, ps, priceds, err := plan[*resources.ProcessingResource](tools.PROCESSING_RESOURCE, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request, wf.Graph.IsProcessing,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) { func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
d, err := wf.Graph.GetAverageTimeProcessingBeforeStart(0, res.GetID(), d, err := wf.Graph.GetAverageTimeProcessingBeforeStart(0, res.GetID(),
@@ -486,12 +635,17 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
} }
return start.Add(time.Duration(d) * time.Second), priced.GetExplicitDurationInS(), nil return start.Add(time.Duration(d) * time.Second), priced.GetExplicitDurationInS(), nil
}, func(started time.Time, duration float64) (*time.Time, error) { }, func(started time.Time, duration float64) (*time.Time, error) {
s := started.Add(time.Duration(duration)) s := started.Add(time.Duration(duration) * time.Second)
return &s, nil return &s, nil
}) })
if err != nil { if err != nil {
return false, 0, priceds, nil, err return false, 0, priceds, nil, err
} }
// Total workflow duration used as the booking window for compute/storage.
// Returns -1 if any processing is a service (open-ended).
workflowDuration := common.GetPlannerLongestTime(priceds)
if _, priceds, err = plan[resources.ResourceInterface](tools.NATIVE_TOOL, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request, if _, priceds, err = plan[resources.ResourceInterface](tools.NATIVE_TOOL, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request,
wf.Graph.IsNativeTool, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) { wf.Graph.IsNativeTool, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
return start, 0, nil return start, 0, nil
@@ -508,11 +662,13 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
}); err != nil { }); err != nil {
return false, 0, priceds, nil, err return false, 0, priceds, nil, err
} }
// 3. Compute/storage: duration = total workflow duration (conservative bound).
for k, f := range map[tools.DataType]func(graph.GraphItem) bool{tools.STORAGE_RESOURCE: wf.Graph.IsStorage, for k, f := range map[tools.DataType]func(graph.GraphItem) bool{tools.STORAGE_RESOURCE: wf.Graph.IsStorage,
tools.COMPUTE_RESOURCE: wf.Graph.IsCompute} { tools.COMPUTE_RESOURCE: wf.Graph.IsCompute} {
if _, priceds, err = plan[resources.ResourceInterface](k, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request, if _, priceds, err = plan[resources.ResourceInterface](k, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request,
f, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) { f, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
nearestStart, longestDuration, err := wf.Graph.GetAverageTimeRelatedToProcessingActivity(start, ps, res, func(i graph.GraphItem) (r resources.ResourceInterface) { nearestStart, _, err := wf.Graph.GetAverageTimeRelatedToProcessingActivity(ps, res, func(i graph.GraphItem) (r resources.ResourceInterface) {
if f(i) { if f(i) {
_, r = i.GetResource() _, r = i.GetResource()
} }
@@ -520,27 +676,31 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
}, *instances.Get(res.GetID()), *partnerships.Get(res.GetID()), }, *instances.Get(res.GetID()), *partnerships.Get(res.GetID()),
*buyings.Get(res.GetID()), *strategies.Get(res.GetID()), bookingMode, request) *buyings.Get(res.GetID()), *strategies.Get(res.GetID()), bookingMode, request)
if err != nil { if err != nil {
return start, longestDuration, err return start, workflowDuration, err
} }
return start.Add(time.Duration(nearestStart) * time.Second), longestDuration, nil return start.Add(time.Duration(nearestStart) * time.Second), workflowDuration, nil
}, func(started time.Time, duration float64) (*time.Time, error) { }, func(started time.Time, duration float64) (*time.Time, error) {
s := started.Add(time.Duration(duration)) if duration < 0 {
return nil, nil // service: open-ended booking
}
s := started.Add(time.Duration(duration) * time.Second)
return &s, nil return &s, nil
}); err != nil { }); err != nil {
return false, 0, priceds, nil, err return false, 0, priceds, nil, err
} }
} }
longest := common.GetPlannerLongestTime(end, priceds, request)
longest := workflowDuration
if _, priceds, err = plan[resources.ResourceInterface](tools.WORKFLOW_RESOURCE, instances, partnerships, buyings, strategies, if _, priceds, err = plan[resources.ResourceInterface](tools.WORKFLOW_RESOURCE, instances, partnerships, buyings, strategies,
bookingMode, wf, priceds, request, wf.Graph.IsWorkflow, bookingMode, wf, priceds, request, wf.Graph.IsWorkflow,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) { func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
start := start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds, request)) * time.Second) start := start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds)) * time.Second)
longest := float64(-1) longest := float64(-1)
r, code, err := res.GetAccessor(request).LoadOne(res.GetID()) r, code, err := res.GetAccessor(request).LoadOne(res.GetID())
if code != 200 || err != nil { if code != 200 || err != nil {
return start, longest, err return start, longest, err
} }
_, neoLongest, priceds2, _, err := r.(*Workflow).Planify(start, end, instances, partnerships, buyings, strategies, bookingMode, request) _, neoLongest, priceds2, _, err := r.(*Workflow).Planify(start, end, instances, partnerships, buyings, strategies, bookingMode, nil, request)
// should ... import priced // should ... import priced
if err != nil { if err != nil {
return start, longest, err return start, longest, err
@@ -558,13 +718,26 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
} }
} }
return start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds, request)) * time.Second), longest, nil return start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds)) * time.Second), longest, nil
}, func(start time.Time, longest float64) (*time.Time, error) { }, func(start time.Time, longest float64) (*time.Time, error) {
s := start.Add(time.Duration(longest) * time.Second) s := start.Add(time.Duration(longest) * time.Second)
return &s, nil return &s, nil
}); err != nil { }); err != nil {
return false, 0, priceds, nil, err return false, 0, priceds, nil, err
} }
// 4. Availability check against the live planner (skipped for PREEMPTED and sub-workflows).
if p != nil && booking.BookingMode(bookingMode) != booking.PREEMPTED {
slide, err := plannerAvailabilitySlide(p, priceds, booking.BookingMode(bookingMode))
if err != nil {
return false, 0, priceds, nil, err
}
if slide > 0 {
// Re-plan from the corrected start; pass nil planner to avoid infinite recursion.
return wf.Planify(start.Add(slide), end, instances, partnerships, buyings, strategies, bookingMode, nil, request)
}
}
isPreemptible := true isPreemptible := true
for _, first := range wf.GetFirstItems() { for _, first := range wf.GetFirstItems() {
_, res := first.GetResource() _, res := first.GetResource()
@@ -576,6 +749,36 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
return isPreemptible, longest, priceds, wf, nil return isPreemptible, longest, priceds, wf, nil
} }
// plannerAvailabilitySlide checks all compute/storage resources in priceds against the planner.
// For PLANNED mode it returns an error immediately on the first conflict.
// For WHEN_POSSIBLE it returns the maximum slide (duration to add to global start) needed to
// clear all conflicts, or 0 if the plan is already conflict-free.
func plannerAvailabilitySlide(p planner.PlannerITF, priceds map[tools.DataType]map[string]pricing.PricedItemITF, mode booking.BookingMode) (time.Duration, error) {
maxSlide := time.Duration(0)
for _, dt := range []tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE} {
for _, priced := range priceds[dt] {
locStart := priced.GetLocationStart()
locEnd := priced.GetLocationEnd()
if locStart == nil || locEnd == nil {
continue // open-ended: skip availability check
}
d := locEnd.Sub(*locStart)
next := p.NextAvailableStart(priced.GetID(), priced.GetInstanceID(), *locStart, d)
slide := next.Sub(*locStart)
if slide <= 0 {
continue
}
if mode == booking.PLANNED {
return 0, errors.New("requested slot is not available for resource " + priced.GetID())
}
if slide > maxSlide {
maxSlide = slide
}
}
}
return maxSlide, nil
}
// Returns a map of DataType (processing,computing,data,storage,worfklow) where each resource (identified by its UUID) // Returns a map of DataType (processing,computing,data,storage,worfklow) where each resource (identified by its UUID)
// is mapped to the list of its items (different appearance) in the graph // is mapped to the list of its items (different appearance) in the graph
// ex: if the same Minio storage is represented by several nodes in the graph, in [tools.STORAGE_RESSOURCE] its UUID will be mapped to // ex: if the same Minio storage is represented by several nodes in the graph, in [tools.STORAGE_RESSOURCE] its UUID will be mapped to
@@ -636,9 +839,6 @@ func plan[T resources.ResourceInterface](
priced.SetLocationEnd(*e) priced.SetLocationEnd(*e)
} }
} }
if e, err := end(started, priced.GetExplicitDurationInS()); err != nil && e != nil {
priced.SetLocationEnd(*e)
}
resources = append(resources, realItem.(T)) resources = append(resources, realItem.(T))
if priceds[dt][item.ID] != nil { if priceds[dt][item.ID] != nil {
priced.AddQuantity(priceds[dt][item.ID].GetQuantity()) priced.AddQuantity(priceds[dt][item.ID].GetQuantity())

View File

@@ -14,7 +14,7 @@ import (
) )
type workflowMongoAccessor struct { type workflowMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Workflow] // AbstractAccessor contains the basic fields of an accessor (model, caller)
computeResourceAccessor utils.Accessor computeResourceAccessor utils.Accessor
collaborativeAreaAccessor utils.Accessor collaborativeAreaAccessor utils.Accessor
workspaceAccessor utils.Accessor workspaceAccessor utils.Accessor
@@ -34,10 +34,11 @@ func new(t tools.DataType, request *tools.APIRequest) *workflowMongoAccessor {
computeResourceAccessor: (&resources.ComputeResource{}).GetAccessor(request), computeResourceAccessor: (&resources.ComputeResource{}).GetAccessor(request),
collaborativeAreaAccessor: (&shallow_collaborative_area.ShallowCollaborativeArea{}).GetAccessor(request), collaborativeAreaAccessor: (&shallow_collaborative_area.ShallowCollaborativeArea{}).GetAccessor(request),
workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request), workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request),
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*Workflow]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request, Request: request,
Type: t, Type: t,
New: func() *Workflow { return &Workflow{} },
}, },
} }
} }
@@ -91,13 +92,17 @@ func (a *workflowMongoAccessor) share(realData *Workflow, delete bool, caller *t
} }
// UpdateOne updates a workflow in the database // UpdateOne updates a workflow in the database
func (a *workflowMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func (a *workflowMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
// avoid the update if the schedule is the same // avoid the update if the schedule is the same
set = a.verifyResource(set) res, code, err := utils.GenericUpdateOne(set, id, a)
if set.(*Workflow).Graph != nil && set.(*Workflow).Graph.Partial { if code != 200 {
return nil, code, err
}
res = a.verifyResource(res)
if res.(*Workflow).Graph != nil && res.(*Workflow).Graph.Partial {
return nil, 403, errors.New("you are not allowed to update a partial workflow") return nil, 403, errors.New("you are not allowed to update a partial workflow")
} }
res, code, err := utils.GenericUpdateOne(set, id, a, &Workflow{}) res, code, err = utils.GenericUpdateOne(res.Serialize(res), id, a)
if code != 200 { if code != 200 {
return nil, code, err return nil, code, err
} }
@@ -153,7 +158,7 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
return return
} }
if err == nil && len(resource) > 0 { // if the workspace already exists, update it if err == nil && len(resource) > 0 { // if the workspace already exists, update it
a.workspaceAccessor.UpdateOne(&workspace.Workspace{ w := &workspace.Workspace{
Active: active, Active: active,
ResourceSet: resources.ResourceSet{ ResourceSet: resources.ResourceSet{
Datas: workflow.Datas, Datas: workflow.Datas,
@@ -162,7 +167,8 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
Workflows: workflow.Workflows, Workflows: workflow.Workflows,
Computes: workflow.Computes, Computes: workflow.Computes,
}, },
}, resource[0].GetID()) }
a.workspaceAccessor.UpdateOne(w.Serialize(w), resource[0].GetID())
} else { // if the workspace does not exist, create it } else { // if the workspace does not exist, create it
a.workspaceAccessor.StoreOne(&workspace.Workspace{ a.workspaceAccessor.StoreOne(&workspace.Workspace{
Active: active, Active: active,
@@ -179,19 +185,16 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
} }
func (a *workflowMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *workflowMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Workflow](id, func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
w := d.(*Workflow) w := d.(*Workflow)
a.execute(w, false, true) // if no workspace is attached to the workflow, create it a.execute(w, false, true) // if no workspace is attached to the workflow, create it
return d, 200, nil return d, 200, nil
}, a) }, a)
} }
func (a *workflowMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Workflow](func(d utils.DBObject) utils.ShallowDBObject { return &d.(*Workflow).AbstractObject }, isDraft, a)
}
func (a *workflowMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { func (a *workflowMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Workflow](filters, search, (&Workflow{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject { return a.verifyResource(d) }, isDraft, a) return utils.GenericSearch[*Workflow](filters, search, a.New().GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject { return a.verifyResource(d) }, isDraft, a)
} }
func (a *workflowMongoAccessor) verifyResource(obj utils.DBObject) utils.DBObject { func (a *workflowMongoAccessor) verifyResource(obj utils.DBObject) utils.DBObject {
@@ -205,17 +208,18 @@ func (a *workflowMongoAccessor) verifyResource(obj utils.DBObject) utils.DBObjec
continue continue
} }
var access utils.Accessor var access utils.Accessor
if t == tools.COMPUTE_RESOURCE { switch t {
case tools.COMPUTE_RESOURCE:
access = resources.NewAccessor[*resources.ComputeResource](t, a.GetRequest(), func() utils.DBObject { return &resources.ComputeResource{} }) access = resources.NewAccessor[*resources.ComputeResource](t, a.GetRequest(), func() utils.DBObject { return &resources.ComputeResource{} })
} else if t == tools.PROCESSING_RESOURCE { case tools.PROCESSING_RESOURCE:
access = resources.NewAccessor[*resources.ProcessingResource](t, a.GetRequest(), func() utils.DBObject { return &resources.ProcessingResource{} }) access = resources.NewAccessor[*resources.ProcessingResource](t, a.GetRequest(), func() utils.DBObject { return &resources.ProcessingResource{} })
} else if t == tools.STORAGE_RESOURCE { case tools.STORAGE_RESOURCE:
access = resources.NewAccessor[*resources.StorageResource](t, a.GetRequest(), func() utils.DBObject { return &resources.StorageResource{} }) access = resources.NewAccessor[*resources.StorageResource](t, a.GetRequest(), func() utils.DBObject { return &resources.StorageResource{} })
} else if t == tools.WORKFLOW_RESOURCE { case tools.WORKFLOW_RESOURCE:
access = resources.NewAccessor[*resources.WorkflowResource](t, a.GetRequest(), func() utils.DBObject { return &resources.WorkflowResource{} }) access = resources.NewAccessor[*resources.WorkflowResource](t, a.GetRequest(), func() utils.DBObject { return &resources.WorkflowResource{} })
} else if t == tools.DATA_RESOURCE { case tools.DATA_RESOURCE:
access = resources.NewAccessor[*resources.DataResource](t, a.GetRequest(), func() utils.DBObject { return &resources.DataResource{} }) access = resources.NewAccessor[*resources.DataResource](t, a.GetRequest(), func() utils.DBObject { return &resources.DataResource{} })
} else { default:
wf.Graph.Clear(resource.GetID()) wf.Graph.Clear(resource.GetID())
} }
if error := utils.VerifyAccess(access, resource.GetID()); error != nil { if error := utils.VerifyAccess(access, resource.GetID()); error != nil {

View File

@@ -4,26 +4,35 @@ import (
"testing" "testing"
"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/tools"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestStoreOneWorkflow(t *testing.T) { func TestNewWorkflowAccessor(t *testing.T) {
w := Workflow{ req := &tools.APIRequest{}
acc := NewAccessor(req)
assert.NotNil(t, acc)
}
func TestWorkflow_StoreDraftDefault(t *testing.T) {
w := &Workflow{
AbstractObject: utils.AbstractObject{Name: "testWorkflow"}, AbstractObject: utils.AbstractObject{Name: "testWorkflow"},
} }
w.StoreDraftDefault()
wma := NewAccessor(nil) assert.False(t, w.IsDraft)
id, _, _ := wma.StoreOne(&w)
assert.NotEmpty(t, id)
} }
func TestLoadOneWorkflow(t *testing.T) { func TestWorkflow_VerifyAuth_NilRequest(t *testing.T) {
w := Workflow{ w := &Workflow{
AbstractObject: utils.AbstractObject{Name: "testWorkflow"}, AbstractObject: utils.AbstractObject{},
}
result := w.VerifyAuth("get", nil)
assert.False(t, result)
} }
wma := NewAccessor(nil) func TestWorkflow_VerifyAuth_AdminRequest(t *testing.T) {
new_w, _, _ := wma.StoreOne(&w) w := &Workflow{}
assert.Equal(t, w, new_w) req := &tools.APIRequest{Admin: true}
result := w.VerifyAuth("get", req)
assert.True(t, result)
} }

View File

@@ -0,0 +1,172 @@
package workflow_execution_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- WorkflowExecution model ----
func TestWorkflowExecution_StoreDraftDefault(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.StoreDraftDefault()
assert.False(t, we.IsDraft)
assert.Equal(t, enum.SCHEDULED, we.State)
}
func TestWorkflowExecution_CanDelete_Draft(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.IsDraft = true
assert.True(t, we.CanDelete())
}
func TestWorkflowExecution_CanDelete_NonDraft(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.IsDraft = false
assert.False(t, we.CanDelete())
}
func TestWorkflowExecution_CanUpdate_StateChange(t *testing.T) {
we := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
set := &workflow_execution.WorkflowExecution{State: enum.STARTED}
ok, returned := we.CanUpdate(set)
assert.True(t, ok)
// Only the state should be propagated
assert.Equal(t, enum.STARTED, returned.(*workflow_execution.WorkflowExecution).State)
}
func TestWorkflowExecution_CanUpdate_SameState_NonDraft(t *testing.T) {
we := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
we.IsDraft = false
set := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
ok, _ := we.CanUpdate(set)
// !r.IsDraft == true → ok
assert.True(t, ok)
}
func TestWorkflowExecution_CanUpdate_SameState_Draft(t *testing.T) {
we := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
we.IsDraft = true
set := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
ok, _ := we.CanUpdate(set)
// !r.IsDraft == false (it is draft) → ok false
assert.False(t, ok)
}
func TestWorkflowExecution_Equals_True(t *testing.T) {
now := time.Now()
a := &workflow_execution.WorkflowExecution{WorkflowID: "wf-1"}
a.ExecDate = now
b := &workflow_execution.WorkflowExecution{WorkflowID: "wf-1"}
b.ExecDate = now
assert.True(t, a.Equals(b))
}
func TestWorkflowExecution_Equals_DifferentDate(t *testing.T) {
a := &workflow_execution.WorkflowExecution{WorkflowID: "wf-1"}
a.ExecDate = time.Now()
b := &workflow_execution.WorkflowExecution{WorkflowID: "wf-1"}
b.ExecDate = time.Now().Add(time.Hour)
assert.False(t, a.Equals(b))
}
func TestWorkflowExecution_Equals_DifferentWorkflow(t *testing.T) {
now := time.Now()
a := &workflow_execution.WorkflowExecution{WorkflowID: "wf-1"}
a.ExecDate = now
b := &workflow_execution.WorkflowExecution{WorkflowID: "wf-2"}
b.ExecDate = now
assert.False(t, a.Equals(b))
}
func TestWorkflowExecution_GetName(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.UUID = "exec-uuid"
we.ExecDate = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
name := we.GetName()
assert.Contains(t, name, "exec-uuid")
assert.Contains(t, name, "2026")
}
func TestWorkflowExecution_GenerateID(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.GenerateID()
assert.NotEmpty(t, we.UUID)
}
func TestWorkflowExecution_GenerateID_KeepsExisting(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.UUID = "existing-uuid"
we.GenerateID()
assert.Equal(t, "existing-uuid", we.UUID)
}
func TestWorkflowExecution_VerifyAuth_AlwaysTrue(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
assert.True(t, we.VerifyAuth("get", nil))
assert.True(t, we.VerifyAuth("delete", &tools.APIRequest{}))
}
func TestWorkflowExecution_GetAccessor(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
acc := we.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
// ---- ArgoStatusToState ----
func TestArgoStatusToState_Succeeded(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.ArgoStatusToState("succeeded")
assert.Equal(t, enum.SUCCESS, we.State)
}
func TestArgoStatusToState_Pending(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.ArgoStatusToState("pending")
assert.Equal(t, enum.SCHEDULED, we.State)
}
func TestArgoStatusToState_Running(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.ArgoStatusToState("running")
assert.Equal(t, enum.STARTED, we.State)
}
func TestArgoStatusToState_Default_Failed(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.ArgoStatusToState("failed")
assert.Equal(t, enum.FAILURE, we.State)
}
func TestArgoStatusToState_CaseInsensitive(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
we.ArgoStatusToState("SUCCEEDED")
assert.Equal(t, enum.SUCCESS, we.State)
}
func TestArgoStatusToState_ReturnsPointer(t *testing.T) {
we := &workflow_execution.WorkflowExecution{}
result := we.ArgoStatusToState("running")
assert.Equal(t, we, result)
}
// ---- NewAccessor ----
func TestNewWorkflowExecutionAccessor(t *testing.T) {
acc := workflow_execution.NewAccessor(&tools.APIRequest{Admin: true})
assert.NotNil(t, acc)
assert.Equal(t, tools.WORKFLOW_EXECUTION, acc.GetType())
}
func TestNewWorkflowExecutionAccessor_NilRequest(t *testing.T) {
acc := workflow_execution.NewAccessor(nil)
assert.NotNil(t, acc)
assert.Equal(t, "", acc.GetUser())
assert.Equal(t, "", acc.GetPeerID())
}

View File

@@ -1,164 +0,0 @@
package workflow_execution_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"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/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockAccessor struct {
mock.Mock
}
func (m *MockAccessor) LoadOne(id string) (interface{}, int, error) {
args := m.Called(id)
return args.Get(0), args.Int(1), args.Error(2)
}
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return nil, args.Int(1), args.Error(2)
}
func (m *MockAccessor) Search(filters interface{}, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func TestStoreDraftDefault(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.StoreDraftDefault()
assert.False(t, exec.IsDraft)
assert.Equal(t, enum.SCHEDULED, exec.State)
}
func TestCanUpdate_StateChange(t *testing.T) {
existing := &workflow_execution.WorkflowExecution{State: enum.DRAFT}
newExec := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
canUpdate, updated := existing.CanUpdate(newExec)
assert.True(t, canUpdate)
assert.Equal(t, enum.SCHEDULED, updated.(*workflow_execution.WorkflowExecution).State)
}
func TestCanUpdate_SameState_Draft(t *testing.T) {
existing := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}, State: enum.DRAFT}
newExec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}, State: enum.DRAFT}
canUpdate, _ := existing.CanUpdate(newExec)
assert.False(t, canUpdate)
}
func TestCanDelete_TrueIfDraft(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}}
assert.True(t, exec.CanDelete())
}
func TestCanDelete_FalseIfNotDraft(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: false}}
assert.False(t, exec.CanDelete())
}
func TestEquals_True(t *testing.T) {
d := time.Now()
exec1 := &workflow_execution.WorkflowExecution{ExecDate: d, WorkflowID: "123"}
exec2 := &workflow_execution.WorkflowExecution{ExecDate: d, WorkflowID: "123"}
assert.True(t, exec1.Equals(exec2))
}
func TestEquals_False(t *testing.T) {
exec1 := &workflow_execution.WorkflowExecution{ExecDate: time.Now(), WorkflowID: "abc"}
exec2 := &workflow_execution.WorkflowExecution{ExecDate: time.Now().Add(time.Hour), WorkflowID: "def"}
assert.False(t, exec1.Equals(exec2))
}
func TestArgoStatusToState_Success(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.ArgoStatusToState("succeeded")
assert.Equal(t, enum.SUCCESS, exec.State)
}
func TestArgoStatusToState_DefaultToFailure(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.ArgoStatusToState("unknown")
assert.Equal(t, enum.FAILURE, exec.State)
}
func TestGenerateID_AssignsUUID(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.GenerateID()
assert.NotEmpty(t, exec.UUID)
}
func TestGetName_ReturnsCorrectFormat(t *testing.T) {
time := time.Now()
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{UUID: "abc"}, ExecDate: time}
assert.Contains(t, exec.GetName(), "abc")
assert.Contains(t, exec.GetName(), time.String())
}
func TestVerifyAuth_AlwaysTrue(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
assert.True(t, exec.VerifyAuth("get", nil))
}
func TestUpdateOne_RejectsZeroState(t *testing.T) {
accessor := &workflow_execution.WorkflowExecutionMongoAccessor{}
set := &workflow_execution.WorkflowExecution{State: 0}
_, code, err := accessor.UpdateOne(set, "someID")
assert.Equal(t, 400, code)
assert.Error(t, err)
}
func TestLoadOne_DraftExpired_ShouldDelete(t *testing.T) {
// Normally would mock time.Now and delete call; for now we test structure
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
exec := &workflow_execution.WorkflowExecution{
ExecDate: time.Now().Add(-2 * time.Minute),
State: enum.DRAFT,
AbstractObject: utils.AbstractObject{UUID: "to-delete"},
}
_, _, _ = accessor.LoadOne(exec.GetID())
// No panic = good enough placeholder
}
func TestLoadOne_ScheduledExpired_ShouldUpdateToForgotten(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
exec := &workflow_execution.WorkflowExecution{
ExecDate: time.Now().Add(-2 * time.Minute),
State: enum.SCHEDULED,
AbstractObject: utils.AbstractObject{UUID: "to-forget"},
}
_, _, _ = accessor.LoadOne(exec.GetID())
}
func TestDeleteOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.DeleteOne("someID")
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestStoreOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.StoreOne(nil)
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestCopyOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.CopyOne(nil)
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestGetExecFilters_BasicPattern(t *testing.T) {
a := workflow_execution.NewAccessor(&tools.APIRequest{})
filters := a.GetExecFilters("foo")
assert.Contains(t, filters.Or["abstractobject.name"][0].Value, "foo")
}

View File

@@ -33,6 +33,9 @@ type WorkflowExecution struct {
State enum.BookingStatus `json:"state" bson:"state" default:"0"` // TEMPORARY TODO DEFAULT 1 -> 0 State is the state of the workflow State enum.BookingStatus `json:"state" bson:"state" default:"0"` // TEMPORARY TODO DEFAULT 1 -> 0 State is the state of the workflow
WorkflowID string `json:"workflow_id" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow WorkflowID string `json:"workflow_id" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
BookingsState map[string]bool `json:"bookings_state" bson:"bookings_state,omitempty"` // WorkflowID is the ID of the workflow
PurchasesState map[string]bool `json:"purchases_state" bson:"purchases_state,omitempty"` // WorkflowID is the ID of the workflow
SelectedInstances workflow.ConfigItem `json:"selected_instances"` SelectedInstances workflow.ConfigItem `json:"selected_instances"`
SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"` SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
SelectedBuyings workflow.ConfigItem `json:"selected_buyings"` SelectedBuyings workflow.ConfigItem `json:"selected_buyings"`
@@ -40,8 +43,8 @@ type WorkflowExecution struct {
} }
func (r *WorkflowExecution) StoreDraftDefault() { func (r *WorkflowExecution) StoreDraftDefault() {
r.IsDraft = false // TODO: TEMPORARY r.IsDraft = true
r.State = enum.SCHEDULED r.State = enum.DRAFT
} }
func (r *WorkflowExecution) CanUpdate(set utils.DBObject) (bool, utils.DBObject) { func (r *WorkflowExecution) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
@@ -62,7 +65,7 @@ func (wfa *WorkflowExecution) Equals(we *WorkflowExecution) bool {
func (ws *WorkflowExecution) PurgeDraft(request *tools.APIRequest) error { func (ws *WorkflowExecution) PurgeDraft(request *tools.APIRequest) error {
if ws.EndDate == nil { if ws.EndDate == nil {
// if no end... then Book like a savage // if no end... then Book like a savage
e := ws.ExecDate.Add(time.Hour) e := ws.ExecDate.UTC().Add(time.Hour)
ws.EndDate = &e ws.EndDate = &e
} }
accessor := ws.GetAccessor(request) accessor := ws.GetAccessor(request)
@@ -127,6 +130,10 @@ use of a datacenter or storage can't be buy for permanent access.
func (d *WorkflowExecution) Buy(bs pricing.BillingStrategy, executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource { func (d *WorkflowExecution) Buy(bs pricing.BillingStrategy, executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource {
purchases := d.buyEach(bs, executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE]) purchases := d.buyEach(bs, executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])
purchases = append(purchases, d.buyEach(bs, executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...) purchases = append(purchases, d.buyEach(bs, executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
d.PurchasesState = map[string]bool{}
for _, p := range purchases {
d.PurchasesState[p.GetID()] = false
}
return purchases return purchases
} }
@@ -146,7 +153,7 @@ func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID str
d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = []string{} d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = []string{}
} }
start := d.ExecDate start := d.ExecDate
if s := priced.GetLocationStart(); s != nil { if s := priced.GetLocationStart(); s != nil && s.After(time.Now()) {
start = *s start = *s
} }
var m map[string]interface{} var m map[string]interface{}
@@ -157,11 +164,14 @@ func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID str
AbstractObject: utils.AbstractObject{ AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(), UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID, Name: d.GetName() + "_" + executionsID + "_" + wfID,
IsDraft: true,
}, },
PricedItem: m, PricedItem: m,
ExecutionID: d.GetID(),
ExecutionsID: executionsID, ExecutionsID: executionsID,
DestPeerID: priced.GetCreatorID(), DestPeerID: priced.GetCreatorID(),
ResourceID: priced.GetID(), ResourceID: priced.GetID(),
InstanceID: priced.GetInstanceID(),
ResourceType: dt, ResourceType: dt,
EndDate: &end, EndDate: &end,
} }
@@ -177,6 +187,12 @@ func (d *WorkflowExecution) Book(executionsID string, wfID string, priceds map[t
booking = append(booking, d.bookEach(executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])...) booking = append(booking, d.bookEach(executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])...)
booking = append(booking, d.bookEach(executionsID, wfID, tools.COMPUTE_RESOURCE, priceds[tools.COMPUTE_RESOURCE])...) booking = append(booking, d.bookEach(executionsID, wfID, tools.COMPUTE_RESOURCE, priceds[tools.COMPUTE_RESOURCE])...)
booking = append(booking, d.bookEach(executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...) booking = append(booking, d.bookEach(executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
for _, p := range booking {
if d.BookingsState == nil {
d.BookingsState = map[string]bool{}
}
d.BookingsState[p.GetID()] = false
}
return booking return booking
} }
@@ -196,10 +212,20 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
d.PeerBookByGraph[priced.GetCreatorID()][itemID] = []string{} d.PeerBookByGraph[priced.GetCreatorID()][itemID] = []string{}
} }
start := d.ExecDate start := d.ExecDate
if s := priced.GetLocationStart(); s != nil { if s := priced.GetLocationStart(); s != nil && s.After(time.Now()) {
start = *s start = *s
} }
end := start.Add(time.Duration(priced.GetExplicitDurationInS()) * time.Second) // Prefer LocationEnd set by Planify; fall back to ExplicitDurationInS only
// when Planify did not compute an end (open-ended / service resources).
var endDate *time.Time
if locEnd := priced.GetLocationEnd(); locEnd != nil {
endDate = locEnd
} else if durationS := priced.GetExplicitDurationInS(); durationS > 0 {
e := start.Add(time.Duration(durationS) * time.Second)
endDate = &e
}
// durationS < 0 means the resource is a service (runs indefinitely):
// leave ExpectedEndDate nil so the booking is open-ended.
var m map[string]interface{} var m map[string]interface{}
b, _ := json.Marshal(priced) b, _ := json.Marshal(priced)
json.Unmarshal(b, &m) json.Unmarshal(b, &m)
@@ -207,17 +233,19 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
AbstractObject: utils.AbstractObject{ AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(), UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID, Name: d.GetName() + "_" + executionsID + "_" + wfID,
IsDraft: true,
}, },
PricedItem: m, PricedItem: m,
ExecutionsID: executionsID, ExecutionsID: executionsID,
State: enum.SCHEDULED, State: enum.DRAFT,
ResourceID: priced.GetID(), ResourceID: priced.GetID(),
InstanceID: priced.GetInstanceID(),
ResourceType: dt, ResourceType: dt,
DestPeerID: priced.GetCreatorID(), DestPeerID: priced.GetCreatorID(),
WorkflowID: wfID, WorkflowID: wfID,
ExecutionID: d.GetID(), ExecutionID: d.GetID(),
ExpectedStartDate: start, ExpectedStartDate: start,
ExpectedEndDate: &end, ExpectedEndDate: endDate,
} }
items = append(items, bookingItem) items = append(items, bookingItem)
d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append( d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append(

View File

@@ -12,17 +12,19 @@ import (
) )
type WorkflowExecutionMongoAccessor struct { type WorkflowExecutionMongoAccessor struct {
utils.AbstractAccessor utils.AbstractAccessor[*WorkflowExecution]
shallow bool shallow bool
} }
func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor { func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{ return &WorkflowExecutionMongoAccessor{
shallow: true, shallow: true,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*WorkflowExecution]{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.WORKFLOW_EXECUTION, Type: tools.WORKFLOW_EXECUTION,
New: func() *WorkflowExecution { return &WorkflowExecution{} },
NotImplemented: []string{"CopyOne"},
}, },
} }
} }
@@ -30,36 +32,27 @@ func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccess
func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor { func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{ return &WorkflowExecutionMongoAccessor{
shallow: false, shallow: false,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor[*WorkflowExecution]{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.WORKFLOW_EXECUTION, Type: tools.WORKFLOW_EXECUTION,
New: func() *WorkflowExecution { return &WorkflowExecution{} },
NotImplemented: []string{"CopyOne"},
}, },
} }
} }
func (wfa *WorkflowExecutionMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { func (wfa *WorkflowExecutionMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented") if set["state"] == nil {
}
func (wfa *WorkflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if set.(*WorkflowExecution).State == 0 {
return nil, 400, errors.New("state is required") return nil, 400, errors.New("state is required")
} }
realSet := WorkflowExecution{State: set.(*WorkflowExecution).State} return utils.GenericUpdateOne(map[string]interface{}{
return utils.GenericUpdateOne(&realSet, id, wfa, &WorkflowExecution{}) "state": set["state"],
} }, id, wfa)
func (wfa *WorkflowExecutionMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented")
}
func (wfa *WorkflowExecutionMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented")
} }
func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*WorkflowExecution](id, func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now() now := time.Now()
now = now.Add(time.Second * -60) now = now.Add(time.Second * -60)
if d.(*WorkflowExecution).State == enum.DRAFT && !a.shallow && now.UTC().After(d.(*WorkflowExecution).ExecDate) { if d.(*WorkflowExecution).State == enum.DRAFT && !a.shallow && now.UTC().After(d.(*WorkflowExecution).ExecDate) {
@@ -73,16 +66,7 @@ func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int
return d, 200, nil return d, 200, nil
}, a) }, a)
} }
func (a *WorkflowExecutionMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
func (a *WorkflowExecutionMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*WorkflowExecution](a.getExec(), isDraft, a)
}
func (a *WorkflowExecutionMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*WorkflowExecution](filters, search, a.GetExecFilters(search), a.getExec(), isDraft, a)
}
func (a *WorkflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now() now := time.Now()
now = now.Add(time.Second * -60) now = now.Add(time.Second * -60)
@@ -99,7 +83,7 @@ func (a *WorkflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.Sh
} }
} }
func (a *WorkflowExecutionMongoAccessor) GetExecFilters(search string) *dbs.Filters { func (a *WorkflowExecutionMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
return &dbs.Filters{ return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by name if no filters are provided Or: map[string][]dbs.Filter{ // filter by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}}, "abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}},

View File

@@ -18,34 +18,48 @@ type MockWorkspaceAccessor struct {
workspace.Workspace workspace.Workspace
} }
func safeDBObject(v interface{}) utils.DBObject {
if v == nil {
return nil
}
return v.(utils.DBObject)
}
func safeShallowList(v interface{}) []utils.ShallowDBObject {
if v == nil {
return nil
}
return v.([]utils.ShallowDBObject)
}
func (m *MockWorkspaceAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func (m *MockWorkspaceAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data) args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
} }
func (m *MockWorkspaceAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func (m *MockWorkspaceAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id) args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
} }
func (m *MockWorkspaceAccessor) DeleteOne(id string) (utils.DBObject, int, error) { func (m *MockWorkspaceAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id) args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
} }
func (m *MockWorkspaceAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (m *MockWorkspaceAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id) args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2) return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
} }
func (m *MockWorkspaceAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { func (m *MockWorkspaceAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft) args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2) return safeShallowList(args.Get(0)), args.Int(1), args.Error(2)
} }
func (m *MockWorkspaceAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { func (m *MockWorkspaceAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft) args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2) return safeShallowList(args.Get(0)), args.Int(1), args.Error(2)
} }
func TestStoreOne_Success(t *testing.T) { func TestStoreOne_Success(t *testing.T) {

View File

@@ -13,7 +13,7 @@ import (
// Workspace is a struct that represents a workspace // Workspace is a struct that represents a workspace
type workspaceMongoAccessor struct { type workspaceMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Workspace] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the workspaceMongoAccessor // New creates a new instance of the workspaceMongoAccessor
@@ -28,10 +28,11 @@ func NewAccessor(request *tools.APIRequest) *workspaceMongoAccessor {
// New creates a new instance of the workspaceMongoAccessor // New creates a new instance of the workspaceMongoAccessor
func new(t tools.DataType, request *tools.APIRequest) *workspaceMongoAccessor { func new(t tools.DataType, request *tools.APIRequest) *workspaceMongoAccessor {
return &workspaceMongoAccessor{ return &workspaceMongoAccessor{
utils.AbstractAccessor{ utils.AbstractAccessor[*Workspace]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request, Request: request,
Type: t, Type: t,
New: func() *Workspace { return &Workspace{} },
}, },
} }
} }
@@ -47,21 +48,19 @@ func (a *workspaceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, erro
} }
// UpdateOne updates a workspace in the database, given its ID, it automatically share to peers if the workspace is shared // UpdateOne updates a workspace in the database, given its ID, it automatically share to peers if the workspace is shared
func (a *workspaceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func (a *workspaceMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
d := set.(*Workspace) // Get the workspace from the set if set["active"] == true { // If the workspace is active, deactivate all the other workspaces
d.Clear()
if d.Active { // If the workspace is active, deactivate all the other workspaces
res, _, err := a.LoadAll(true) res, _, err := a.LoadAll(true)
if err == nil { if err == nil {
for _, r := range res { for _, r := range res {
if r.GetID() != id { if r.GetID() != id {
r.(*Workspace).Active = false set["active"] = false
a.UpdateOne(r.(*Workspace), r.GetID()) a.UpdateOne(set, r.GetID())
} }
} }
} }
} }
res, code, err := utils.GenericUpdateOne(set, id, a, &Workspace{}) res, code, err := utils.GenericUpdateOne(set, id, a)
if code == 200 && res != nil { if code == 200 && res != nil {
a.share(res.(*Workspace), tools.PUT, a.GetCaller()) a.share(res.(*Workspace), tools.PUT, a.GetCaller())
} }
@@ -87,13 +86,8 @@ func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject,
return utils.GenericStoreOne(d, a) return utils.GenericStoreOne(d, a)
} }
// CopyOne copies a workspace in the database
func (a *workspaceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *workspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *workspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Workspace](id, func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne[*Workspace](id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
d.(*Workspace).Fill(a.GetRequest()) d.(*Workspace).Fill(a.GetRequest())
return d, 200, nil return d, 200, nil
}, a) }, a)

View File

@@ -157,7 +157,7 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
reachable := false reachable := false
for _, api := range apis { // Check the state of each remote API in the list for _, api := range apis { // Check the state of each remote API in the list
var resp APIStatusResponse var resp APIStatusResponse
b, err := caller.CallGet("http://"+api.API()+":8080", "/oc/version/status") // Call the status endpoint of the remote API (standard OC status endpoint) b, err := caller.CallGet("http://"+api.InnerAPI()+":8080", "/oc/version/status") // Call the status endpoint of the remote API (standard OC status endpoint)
if err != nil { if err != nil {
l.Error().Msg(api.String() + " not reachable") l.Error().Msg(api.String() + " not reachable")
state = REDUCED_SERVICE // If a remote API is not reachable, return reduced service state = REDUCED_SERVICE // If a remote API is not reachable, return reduced service

View File

@@ -1,6 +1,10 @@
package tools package tools
import "strings" import (
"strings"
"cloud.o-forge.io/core/oc-lib/config"
)
type DataType int type DataType int
@@ -23,36 +27,45 @@ const (
WORKSPACE_HISTORY WORKSPACE_HISTORY
ORDER ORDER
PURCHASE_RESOURCE PURCHASE_RESOURCE
ADMIRALTY_SOURCE
ADMIRALTY_TARGET
ADMIRALTY_SECRET
ADMIRALTY_KUBECONFIG
ADMIRALTY_NODES
LIVE_DATACENTER LIVE_DATACENTER
LIVE_STORAGE LIVE_STORAGE
BILL BILL
MINIO_SVCACC
MINIO_SVCACC_SECRET
NATIVE_TOOL NATIVE_TOOL
EXECUTION_VERIFICATION
) )
var NOAPI = "" var NOAPI = func() string {
var CATALOGAPI = "oc-catalog" return ""
var SHAREDAPI = "oc-shared" }
var WORKFLOWAPI = "oc-workflow" var CATALOGAPI = func() string {
var WORKSPACEAPI = "oc-workspace" return config.GetConfig().InternalCatalogAPI
var PEERSAPI = "oc-peer" }
var DATACENTERAPI = "oc-datacenter" var SHAREDAPI = func() string {
var PURCHASEAPI = "oc-catalog/purchase" return config.GetConfig().InternalSharedAPI
var ADMIRALTY_SOURCEAPI = DATACENTERAPI + "/admiralty/source" }
var ADMIRALTY_TARGETAPI = DATACENTERAPI + "/admiralty/target" var WORKFLOWAPI = func() string {
var ADMIRALTY_SECRETAPI = DATACENTERAPI + "/admiralty/secret" return config.GetConfig().InternalWorkflowAPI
var ADMIRALTY_KUBECONFIGAPI = DATACENTERAPI + "/admiralty/kubeconfig" }
var ADMIRALTY_NODESAPI = DATACENTERAPI + "/admiralty/node" var WORKSPACEAPI = func() string {
var MINIO = DATACENTERAPI + "/minio" return config.GetConfig().InternalWorkspaceAPI
}
var PEERSAPI = func() string {
return config.GetConfig().InternalPeerAPI
}
var DATACENTERAPI = func() string {
return config.GetConfig().InternalDatacenterAPI
}
var SCHEDULERAPI = func() string {
return config.GetConfig().InternalSchedulerAPI
}
var PURCHASEAPI = func() string {
return config.GetConfig().InternalCatalogAPI + "/purchase"
}
// Bind the standard API name to the data type // Bind the standard API name to the data type
var DefaultAPI = [...]string{ var InnerDefaultAPI = [...]func() string{
NOAPI, NOAPI,
CATALOGAPI, CATALOGAPI,
CATALOGAPI, CATALOGAPI,
@@ -65,22 +78,16 @@ var DefaultAPI = [...]string{
PEERSAPI, PEERSAPI,
SHAREDAPI, SHAREDAPI,
SHAREDAPI, SHAREDAPI,
DATACENTERAPI, SCHEDULERAPI,
NOAPI, NOAPI,
NOAPI, NOAPI,
NOAPI, NOAPI,
PURCHASEAPI, PURCHASEAPI,
ADMIRALTY_SOURCEAPI,
ADMIRALTY_TARGETAPI,
ADMIRALTY_SECRETAPI,
ADMIRALTY_KUBECONFIGAPI,
ADMIRALTY_NODESAPI,
DATACENTERAPI, DATACENTERAPI,
DATACENTERAPI, DATACENTERAPI,
NOAPI, NOAPI,
MINIO,
MINIO,
CATALOGAPI, CATALOGAPI,
SCHEDULERAPI,
} }
// Bind the standard data name to the data type // Bind the standard data name to the data type
@@ -102,25 +109,28 @@ var Str = [...]string{
"workspace_history", "workspace_history",
"order", "order",
"purchase_resource", "purchase_resource",
"admiralty_source",
"admiralty_target",
"admiralty_secret",
"admiralty_kubeconfig",
"admiralty_node",
"live_datacenter", "live_datacenter",
"live_storage", "live_storage",
"bill", "bill",
"service_account",
"secret",
"native_tool", "native_tool",
"execution_verification",
}
func FromString(comp string) int {
for i, str := range Str {
if str == comp {
return i
}
}
return -1
} }
func FromInt(i int) string { func FromInt(i int) string {
return Str[i] return Str[i]
} }
func (d DataType) API() string { // API - Returns the API name of the data type func (d DataType) InnerAPI() string { // API - Returns the API name of the data type
return DefaultAPI[d] return InnerDefaultAPI[d]()
} }
func (d DataType) String() string { // String - Returns the string name of the data type func (d DataType) String() string { // String - Returns the string name of the data type
@@ -138,7 +148,7 @@ func (d DataType) EnumIndex() int {
func DataTypeList() []DataType { func DataTypeList() []DataType {
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE, return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE,
WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY, WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY,
ORDER, PURCHASE_RESOURCE, ADMIRALTY_SOURCE, ADMIRALTY_TARGET, ADMIRALTY_SECRET, ADMIRALTY_KUBECONFIG, ADMIRALTY_NODES, ORDER, PURCHASE_RESOURCE,
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL} LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL}
} }
@@ -156,6 +166,12 @@ const (
PB_CREATE PB_CREATE
PB_UPDATE PB_UPDATE
PB_DELETE PB_DELETE
PB_PLANNER
PB_CLOSE_PLANNER
PB_CONSIDERS
PB_ADMIRALTY_CONFIG
PB_MINIO_CONFIG
PB_CLOSE_SEARCH
NONE NONE
) )
@@ -171,12 +187,25 @@ func GetActionString(ss string) PubSubAction {
return PB_DELETE return PB_DELETE
case "search_response": case "search_response":
return PB_SEARCH_RESPONSE return PB_SEARCH_RESPONSE
case "planner":
return PB_PLANNER
case "close_planner":
return PB_CLOSE_PLANNER
case "considers":
return PB_CONSIDERS
case "admiralty_config":
return PB_ADMIRALTY_CONFIG
case "minio_config":
return PB_MINIO_CONFIG
case "close_search":
return PB_CLOSE_SEARCH
default: default:
return NONE return NONE
} }
} }
var path = []string{"search", "search_response", "create", "update", "delete"} var path = []string{"search", "search_response", "create", "update", "delete", "planner", "close_planner",
"considers", "admiralty_config", "minio_config", "close_search"}
func (m PubSubAction) String() string { func (m PubSubAction) String() string {
return strings.ToUpper(path[m]) return strings.ToUpper(path[m])

610
tools/kubernetes.go Normal file
View File

@@ -0,0 +1,610 @@
package tools
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"cloud.o-forge.io/core/oc-lib/logs"
authv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
apply "k8s.io/client-go/applyconfigurations/core/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
var gvrSources = schema.GroupVersionResource{Group: "multicluster.admiralty.io", Version: "v1alpha1", Resource: "sources"}
var gvrTargets = schema.GroupVersionResource{Group: "multicluster.admiralty.io", Version: "v1alpha1", Resource: "targets"}
type KubernetesService struct {
Set *kubernetes.Clientset
Host string
CA string
Cert string
Data string
}
func NewDynamicClient(host string, ca string, cert string, data string) (*dynamic.DynamicClient, error) {
decodedCa, _ := base64.StdEncoding.DecodeString(ca)
decodedCert, _ := base64.StdEncoding.DecodeString(cert)
decodedKey, _ := base64.StdEncoding.DecodeString(data)
config := &rest.Config{
Host: host,
TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(decodedCa),
CertData: []byte(decodedCert),
KeyData: []byte(decodedKey),
},
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
return nil, errors.New("Error creating Dynamic client: " + err.Error())
}
if dynamicClient == nil {
return nil, errors.New("Error creating Dynamic client: dynamicClient is nil")
}
return dynamicClient, nil
}
func NewKubernetesService(host string, ca string, cert string, data string) (*KubernetesService, error) {
decodedCa, _ := base64.StdEncoding.DecodeString(ca)
decodedCert, _ := base64.StdEncoding.DecodeString(cert)
decodedKey, _ := base64.StdEncoding.DecodeString(data)
config := &rest.Config{
Host: host,
TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(decodedCa),
CertData: []byte(decodedCert),
KeyData: []byte(decodedKey),
},
}
// Create clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, errors.New("Error creating Kubernetes client: " + err.Error())
}
if clientset == nil {
return nil, errors.New("Error creating Kubernetes client: clientset is nil")
}
return &KubernetesService{
Set: clientset,
Host: host,
CA: ca,
Cert: cert,
Data: data,
}, nil
}
func (k *KubernetesService) CreateNamespace(ctx context.Context, ns string) error {
// Define the namespace
fmt.Println("ExecutionID in CreateNamespace() : ", ns)
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ns,
Labels: map[string]string{
"multicluster-scheduler": "enabled",
},
},
}
// Create the namespace
fmt.Println("Creating namespace...")
if _, err := k.Set.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{}); err != nil {
return errors.New("Error creating namespace: " + err.Error())
}
fmt.Println("Namespace created successfully!")
return nil
}
func (k *KubernetesService) CreateServiceAccount(ctx context.Context, ns string) error {
// Create the ServiceAccount object
serviceAccount := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "sa-" + ns,
Namespace: ns,
},
}
// Create the ServiceAccount in the specified namespace
_, err := k.Set.CoreV1().ServiceAccounts(ns).Create(ctx, serviceAccount, metav1.CreateOptions{})
if err != nil {
return errors.New("Failed to create ServiceAccount: " + err.Error())
}
return nil
}
func (k *KubernetesService) CreateRole(ctx context.Context, ns string, role string, groups [][]string, resources [][]string, verbs [][]string) error {
// Create the Role object
if len(groups) != len(resources) || len(resources) != len(verbs) {
return errors.New("Invalid input: groups, resources, and verbs must have the same length")
}
rules := []rbacv1.PolicyRule{}
for i, group := range groups {
rules = append(rules, rbacv1.PolicyRule{
APIGroups: group,
Resources: resources[i],
Verbs: verbs[i],
})
}
r := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: role,
Namespace: ns,
},
Rules: rules,
}
// Create the Role in the specified namespace
_, err := k.Set.RbacV1().Roles(ns).Create(ctx, r, metav1.CreateOptions{})
if err != nil {
return errors.New("Failed to create Role: " + err.Error())
}
return nil
}
func (k *KubernetesService) CreateRoleBinding(ctx context.Context, ns string, roleBinding string, role string) error {
// Create the RoleBinding object
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleBinding,
Namespace: ns,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: "sa-" + ns,
Namespace: ns,
},
},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: role,
APIGroup: "rbac.authorization.k8s.io",
},
}
// Create the RoleBinding in the specified namespace
_, err := k.Set.RbacV1().RoleBindings(ns).Create(ctx, rb, metav1.CreateOptions{})
if err != nil {
return errors.New("Failed to create RoleBinding: " + err.Error())
}
return nil
}
// ProvisionExecutionNamespace creates the full Argo execution environment for a
// namespace: namespace, service-account, role and role-binding. Idempotent — if
// the namespace already exists the call is a no-op.
func (k *KubernetesService) ProvisionExecutionNamespace(ctx context.Context, ns string) error {
existing, _ := k.GetNamespace(ctx, ns)
if existing != nil {
return nil
}
if err := k.CreateNamespace(ctx, ns); err != nil && !strings.Contains(err.Error(), "already exists") {
return err
}
if err := k.CreateServiceAccount(ctx, ns); err != nil && !strings.Contains(err.Error(), "already exists") {
return err
}
role := "argo-role"
if err := k.CreateRole(ctx, ns, role,
[][]string{{"coordination.k8s.io"}, {""}, {""}},
[][]string{{"leases"}, {"secrets"}, {"pods"}},
[][]string{{"get", "create", "update"}, {"get"}, {"patch"}},
); err != nil {
return err
}
return k.CreateRoleBinding(ctx, ns, "argo-role-binding", role)
}
// TeardownExecutionNamespace deletes the namespace and lets Kubernetes cascade
// the deletion of all contained resources (SA, Role, RoleBinding, pods…).
func (k *KubernetesService) TeardownExecutionNamespace(ctx context.Context, ns string) error {
if err := k.Set.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}); err != nil {
return errors.New("error deleting namespace " + ns + ": " + err.Error())
}
return nil
}
func (k *KubernetesService) DeleteNamespace(ctx context.Context, ns string, f func()) error {
targetGVR := schema.GroupVersionResource{
Group: "multicluster.admiralty.io",
Version: "v1alpha1",
Resource: "targets",
}
// Delete the Target
dyn, err := NewDynamicClient(k.Host, k.CA, k.Cert, k.Data)
if err != nil {
return err
}
err = dyn.Resource(targetGVR).Namespace(ns).Delete(context.TODO(), "target-"+ns, metav1.DeleteOptions{})
if err != nil {
return err
}
err = k.Set.CoreV1().ServiceAccounts(ns).Delete(context.TODO(), "sa-"+ns, metav1.DeleteOptions{})
if err != nil {
return err
}
// Delete the namespace
if err := k.Set.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}); err != nil {
return errors.New("Error deleting namespace: " + err.Error())
}
f()
// monitor.StreamRegistry.Cancel(ns)
fmt.Println("Namespace deleted successfully!")
return nil
}
// Returns the string representing the token generated for the serviceAccount
// in the namespace identified by the value `ns` with the name sa-`ns`, which is valid for
// `duration` seconds
func (k *KubernetesService) GenerateToken(ctx context.Context, ns string, duration int) (string, error) {
// Define TokenRequest (valid for 1 hour)
d := int64(duration)
tokenRequest := &authv1.TokenRequest{
Spec: authv1.TokenRequestSpec{
ExpirationSeconds: &d, // 1 hour validity
},
}
// Generate the token
token, err := k.Set.CoreV1().
ServiceAccounts(ns).
CreateToken(ctx, "sa-"+ns, tokenRequest, metav1.CreateOptions{})
if err != nil {
return "", errors.New("Failed to create token for ServiceAccount: " + err.Error())
}
return token.Status.Token, nil
}
// Needs refactoring :
// - Retrieving the metada (in a method that Unmarshall the part of the json in a metadata object)
func (k *KubernetesService) GetTargets(ctx context.Context) ([]string, error) {
var listTargets []string
resp, err := getCDRapiKube(*k.Set, ctx, "/apis/multicluster.admiralty.io/v1alpha1/targets")
if err != nil {
return nil, err
}
var targetDict map[string]interface{}
err = json.Unmarshal(resp, &targetDict)
if err != nil {
return nil, err
}
data := targetDict["items"].([]interface{})
for _, item := range data {
var metadata metav1.ObjectMeta
item := item.(map[string]interface{})
byteMetada, err := json.Marshal(item["metadata"])
if err != nil {
fmt.Println("Error while Marshalling metadata field")
return nil, err
}
err = json.Unmarshal(byteMetada, &metadata)
if err != nil {
fmt.Println("Error while Unmarshalling metadata field to the library object")
return nil, err
}
listTargets = append(listTargets, metadata.Name)
}
return listTargets, nil
}
// Admiralty Target allows a cluster to deploy pods to remote cluster
//
// The remote cluster must :
//
// - have declared a Source resource
//
// - have declared the same namespace as the one where the pods are created in the local cluster
//
// - have delcared a serviceAccount with sufficient permission to create pods
func (k *KubernetesService) CreateAdmiraltyTarget(context context.Context, executionId string, peerId string) ([]byte, error) {
exists, err := k.GetKubeconfigSecret(context, executionId, peerId)
if err != nil {
fmt.Println("Error verifying kube-secret before creating target")
return nil, err
}
if exists == nil {
fmt.Println("Target needs to be binded to a secret in namespace ", executionId)
return nil, nil // Maybe we could create a wrapper for errors and add more info to have
}
targetName := "target-" + GetConcatenatedName(peerId, executionId)
target := map[string]interface{}{
"apiVersion": "multicluster.admiralty.io/v1alpha1",
"kind": "Target",
"metadata": map[string]interface{}{
"name": targetName,
"namespace": executionId,
// "labels": map[string]interface{}{
// "peer": peerId,
// },
},
"spec": map[string]interface{}{
"kubeconfigSecret": map[string]string{
"name": "kube-secret-" + GetConcatenatedName(peerId, executionId),
},
},
}
res, err := dynamicClientApply(k.Host, k.CA, k.Cert, k.Data, executionId, targetName, gvrTargets, context, target)
if err != nil {
return nil, errors.New("Error when trying to apply Target definition :" + err.Error())
}
return res, nil
}
// Admiralty Source allows a cluster to receive pods from a remote cluster
//
// The source must be associated to a serviceAccount, which will execute the pods locally.
// This serviceAccount must have sufficient permission to create and patch pods
//
// This method is temporary to implement the use of Admiralty, but must be edited
// to rather contact the oc-datacenter from the remote cluster to create the source
// locally and retrieve the token for the serviceAccount
func (k *KubernetesService) CreateAdmiraltySource(context context.Context, executionId string) ([]byte, error) {
source := map[string]interface{}{
"apiVersion": "multicluster.admiralty.io/v1alpha1",
"kind": "Source",
"metadata": map[string]interface{}{
"name": "source-" + executionId,
"namespace": executionId,
},
"spec": map[string]interface{}{
"serviceAccountName": "sa-" + executionId,
},
}
res, err := dynamicClientApply(k.Host, k.CA, k.Cert, k.Data, executionId, "source-"+executionId, gvrSources, context, source)
if err != nil {
return nil, errors.New("Error when trying to apply Source definition :" + err.Error())
}
return res, nil
}
// Create a secret from a kubeconfing. Use it to create the secret binded to an Admiralty
// target, which must contain the serviceAccount's token value
func (k *KubernetesService) CreateKubeconfigSecret(context context.Context, kubeconfig string, executionId string, peerId string) ([]byte, error) {
config, err := base64.StdEncoding.DecodeString(kubeconfig)
// config, err := base64.RawStdEncoding.DecodeString(kubeconfig)
if err != nil {
fmt.Println("Error while encoding kubeconfig")
return nil, err
}
secretApplyConfig := apply.Secret("kube-secret-"+GetConcatenatedName(peerId, executionId),
executionId).
WithData(map[string][]byte{
"config": config,
},
)
resp, err := k.Set.CoreV1().
Secrets(executionId).
Apply(context,
secretApplyConfig,
metav1.ApplyOptions{
FieldManager: "admiralty-manager",
})
if err != nil {
fmt.Println("Error while trying to contact API to get secret kube-secret-" + executionId)
return nil, err
}
data, err := json.Marshal(resp)
if err != nil {
fmt.Println("Couldn't marshal resp from : ", data)
return nil, err
}
return data, nil
}
func (k *KubernetesService) GetKubeconfigSecret(context context.Context, executionId string, peerId string) ([]byte, error) {
resp, err := k.Set.CoreV1().
Secrets(executionId).
Get(context, "kube-secret-"+GetConcatenatedName(peerId, executionId), metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
fmt.Println("kube-secret not found for execution", executionId)
return nil, nil
}
fmt.Println("Error while trying to contact API to get secret kube-secret-" + executionId)
return nil, err
}
data, err := json.Marshal(resp)
if err != nil {
fmt.Println("Couldn't marshal resp from : ", data)
return nil, err
}
return data, nil
}
func (k *KubernetesService) DeleteKubeConfigSecret(executionID string) ([]byte, error) {
return []byte{}, nil
}
func (k *KubernetesService) GetNamespace(context context.Context, executionID string) (*v1.Namespace, error) {
resp, err := k.Set.CoreV1().Namespaces().Get(context, executionID, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
logger := logs.GetLogger()
logger.Error().Msg("An error occured when trying to get namespace " + executionID + " : " + err.Error())
return nil, err
}
return resp, nil
}
func getCDRapiKube(client kubernetes.Clientset, ctx context.Context, path string) ([]byte, error) {
resp, err := client.RESTClient().Get().
AbsPath(path).
DoRaw(ctx) // from https://stackoverflow.com/questions/60764908/how-to-access-kubernetes-crd-using-client-go
if err != nil {
fmt.Println("Error from k8s API when getting "+path+" : ", err)
return nil, err
}
return resp, nil
}
func dynamicClientApply(host string, ca string, cert string, data string, executionId string, resourceName string, resourceDefinition schema.GroupVersionResource, ctx context.Context, object map[string]interface{}) ([]byte, error) {
cli, err := NewDynamicClient(host, ca, cert, data)
if err != nil {
return nil, errors.New("Could not retrieve dynamic client when creating Admiralty Source : " + err.Error())
}
res, err := cli.Resource(resourceDefinition).
Namespace(executionId).
Apply(ctx,
resourceName,
&unstructured.Unstructured{Object: object},
metav1.ApplyOptions{
FieldManager: "kubectl-client-side-apply",
},
)
if err != nil {
fmt.Println("Error from k8s API when applying "+fmt.Sprintf("%v", object)+" to "+gvrSources.String()+" : ", err)
return nil, err
}
// We can add more info to the log with the content of resp if not nil
resByte, err := json.Marshal(res)
if err != nil {
fmt.Println("Error trying to create a Source on remote cluster : ", err, " : ", res)
return nil, err
}
return resByte, nil
}
func (k *KubernetesService) CheckHealth() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Check API server connectivity
_, err := k.Set.ServerVersion()
if err != nil {
return fmt.Errorf("API server unreachable: %v", err)
}
// Check nodes status
nodes, err := k.Set.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list nodes: %v", err)
}
for _, node := range nodes.Items {
for _, condition := range node.Status.Conditions {
if condition.Type == "Ready" && condition.Status != "True" {
return fmt.Errorf("node %s not ready", node.Name)
}
}
}
// Optional: Check if all pods in kube-system are running
pods, err := k.Set.CoreV1().Pods("kube-system").List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list pods: %v", err)
}
for _, pod := range pods.Items {
if pod.Status.Phase != "Running" && pod.Status.Phase != "Succeeded" {
return fmt.Errorf("pod %s in namespace kube-system is %s", pod.Name, pod.Status.Phase)
}
}
return nil
}
// Returns the Kubernetes' Node object corresponding to the executionID if it exists on this host
//
// The node is created when an admiralty Target (on host) can connect to an admiralty Source (on remote)
func (k *KubernetesService) GetOneNode(context context.Context, executionID string, peerId string) (*v1.Node, error) {
concatenatedName := GetConcatenatedName(peerId, executionID)
res, err := k.Set.CoreV1().
Nodes().
List(
context,
metav1.ListOptions{},
)
if err != nil {
fmt.Println("Error getting the list of nodes from k8s API")
return nil, err
}
for _, node := range res.Items {
if isNode := strings.Contains(node.Name, "admiralty-"+executionID+"-target-"+concatenatedName+"-"); isNode {
return &node, nil
}
}
return nil, nil
}
func (k *KubernetesService) CreateSecret(context context.Context, minioId string, executionID string, access string, secret string) error {
data := map[string][]byte{
"access-key": []byte(access),
"secret-key": []byte(secret),
}
s := v1.Secret{
Type: v1.SecretTypeOpaque,
Data: data,
ObjectMeta: metav1.ObjectMeta{
Name: minioId + "-secret-s3",
},
}
_, err := k.Set.CoreV1().Secrets(executionID).Create(context, &s, metav1.CreateOptions{})
if err != nil {
logger := logs.GetLogger()
logger.Error().Msg("An error happened when creating the secret holding minio credentials in namespace " + executionID + " : " + err.Error())
return err
}
return nil
}
// ============== ADMIRALTY ==============
// Returns a concatenation of the peerId and namespace in order for
// kubernetes ressources to have a unique name, under 63 characters
// and yet identify which peer they are created for
func GetConcatenatedName(peerId string, namespace string) string {
s := strings.Split(namespace, "-")[:2]
n := s[0] + "-" + s[1]
return peerId + "-" + n
}

View File

@@ -17,33 +17,49 @@ type NATSResponse struct {
FromApp string `json:"from_app"` FromApp string `json:"from_app"`
Datatype DataType `json:"datatype"` Datatype DataType `json:"datatype"`
User string `json:"user"` User string `json:"user"`
Groups []string `json:"groups"`
Method int `json:"method"` Method int `json:"method"`
SearchAttr string `json:"search_attr"`
Payload []byte `json:"payload"` Payload []byte `json:"payload"`
} }
// NATS Method Enum defines the different methods that can be used to interact with the NATS server // NATS Method Enum defines the different methods that can be used to interact with the NATS server
type NATSMethod int type NATSMethod int
var meths = []string{"remove execution", "create execution", "discovery", var meths = []string{"remove execution", "create execution", "planner execution", "discovery",
"workflow event", "remove peer", "create peer", "create resource", "remove resource", "verify_resource", "workflow event", "argo kube event", "create resource", "remove resource",
"propalgation event", "catalogsearch event", "propalgation event", "search event", "confirm event",
"considers event", "admiralty config event", "minio config event",
"workflow started event", "workflow step done event", "workflow done event",
} }
const ( const (
REMOVE_EXECUTION NATSMethod = iota REMOVE_EXECUTION NATSMethod = iota
CREATE_EXECTUTION CREATE_EXECUTION
PLANNER_EXECUTION
DISCOVERY DISCOVERY
WORKFLOW_EVENT
REMOVE_PEER WORKFLOW_EVENT
CREATE_PEER ARGO_KUBE_EVENT
CREATE_RESOURCE CREATE_RESOURCE
REMOVE_RESOURCE REMOVE_RESOURCE
VERIFY_RESOURCE
PROPALGATION_EVENT PROPALGATION_EVENT
CATALOG_SEARCH_EVENT SEARCH_EVENT
CONFIRM_EVENT
CONSIDERS_EVENT
ADMIRALTY_CONFIG_EVENT
MINIO_CONFIG_EVENT
// Workflow lifecycle events emitted by oc-monitord.
// oc-scheduler listens to STARTED and DONE to maintain WorkflowExecution state.
// oc-datacenter listens to STEP_DONE and DONE to close bookings and tear down infra.
WORKFLOW_STARTED_EVENT
WORKFLOW_STEP_DONE_EVENT
WORKFLOW_DONE_EVENT
) )
func (n NATSMethod) String() string { func (n NATSMethod) String() string {
@@ -52,8 +68,10 @@ func (n NATSMethod) String() string {
// NameToMethod returns the NATSMethod enum value from a string // NameToMethod returns the NATSMethod enum value from a string
func NameToMethod(name string) NATSMethod { func NameToMethod(name string) NATSMethod {
for _, v := range [...]NATSMethod{REMOVE_EXECUTION, CREATE_EXECTUTION, DISCOVERY, WORKFLOW_EVENT, for _, v := range [...]NATSMethod{REMOVE_EXECUTION, CREATE_EXECUTION, PLANNER_EXECUTION, DISCOVERY, WORKFLOW_EVENT, ARGO_KUBE_EVENT,
REMOVE_PEER, CREATE_PEER, CREATE_RESOURCE, REMOVE_RESOURCE, VERIFY_RESOURCE, PROPALGATION_EVENT, CATALOG_SEARCH_EVENT} { CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT, CONFIRM_EVENT,
CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT,
WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT} {
if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) { if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) {
return v return v
} }

View File

@@ -54,8 +54,6 @@ type HTTPCallerITF interface {
CallDelete(url string, subpath string) ([]byte, error) CallDelete(url string, subpath string) ([]byte, error)
} }
var HTTPCallerInstance = &HTTPCaller{} // Singleton instance of the HTTPCaller
type HTTPCaller struct { type HTTPCaller struct {
URLS map[DataType]map[METHOD]string // Map of the different methods and their urls URLS map[DataType]map[METHOD]string // Map of the different methods and their urls
Disabled bool // Disabled flag Disabled bool // Disabled flag
@@ -115,7 +113,7 @@ func (caller *HTTPCaller) CallDelete(url string, subpath string) ([]byte, error)
} }
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil || req == nil || req.Body == nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()

View File

@@ -0,0 +1,33 @@
package tools
import "time"
// StepMetric carries the outcome of one Argo step node as observed by oc-monitord.
// Embedded in WorkflowLifecycleEvent.Steps for the WORKFLOW_DONE_EVENT recap.
type StepMetric struct {
BookingID string `json:"booking_id"`
State int `json:"state"`
RealStart *time.Time `json:"real_start,omitempty"`
RealEnd *time.Time `json:"real_end,omitempty"`
}
// WorkflowLifecycleEvent is the NATS payload emitted by oc-monitord on
// WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, and WORKFLOW_DONE_EVENT.
//
// - ExecutionID : WorkflowExecution UUID (used by oc-scheduler to update state)
// - ExecutionsID : run-group ID shared by all bookings of the same run
// - BookingID : non-empty only for WORKFLOW_STEP_DONE_EVENT
// - State : target state (enum index: SUCCESS=3, FAILURE=4, STARTED=2, …)
// - RealStart : actual start timestamp recorded by Argo (nil if unknown)
// - RealEnd : actual end timestamp recorded by Argo (nil for STARTED events)
// - Steps : non-nil only for WORKFLOW_DONE_EVENT — full recap of every step
// so oc-scheduler and oc-catalog can catch up if they missed STEP_DONE events
type WorkflowLifecycleEvent struct {
ExecutionID string `json:"execution_id"`
ExecutionsID string `json:"executions_id"`
BookingID string `json:"booking_id,omitempty"`
State int `json:"state"`
RealStart *time.Time `json:"real_start,omitempty"`
RealEnd *time.Time `json:"real_end,omitempty"`
Steps []StepMetric `json:"steps,omitempty"`
}