168 Commits

Author SHA1 Message Date
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
mr
3bcf0da56a inspect oc-auth 2026-02-10 09:12:02 +01:00
mr
b9c9b66780 Sign Resource 2026-02-09 12:37:03 +01:00
mr
b767afb301 signature to byte 2026-02-09 10:55:36 +01:00
mr
bafeee0d05 GetSignature() string 2026-02-09 10:50:10 +01:00
mr
c2aa2fedaa Verify Resource integrity base struct 2026-02-09 10:03:40 +01:00
mr
b9e7ce20b6 store 2026-02-05 15:30:23 +01:00
mr
543d28e5d2 relation duplicate key 2026-02-05 15:17:47 +01:00
mr
04d25c7ef1 as _id 2026-02-05 15:14:35 +01:00
mr
c8e98bea61 id as _id 2026-02-05 15:11:03 +01:00
mr
2cf960e4f7 mongo adjust 2026-02-05 15:06:45 +01:00
mr
e32ba1e37a test 2026-02-05 14:54:41 +01:00
mr
5798e16263 StoreOne 2026-02-05 14:25:26 +01:00
mr
342451db25 publishing log 2026-02-05 14:16:30 +01:00
mr
425cd2a9ba NATS_SERVER 2026-02-05 14:10:48 +01:00
mr
9e30133628 beego.Run() 2026-02-05 14:04:17 +01:00
mr
d9f646aac2 security inject appName 2026-02-04 09:38:45 +01:00
mr
ef916fe2d9 InitAPI 2026-02-03 16:05:31 +01:00
mr
4258f6b580 missing import 2026-02-03 16:01:23 +01:00
mr
06ad584b6a missing import 2026-02-03 16:01:15 +01:00
mr
c650f08b8e add setup 2026-02-03 15:57:12 +01:00
mr
c90b55f312 Beego set up 2026-02-03 15:56:39 +01:00
mr
4f28b9b589 loop on discovery 2026-02-03 09:37:53 +01:00
mr
30e6c9a618 bug on enums 2026-02-03 08:44:47 +01:00
mr
186ba3e689 address 2026-01-29 13:20:33 +01:00
mr
c1519f6b26 update peer model 2026-01-29 13:12:15 +01:00
mr
97cf629e27 moove user from propalgation to Nats Response 2026-01-28 17:27:02 +01:00
mr
c0d89ea9e1 utils to transform byte to resource 2026-01-28 17:04:40 +01:00
mr
7ccd7fe16b CATALOG_SEARCH_EVENT 2026-01-28 17:00:59 +01:00
mr
886f9d15ba include datatype to propalgationMessage 2026-01-28 16:51:27 +01:00
mr
d26789d64e Propalgation Mess 2026-01-28 16:44:47 +01:00
mr
7911cf29de Update NATS channel allowed 2026-01-28 16:29:19 +01:00
mr
743f4a6ff7 share pubsub action 2026-01-28 16:22:42 +01:00
mr
d2c5d20318 Lightest AdminRequest 2026-01-28 15:52:48 +01:00
mr
e3fe49c239 Structured NATS 2026-01-28 15:49:21 +01:00
mr
1c9d7b63c0 NATSResponse 2026-01-28 15:08:07 +01:00
mr
d098d253d8 NATSMethod 2026-01-28 15:06:32 +01:00
mr
ecb734187a NATSResponse 2026-01-28 15:05:48 +01:00
mr
3c052bf165 Peer Accessor modify 2026-01-27 15:37:28 +01:00
mr
9af8d15672 DISCOVERY 2026-01-27 12:49:51 +01:00
mr
4d57767005 better implem obj hierarchy 2026-01-27 11:14:28 +01:00
mr
9b4f9e420a Mongo Improvment for filtering 2026-01-27 10:59:38 +01:00
mr
d772a703da adjust useless line 2026-01-27 09:50:07 +01:00
mr
c69069449f chan name fail 2026-01-27 09:37:51 +01:00
mr
643beacd4b Configuration NATS 2026-01-27 09:35:47 +01:00
mr
802786daa7 Peer Manipulation 2026-01-26 16:29:09 +01:00
mr
b35d4e2b36 url 2026-01-26 14:34:48 +01:00
mr
1f93493965 relation 2026-01-26 14:32:56 +01:00
mr
055e6c70cd filter peer 2026-01-26 13:00:55 +01:00
mr
85a8857938 oclib -> Generate Peer ID 2026-01-26 12:34:04 +01:00
mr
bc94f2b188 Adjust Mongo 2026-01-26 10:36:15 +01:00
mr
6b12aa1713 silence InitNative 2026-01-23 10:03:29 +01:00
mr
f3d7c65b18 partner 2026-01-23 07:51:15 +01:00
mr
d06c9e9337 GetRelationPath 2026-01-23 07:48:04 +01:00
mr
0308b4ea10 add func 2026-01-23 07:45:36 +01:00
mr
b71b1e741d adjust peer element for partnership 2026-01-23 07:38:20 +01:00
mr
8f5f3e331d PENDING status 2026-01-22 16:11:54 +01:00
mr
2d1d76767c keep only verify 2026-01-22 15:58:38 +01:00
mr
00bcca379f Peer Evolve 2026-01-22 15:55:27 +01:00
mr
b987286759 NATS URL 2026-01-22 14:18:02 +01:00
mr
c72954d2f7 no needed group 2026-01-22 13:58:21 +01:00
mr
bcfd43e140 group.Group 2026-01-22 13:45:19 +01:00
mr
a4512e4da6 add Groups 2026-01-22 13:38:55 +01:00
mr
1c3b9218f7 correction on bookig flow 2026-01-15 13:27:57 +01:00
mr
7c5d5c491f Better peer filtering 2026-01-15 12:26:56 +01:00
mr
76eb167c5b peer improvment 2026-01-15 12:15:04 +01:00
mr
fa5b754333 add Selected in Workflow Exec 2026-01-14 13:57:49 +01:00
mr
0e378dc19c add Selected in Workflow Exec 2026-01-14 13:55:32 +01:00
mr
5cdfc28d2f delete scheduler 2026-01-13 16:53:25 +01:00
mr
6d745fe922 add event base intelligency 2026-01-13 16:04:31 +01:00
mr
c35b06e0bc add NATS EVENT 2026-01-12 15:40:37 +01:00
mr
be770ec763 multipart file over os.file 2026-01-12 14:26:29 +01:00
mr
27f295f17e Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib
Merged Main OCLIB
2026-01-12 12:00:32 +01:00
mr
33b8d2799a update import plantUML 2026-01-12 11:59:05 +01:00
188b758f7a Ajouter .gitattributes 2025-11-01 16:39:25 +01:00
pb
f4b0cf5683 changed the value used as the key in the related map, using the 'node id' was erasing some relations when a a processing is linked to two storages 2025-08-08 16:15:53 +02:00
pb
e7a71188a3 changed the value used as the key in the related map, using the 'node id' was erasing some relations when a a processing is linked to two storages 2025-08-08 16:05:36 +02:00
pb
40a61387b9 added the UUID of the peer when an error appears 2025-08-05 13:39:21 +02:00
pb
cc939451fd added the UUID of the peer when an error appears 2025-08-05 13:25:47 +02:00
pb
76e9b2562e added the enum to reach the /minio/secet route 2025-08-05 11:56:27 +02:00
pb
cc3091d401 added the function to load one ressource for each ressource type 2025-07-31 15:53:05 +02:00
pb
3ddbf1a967 added comments 2025-07-30 18:28:19 +02:00
pb
be2a1cc114 corrction of the non initialized map 2025-07-30 18:21:09 +02:00
pb
a093369dc5 corrction of the non initialized map 2025-07-30 18:15:55 +02:00
pb
76d83878eb added a method to workflow which allows to retrieve items by resource type and resource id 2025-07-30 18:01:23 +02:00
pb
e735f78e58 added a new method to create kubernetes names with the same naming 2025-07-15 14:58:19 +02:00
pb
98a2359c9d added a tweak to PeerItemOrder GetPrice in order to not crash on nil Purchase 2025-07-10 11:47:54 +02:00
pb
83e590d4e1 added error handling 2025-07-09 17:42:37 +02:00
pb
4e3ff9aa08 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib 2025-07-09 16:54:37 +02:00
pb
424d523c5e added the bson tag for DestPeerId which was causing error when deserializing from mongo results 2025-07-09 16:50:41 +02:00
mr
346275e12c test 2025-07-08 13:59:55 +02:00
mr
6ab774cc43 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main
test
2025-07-08 13:42:28 +02:00
mr
2748b59221 test booking 2025-07-08 13:42:13 +02:00
pb
34f01e9740 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib 2025-07-08 12:02:34 +02:00
pb
dcdc6ff1d9 added more info in GenericStoreOne error generated by validate() 2025-07-08 12:02:18 +02:00
pb
365b924e4b Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib 2025-07-07 16:30:58 +02:00
pb
e7e56d1859 commented the PricingProfile check for IsBooked 2025-07-07 16:30:29 +02:00
mr
443546027b Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main 2025-07-04 10:44:59 +02:00
mr
1c4f3f756f test 2025-07-04 10:44:14 +02:00
pb
3971d5ca5d added the value to reach the minio route on datacenter 2025-06-30 14:06:03 +02:00
pb
e95d1aa53b implemented StoreOne for orders 2025-06-27 17:33:39 +02:00
pb
1ab2bd2153 commented the condition that prevents the booking if pricing is not selected to allow dev while the feature is not implemented 2025-06-27 17:10:32 +02:00
pb
d35ad440fa removed the 'validate:required' from PricedItem in Booking because we don't have implemented the error handling nor the feature yet 2025-06-27 16:21:54 +02:00
mr
d58dc56024 corrected live accessor 2025-06-26 16:20:41 +02:00
mr
34b7cdcf06 corrected live accessor 2025-06-26 15:59:21 +02:00
mr
af0d7807bc corrected live accessor 2025-06-26 15:57:49 +02:00
mr
e600fedcab access to live storage 2025-06-24 12:22:27 +02:00
mr
147c7bc3a1 access to live storage 2025-06-24 11:58:52 +02:00
mr
3fdf5c3ebf Live Structure 2025-06-24 11:32:07 +02:00
mr
cd177bd779 Live Structure 2025-06-24 11:29:04 +02:00
mr
2c8dcbe93d wait for NATS 2025-06-24 08:49:53 +02:00
mr
e84d262f38 add sets up 2025-06-20 12:11:13 +02:00
mr
29b192211d add sets up 2025-06-20 12:10:36 +02:00
mr
583ca2fbac add sets up 2025-06-20 10:50:01 +02:00
mr
82d25b0bee add sets up 2025-06-20 10:48:08 +02:00
mr
181b3249b8 add sets up 2025-06-20 10:47:33 +02:00
mr
8b38249df7 add sets up 2025-06-20 09:27:55 +02:00
mr
01af8237db add sets up 2025-06-20 09:22:09 +02:00
mr
2f4884c655 add sets up 2025-06-20 09:07:53 +02:00
mr
c9ee2a1d24 add sets up 2025-06-20 09:05:44 +02:00
mr
8d5ba6a5e4 add sets up 2025-06-20 08:34:33 +02:00
mr
d3cfe019e3 add sets up 2025-06-20 08:10:52 +02:00
mr
4c2ecd3f41 add sets up 2025-06-20 07:53:32 +02:00
mr
d8ccdec501 add sets up 2025-06-20 07:51:32 +02:00
mr
938f9f1326 set up 2025-06-19 08:11:11 +02:00
mr
29bc21735d setup 2025-06-18 07:58:40 +02:00
mr
ec7a7e4746 draft of compute units catalog 2025-06-17 16:42:21 +02:00
mr
0b0952b28c draft of compute units catalog 2025-06-17 16:14:44 +02:00
mr
9e52663261 draft of compute units catalog 2025-06-17 15:35:02 +02:00
mr
8f2adb76e4 add draft compute units 2025-06-17 15:06:33 +02:00
mr
0d6c329477 draft of compute units catalog 2025-06-17 14:57:36 +02:00
mr
1c751f7253 add draft compute units 2025-06-17 14:51:41 +02:00
mr
2264d22c69 draft of compute units catalog 2025-06-17 14:21:37 +02:00
mr
9fe72ea96e draft tests 2025-06-16 13:48:32 +02:00
mr
48299810e0 draft test 2025-06-16 11:24:39 +02:00
mr
2a0ab8e549 access to workflow IsDeps func 2025-06-12 10:47:38 +02:00
mr
23a9d648d2 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main
merge oclib
2025-06-12 10:42:38 +02:00
mr
a3029fa3f9 refactor addition in oclib for better use 2025-06-12 10:42:05 +02:00
pb
387785b40c reimported logs without import cycle 2025-06-04 10:33:00 +02:00
pb
03dea55131 added another error log when the status is dead 2025-06-03 10:00:47 +02:00
pb
7b8aa989f6 added a log when an API is not reachable 2025-06-02 18:22:08 +02:00
pb
6ab6383144 corrected an use of the original http caller instead of the deep copy 2025-05-27 18:07:00 +02:00
pb
690d60f9d6 corrected the getBooking function parameters 2025-05-27 16:13:46 +02:00
pb
da0de80afd Booking check and booking post have been transformed in goroutine to improve performance when booking several execution with cron expressions 2025-05-27 15:38:24 +02:00
90 changed files with 7038 additions and 1678 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Force Go as the main language
*.go linguist-detectable=true
* linguist-language=Go

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

@@ -9,14 +9,24 @@ import "sync"
// ===================================================
type Config struct {
NATSUrl string
MongoUrl string
MongoDatabase string
Host string
Port string
LokiUrl string
LogLevel string
Whitelist bool
APIPort int
NATSUrl string
MongoUrl string
MongoDatabase string
Host string
Port string
LokiUrl string
LogLevel string
Whitelist bool
PrivateKeyPath string
PublicKeyPath string
InternalCatalogAPI string
InternalSharedAPI string
InternalWorkflowAPI string
InternalWorkspaceAPI string
InternalPeerAPI string
InternalDatacenterAPI string
}
func (c Config) GetUrl() string {
@@ -37,12 +47,24 @@ func GetConfig() *Config {
return instance
}
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel 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) *Config {
GetConfig().MongoUrl = mongoUrl
GetConfig().MongoDatabase = database
GetConfig().NATSUrl = natsUrl
GetConfig().LokiUrl = lokiUrl
GetConfig().LogLevel = logLevel
GetConfig().Whitelist = true
GetConfig().APIPort = port
GetConfig().PrivateKeyPath = pkPath
GetConfig().PublicKeyPath = ppPath
GetConfig().InternalCatalogAPI = internalCatalogAPI
GetConfig().InternalSharedAPI = internalSharedAPI
GetConfig().InternalWorkflowAPI = internalWorkflowAPI
GetConfig().InternalWorkspaceAPI = internalWorkspaceAPI
GetConfig().InternalPeerAPI = internalPeerAPI
GetConfig().InternalDatacenterAPI = internalDatacenterAPI
return GetConfig()
}

View File

@@ -23,12 +23,11 @@ import (
* The configuration loader will give priority to the local file over the default file
*/
func GetConfLoader() *onion.Onion {
func GetConfLoader(appName string) *onion.Onion {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
AppName := GetAppName()
EnvPrefix := "OC_"
defaultConfigFile := "/etc/oc/" + AppName[3:] + ".json"
localConfigFile := "./" + AppName[3:] + ".json"
defaultConfigFile := "/etc/oc/" + appName[3:] + ".json"
localConfigFile := "./" + appName[3:] + ".json"
var configFile string
var o *onion.Onion
l3 := GetEnvVarLayer(EnvPrefix)

View File

@@ -2,6 +2,7 @@ package dbs
import (
"fmt"
"runtime/debug"
"strings"
"go.mongodb.org/mongo-driver/bson"
@@ -19,6 +20,8 @@ const (
GT
EQUAL
NOT
ELEMMATCH
OR
)
var str = [...]string{
@@ -31,113 +34,43 @@ var str = [...]string{
"gt",
"equal",
"not",
"elemMatch",
}
func (m Operator) String() string {
return str[m]
}
func (m Operator) ToMongoEOperator(k string, value interface{}) bson.E {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
}
}()
defaultValue := bson.E{Key: k, Value: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
switch m {
case LIKE:
return bson.E{Key: k, Value: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
case EXISTS:
return bson.E{Key: k, Value: bson.M{"$exists": ToValueOperator(StringToOperator(m.String()), value)}}
case IN:
return bson.E{Key: k, Value: bson.M{"$in": ToValueOperator(StringToOperator(m.String()), value)}}
case GTE:
return bson.E{Key: k, Value: bson.M{"$gte": ToValueOperator(StringToOperator(m.String()), value)}}
case GT:
return bson.E{Key: k, Value: bson.M{"$gt": ToValueOperator(StringToOperator(m.String()), value)}}
case LTE:
return bson.E{Key: k, Value: bson.M{"$lte": ToValueOperator(StringToOperator(m.String()), value)}}
case LT:
return bson.E{Key: k, Value: bson.M{"$lt": ToValueOperator(StringToOperator(m.String()), value)}}
case EQUAL:
return bson.E{Key: k, Value: value}
case NOT:
v := value.(Filters)
orList := bson.A{}
andList := bson.A{}
f := bson.D{}
for k, filter := range v.Or {
for _, ff := range filter {
orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range v.And {
for _, ff := range filter {
andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
return bson.E{Key: "$not", Value: f}
default:
return defaultValue
}
}
func (m Operator) ToMongoOperator(k string, value interface{}) bson.M {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
fmt.Println("Recovered. Error:\n", r, debug.Stack())
}
}()
defaultValue := bson.M{k: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value)}}
switch m {
case LIKE:
return bson.M{k: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value)}}
case EXISTS:
return bson.M{k: bson.M{"$exists": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value)}}
case IN:
return bson.M{k: bson.M{"$in": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value)}}
case GTE:
return bson.M{k: bson.M{"$gte": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value)}}
case GT:
return bson.M{k: bson.M{"$gt": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value)}}
case LTE:
return bson.M{k: bson.M{"$lte": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value)}}
case LT:
return bson.M{k: bson.M{"$lt": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value)}}
case ELEMMATCH:
return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value)}}
case EQUAL:
return bson.M{k: value}
case NOT:
v := value.(Filters)
orList := bson.A{}
andList := bson.A{}
f := bson.D{}
for k, filter := range v.Or {
for _, ff := range filter {
orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range v.And {
for _, ff := range filter {
andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
return bson.M{"$not": f}
return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value)}
case OR:
return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value)}
default:
return defaultValue
}
@@ -152,13 +85,46 @@ func StringToOperator(s string) Operator {
return LIKE
}
func ToValueOperator(operator Operator, value interface{}) interface{} {
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
value = ""
func GetBson(filters *Filters) bson.D {
f := bson.D{}
orList := bson.A{}
andList := bson.A{}
if filters != nil {
for k, filter := range filters.Or {
for _, ff := range filter {
orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range filters.And {
for _, ff := range filter {
andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
}
if operator == LIKE {
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
return f
}
func (m Operator) ToValueOperator(operator Operator, value interface{}) interface{} {
switch value := value.(type) {
case *Filters:
return GetBson(value)
default:
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
value = ""
}
if operator == LIKE {
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
}
}
return value
}

View File

@@ -247,10 +247,7 @@ func (m *MongoDB) StoreOne(obj interface{}, id string, collection_name string) (
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return "", 503, err
}
var doc map[string]interface{}
b, _ := bson.Marshal(obj)
bson.Unmarshal(b, &doc)
doc["_id"] = id
doc := map[string]interface{}{"_id": id}
targetDBCollection := CollectionMap[collection_name]
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel()
@@ -261,7 +258,7 @@ func (m *MongoDB) StoreOne(obj interface{}, id string, collection_name string) (
return "", 409, err
}
return id, 200, nil
return m.UpdateOne(obj, id, collection_name)
}
func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResult, int, error) {
@@ -289,29 +286,8 @@ func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.C
opts := options.Find()
opts.SetLimit(1000)
targetDBCollection := CollectionMap[collection_name]
orList := bson.A{}
andList := bson.A{}
f := bson.D{}
if filters != nil {
for k, filter := range filters.Or {
for _, ff := range filter {
orList = append(orList, dbs.StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range filters.And {
for _, ff := range filter {
andList = append(andList, dbs.StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
}
f := dbs.GetBson(filters)
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()

View File

@@ -194,7 +194,7 @@ AccessPricingProfile ^-- ProcessingResourcePricingProfile
ExploitPricingProfile ^-- ComputeResourcePricingProfile
ExploitPricingProfile ^-- StorageResourcePricingProfile
interface PricingProfileITF {
GetPrice(quantity float64, val float64, start date, end date, request) float64
GetPriceHT(quantity float64, val float64, start date, end date, request) float64
IsPurchased() bool
}
class AccessPricingProfile {
@@ -319,7 +319,7 @@ Workflow "1 " --* "many " ExploitResourceSet
class Workflow {}
interface PricedItemITF {
getPrice(request) float64, error
GetPriceHT(request) float64, error
}
@enduml

View File

@@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"net/http"
"os"
"slices"
"strings"
"runtime/debug"
@@ -15,11 +17,14 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
"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/peer"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
@@ -27,7 +32,10 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
"github.com/beego/beego/v2/server/web/filter/cors"
"github.com/google/uuid"
"github.com/goraz/onion"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/rs/zerolog"
)
@@ -51,11 +59,43 @@ const (
RULE = tools.RULE
BOOKING = tools.BOOKING
ORDER = tools.ORDER
LIVE_DATACENTER = tools.LIVE_DATACENTER
LIVE_STORAGE = tools.LIVE_STORAGE
PURCHASE_RESOURCE = tools.PURCHASE_RESOURCE
NATIVE_TOOL = tools.NATIVE_TOOL
)
func GetMySelf() (*peer.Peer, error) {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
return pp.(*peer.Peer), err
}
func IsMySelf(peerID string) (bool, string) {
return utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
}
func GenerateNodeID() (string, error) {
folderStatic := "/var/lib/opencloud-node"
if _, err := os.Stat(folderStatic); err == nil {
os.MkdirAll(folderStatic, 0644)
}
folderStatic += "/node_id"
if _, err := os.Stat(folderStatic); os.IsNotExist(err) {
hostname, err := os.Hostname()
if err != nil {
return "", err
}
id := uuid.NewSHA1(uuid.NameSpaceOID, []byte("oc-"+hostname))
err = os.WriteFile(folderStatic, []byte(id.String()), 0644)
return id.String(), err
}
data, err := os.ReadFile(folderStatic)
return string(data), err
}
// will turn into standards api hostnames
func (d LibDataEnum) API() string {
return tools.DefaultAPI[d]
return tools.Str[d]
}
// will turn into standards name
@@ -97,12 +137,13 @@ type LibData struct {
}
func InitDaemon(appName string) {
beego.BConfig.AppName = appName
config.SetAppName(appName) // set the app name to the logger to define the main log chan
// create a temporary console logger for init
logs.SetLogger(logs.CreateLogger("main"))
// Load the right config file
o := GetConfLoader()
o := GetConfLoader(appName)
// resources.InitNative()
// feed the library with the loaded config
SetConfig(
o.GetStringDefault("MONGO_URL", "mongodb://127.0.0.1:27017"),
@@ -110,6 +151,16 @@ func InitDaemon(appName string) {
o.GetStringDefault("NATS_URL", "nats://localhost:4222"),
o.GetStringDefault("LOKI_URL", ""),
o.GetStringDefault("LOG_LEVEL", "info"),
o.GetIntDefault("API_PORT", 8080),
o.GetStringDefault("PUBLIC_KEY_PATH", "./pem/public.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"),
)
// Beego init
beego.BConfig.AppName = appName
@@ -161,8 +212,19 @@ func ExtractTokenInfo(request http.Request) (string, string, []string) {
return "", "", []string{}
}
func Init(appName string) {
func InitAPI(appName string) {
InitDaemon(appName)
beego.BConfig.Listen.HTTPPort = config.GetConfig().APIPort
beego.BConfig.WebConfig.DirectoryIndex = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
c := cors.Allow(&cors.Options{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
ExposeHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
})
beego.InsertFilter("*", beego.BeforeRouter, c)
api := &tools.API{}
api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo())
}
@@ -186,8 +248,10 @@ func GetLogger() zerolog.Logger {
* @param logLevel string
* @return *Config
*/
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *config.Config {
cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel)
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string,
port int, pkpath string, pppath string,
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI, internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI string) *config.Config {
cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath, internalCatalogAPI, internalSharedAPI, internalWorkflowAPI, internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI)
defer func() {
if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
@@ -221,72 +285,28 @@ func GetConfig() *config.Config {
* The configuration loader will give priority to the local file over the default file
*/
func GetConfLoader() *onion.Onion {
return config.GetConfLoader()
func GetConfLoader(appName string) *onion.Onion {
return config.GetConfLoader(appName)
}
type Request struct {
collection LibDataEnum
user string
peerID string
groups []string
caller *tools.HTTPCaller
Collection LibDataEnum
User string
PeerID string
Groups []string
Caller *tools.HTTPCaller
admin bool
}
func NewRequest(collection LibDataEnum, user string, peerID string, groups []string, caller *tools.HTTPCaller) *Request {
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 ToScheduler(m interface{}) (n *workflow_execution.WorkflowSchedule) {
defer func() {
if r := recover(); r != nil {
return
}
}()
return m.(*workflow_execution.WorkflowSchedule)
}
func (r *Request) Schedule(wfID string, scheduler *workflow_execution.WorkflowSchedule) (*workflow_execution.WorkflowSchedule, error) {
ws, _, _, err := scheduler.Schedules(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
if err != nil {
return nil, err
}
fmt.Println("BAM", ws)
return ws, nil
}
func (r *Request) CheckBooking(wfID string, start string, end string, durationInS float64, cron string) bool {
ok, _, _, _, err := workflow_execution.NewScheduler(start, end, durationInS, cron).CheckBooking(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
if err != nil {
fmt.Println(err)
return false
}
return ok
}
func (r *Request) DraftOrder(scheduler *workflow_execution.WorkflowSchedule) (*order.Order, error) {
o := &order.Order{}
if err := o.DraftOrder(scheduler, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}); err != nil {
return nil, err
}
return o, nil
func NewRequestAdmin(collection LibDataEnum, caller *tools.HTTPCaller) *Request {
return &Request{Collection: collection, Caller: caller, admin: true}
}
/*
func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.WorkflowSchedule) error {
return o.Pay(scheduler, &tools.APIRequest{
Caller: r.caller,
@@ -294,8 +314,9 @@ func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.Wo
PeerID: r.peerID,
Groups: r.groups,
})
return nil
}
*/
/*
* Search will search for the data in the database
* @param filters *dbs.Filters
@@ -311,11 +332,12 @@ func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data
data = LibDataShallow{Data: nil, Code: 500, Err: "Panic recovered in LoadAll : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).Search(filters, word, isDraft)
if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
@@ -338,11 +360,12 @@ func (r *Request) LoadAll(isDraft bool) (data LibDataShallow) {
data = LibDataShallow{Data: nil, Code: 500, Err: "Panic recovered in LoadAll : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).LoadAll(isDraft)
if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
@@ -366,11 +389,12 @@ func (r *Request) LoadOne(id string) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in LoadOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).LoadOne(id)
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -395,12 +419,13 @@ func (r *Request) UpdateOne(set map[string]interface{}, id string) (data LibData
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
model := models.Model(r.collection.EnumIndex())
model := models.Model(r.Collection.EnumIndex())
d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).UpdateOne(model.Deserialize(set, model), id)
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -424,11 +449,12 @@ func (r *Request) DeleteOne(id string) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in DeleteOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).DeleteOne(id)
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -452,12 +478,13 @@ func (r *Request) StoreOne(object map[string]interface{}) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in StoreOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
model := models.Model(r.collection.EnumIndex())
model := models.Model(r.Collection.EnumIndex())
d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).StoreOne(model.Deserialize(object, model))
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -481,12 +508,13 @@ func (r *Request) CopyOne(object map[string]interface{}) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
model := models.Model(r.collection.EnumIndex())
model := models.Model(r.Collection.EnumIndex())
d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).CopyOne(model.Deserialize(object, model))
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -576,3 +604,154 @@ func (l *LibData) ToOrder() *order.Order {
}
return nil
}
func (l *LibData) ToLiveDatacenter() *live.LiveDatacenter {
if l.Data.GetAccessor(nil).GetType() == tools.LIVE_DATACENTER {
return l.Data.(*live.LiveDatacenter)
}
return nil
}
func (l *LibData) ToLiveStorage() *live.LiveStorage {
if l.Data.GetAccessor(nil).GetType() == tools.LIVE_STORAGE {
return l.Data.(*live.LiveStorage)
}
return nil
}
func (l *LibData) ToBookings() *booking.Booking {
if l.Data.GetAccessor(nil).GetType() == tools.BOOKING {
return l.Data.(*booking.Booking)
}
return nil
}
func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
if l.Data.GetAccessor(nil).GetType() == tools.PURCHASE_RESOURCE {
return l.Data.(*purchase_resource.PurchaseResource)
}
return nil
}
// ------------- Loading resources ----------GetAccessor
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
res := NewRequest(LibDataEnum(STORAGE_RESOURCE), user, peerID, groups, nil).LoadOne(storageId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading storage ressource " + storageId)
return nil, errors.New(res.Err)
}
return res.ToStorageResource(), nil
}
func LoadOneComputing(computingId string, user string, peerID string, groups []string) (*resources.ComputeResource, error) {
res := NewRequest(LibDataEnum(COMPUTE_RESOURCE), user, peerID, groups, nil).LoadOne(computingId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading computing ressource " + computingId)
return nil, errors.New(res.Err)
}
return res.ToComputeResource(), nil
}
func LoadOneProcessing(processingId string, user string, peerID string, groups []string) (*resources.ProcessingResource, error) {
res := NewRequest(LibDataEnum(PROCESSING_RESOURCE), user, peerID, groups, nil).LoadOne(processingId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading processing ressource " + processingId)
return nil, errors.New(res.Err)
}
return res.ToProcessingResource(), nil
}
func LoadOneData(dataId string, user string, peerID string, groups []string) (*resources.DataResource, error) {
res := NewRequest(LibDataEnum(DATA_RESOURCE), user, peerID, groups, nil).LoadOne(dataId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading data ressource " + dataId)
return nil, errors.New(res.Err)
}
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
}
}

55
go.mod Normal file → Executable file
View File

@@ -1,28 +1,58 @@
module cloud.o-forge.io/core/oc-lib
go 1.22.0
go 1.25.0
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/google/uuid v1.6.0
github.com/goraz/onion v0.1.3
github.com/nats-io/nats.go v1.37.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.11.1
k8s.io/apimachinery v0.35.1
)
require (
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/nuid v1.0.1 // 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 (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
go.mongodb.org/mongo-driver v1.16.0
golang.org/x/sys v0.22.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)
require (
@@ -36,8 +66,8 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // 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/libp2p/go-libp2p/core v0.43.0-rc2
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -45,16 +75,17 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/robfig/cron v1.2.0
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.8 // 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 Normal file → Executable file
View File

@@ -1,6 +1,8 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beego/beego/v2 v2.3.1 h1:7MUKMpJYzOXtCUsTEoXOxsDV/UcHw6CPbaWMlthVNsc=
github.com/beego/beego/v2 v2.3.1/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
@@ -10,15 +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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -27,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/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-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/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
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/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
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=
@@ -42,34 +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/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
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.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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.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/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-libp2p/core v0.43.0-rc2 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
github.com/magiconair/properties v1.8.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/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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/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/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc=
github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo=
github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
@@ -77,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/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -89,12 +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/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/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/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -105,10 +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/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/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.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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -120,23 +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=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
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-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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/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-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-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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/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-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -147,27 +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.6.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.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/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-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.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=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/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.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/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=

221
models/bill/bill.go Normal file
View File

@@ -0,0 +1,221 @@
package bill
import (
"encoding/json"
"fmt"
"sync"
"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/order"
"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/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* Booking is a struct that represents a booking
*/
type Bill struct {
utils.AbstractObject
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
}
func GenerateBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
// hhmmm : should get... the loop.
return &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: false,
},
OrderID: order.UUID,
Status: enum.PENDING,
// SubOrders: peerOrders,
}, nil
}
func DraftFirstBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
peers := map[string][]*PeerItemOrder{}
for _, p := range order.Purchases {
// TODO : if once
if _, ok := peers[p.DestPeerID]; !ok {
peers[p.DestPeerID] = []*PeerItemOrder{}
}
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
Purchase: p,
Item: p.PricedItem,
Quantity: 1,
})
}
for _, b := range order.Bookings {
// TODO : if once
isPurchased := false
for _, p := range order.Purchases {
if p.ResourceID == b.ResourceID {
isPurchased = true
break
}
}
if isPurchased {
continue
}
if _, ok := peers[b.DestPeerID]; !ok {
peers[b.DestPeerID] = []*PeerItemOrder{}
}
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
Item: b.PricedItem,
})
}
peerOrders := map[string]*PeerOrder{}
for peerID, items := range peers {
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
if err != nil {
return nil, err
}
peerOrders[peerID] = &PeerOrder{
PeerID: peerID,
BillingAddress: pr.(*peer.Peer).WalletAddress,
Items: items,
}
}
bill := &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: true,
},
OrderID: order.UUID,
Status: enum.PENDING,
SubOrders: peerOrders,
}
return bill.SumUpBill(request)
}
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (r *Bill) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.Status != set.(*Bill).Status {
return true, &Bill{Status: set.(*Bill).Status} // only state can be updated
}
return r.IsDraft, set
}
func (r *Bill) CanDelete() bool {
return r.IsDraft // only draft order can be deleted
}
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return d, err
}
d.Total += b.Total
}
return d, nil
}
type PeerOrder struct {
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING
go func() {
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER ON RETURN UPDATE STATUS
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID {
for _, b := range d.Items {
var priced *resources.PricedResource
bb, _ := json.Marshal(b.Item)
json.Unmarshal(bb, priced)
if !priced.IsPurchasable() {
continue
}
accessor := purchase_resource.NewAccessor(request)
accessor.StoreOne(&purchase_resource.PurchaseResource{
ResourceID: priced.GetID(),
ResourceType: priced.GetType(),
EndDate: priced.GetLocationEnd(),
})
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPriceHT(request) // missing something
if err != nil {
return err
}
d.Total += tot
}
return nil
}
type PeerItemOrder struct {
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
}
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
/////////// Temporary in order to allow GenerateOrder to complete while billing is still WIP
if d.Purchase == nil {
return 0, nil
}
///////////
var priced *resources.PricedResource
b, _ := json.Marshal(d.Item)
err := json.Unmarshal(b, priced)
if err != nil {
fmt.Println(err)
return 0, err
}
accessor := purchase_resource.NewAccessor(request)
search, code, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
},
}, "", d.Purchase.IsDraft)
if code == 200 && len(search) > 0 {
for _, s := range search {
if s.(*purchase_resource.PurchaseResource).EndDate == nil || time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
return 0, nil
}
}
}
p, err := priced.GetPriceHT()
if err != nil {
return 0, err
}
return p * float64(d.Quantity), nil
}
// WTF HOW TO SELECT THE RIGHT PRICE ???
// SHOULD SET A BUYING STATUS WHEN PAYMENT IS VALIDATED

View File

@@ -0,0 +1,23 @@
package bill
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 billMongoAccessor struct {
utils.AbstractAccessor[*Bill] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the billMongoAccessor
func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
return &billMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Bill]{
Logger: logs.CreateLogger(tools.BILL.String()), // Create a logger with the data type
Request: request,
Type: tools.BILL,
New: func() *Bill { return &Bill{} },
},
}
}

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

@@ -0,0 +1,2 @@
# Billing process
Scheduler process a drafted order + a first bill corresponding to every once buying.

View File

@@ -5,6 +5,7 @@ import (
"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/models"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"go.mongodb.org/mongo-driver/bson/primitive"
@@ -14,20 +15,54 @@ import (
* Booking is a struct that represents a booking
*/
type Booking struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
DestPeerID string `json:"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
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
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
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty"` // We need to add the validate:"required" tag once the pricing feature is implemented, removed to avoid handling the error
ResumeMetrics map[string]map[string]models.MetricResume `json:"resume_metrics,omitempty" bson:"resume_metrics,omitempty"`
ExecutionMetrics map[string][]models.MetricsSnapshot `json:"metrics,omitempty" bson:"metrics,omitempty"`
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
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
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
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
RealStartDate *time.Time `json:"real_start_date,omitempty" bson:"real_start_date,omitempty"` // RealStartDate is the real start date of the booking
RealEndDate *time.Time `json:"real_end_date,omitempty" bson:"real_end_date,omitempty"` // RealEndDate is the real end date of the booking
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
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
}
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
m := map[string]map[string]models.MetricResume{}
for instance, snapshot := range b.ExecutionMetrics {
m[instance] = map[string]models.MetricResume{}
for _, metric := range snapshot {
for _, mm := range metric.Metrics {
if resume, ok := m[instance][mm.Name]; !ok {
m[instance][mm.Name] = models.MetricResume{
Delta: 0,
LastValue: mm.Value,
}
} else {
delta := resume.LastValue - mm.Value
if delta == 0 {
resume.Delta = delta
} else {
resume.Delta = (resume.Delta + delta) / 2
}
resume.LastValue = mm.Value
m[instance][mm.Name] = resume
}
}
}
}
return m
}
// CheckBooking checks if a booking is possible on a specific compute resource
@@ -85,7 +120,7 @@ func (d *Booking) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *Booking) VerifyAuth(request *tools.APIRequest) bool {
func (d *Booking) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}

View File

@@ -4,24 +4,24 @@ import (
"errors"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"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/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type bookingMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
type BookingMongoAccessor struct {
utils.AbstractAccessor[*Booking] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
return &bookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
// New creates a new instance of the BookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
return &BookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Booking]{
Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type
Request: request,
Type: tools.BOOKING,
New: func() *Booking { return &Booking{} },
},
}
}
@@ -29,11 +29,7 @@ func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *bookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
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")
}
@@ -41,16 +37,8 @@ func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.D
return utils.GenericUpdateOne(realSet, id, a, &Booking{})
}
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) {
return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now()
now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
@@ -67,15 +55,7 @@ func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
}, a)
}
func (a *bookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
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 {
func (a *BookingMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now()
now = now.Add(time.Second * -60)

23
models/booking/enums.go Normal file
View File

@@ -0,0 +1,23 @@
package booking
type BookingMode int
const (
PLANNED BookingMode = iota // predictible
PREEMPTED // can be both predictible or unpredictible, first one asking for a quick exec, second on event, but we pay to preempt in any case.
WHEN_POSSIBLE // unpredictable, two mode of payment can be available on that case: fixed, or per USE
)
/*
Ok make a point there:
There is 3 notions about booking & payment :
Booking mode : WHEN is executed
Buying mode : Duration of payment
Pricing Mode : How Many time we pay
We can simplify Buying Mode and Pricing Mode, some Buying Mode implied limited pricing mode
Such as Rules. Just like PERMANENT BUYING can be paid only once.
Booking Mode on WHEN POSSIBLE make an exception, because we can't know when executed.
*/

View File

@@ -0,0 +1,434 @@
package planner
import (
"encoding/json"
"time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"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)
}
// 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)
bookings, code, err := accessor.Search(nil, "*", false)
if code != 200 || err != nil {
return nil, err
}
p := &Planner{
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
}
for _, b := range bookings {
bk := b.(*booking.Booking)
// 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.Add(time.Hour)
end = &e
}
instanceID, usage, cap := extractSlotData(bk, request)
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(time.Hour)
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
}
// 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
}

View File

@@ -0,0 +1,87 @@
package booking_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"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/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
func TestBooking_GetDurations(t *testing.T) {
start := time.Now().Add(-2 * time.Hour)
end := start.Add(1 * time.Hour)
realStart := start.Add(30 * time.Minute)
realEnd := realStart.Add(90 * time.Minute)
b := &booking.Booking{
ExpectedStartDate: start,
ExpectedEndDate: &end,
RealStartDate: &realStart,
RealEndDate: &realEnd,
}
assert.Equal(t, 30*time.Minute, b.GetDelayForLaunch())
assert.Equal(t, 90*time.Minute, b.GetRealDuration())
assert.Equal(t, end.Sub(start), b.GetUsualDuration())
assert.Equal(t, b.GetRealDuration()-b.GetUsualDuration(), b.GetDelayOnDuration())
assert.Equal(t, realEnd.Sub(start), b.GetDelayForFinishing())
}
func TestBooking_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
b := &booking.Booking{}
accessor := b.GetAccessor(req)
assert.NotNil(t, accessor)
assert.Equal(t, tools.BOOKING, accessor.(*booking.BookingMongoAccessor).Type)
}
func TestBooking_VerifyAuth(t *testing.T) {
assert.True(t, (&booking.Booking{}).VerifyAuth("get", nil))
}
func TestBooking_StoreDraftDefault(t *testing.T) {
b := &booking.Booking{}
b.StoreDraftDefault()
assert.False(t, b.IsDraft)
}
func TestBooking_CanUpdate(t *testing.T) {
now := time.Now()
b := &booking.Booking{
State: enum.SCHEDULED,
AbstractObject: utils.AbstractObject{IsDraft: false},
RealStartDate: &now,
}
set := &booking.Booking{
State: enum.DELAYED,
RealStartDate: &now,
}
ok, result := b.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.DELAYED, result.(*booking.Booking).State)
}
func TestBooking_CanDelete(t *testing.T) {
b := &booking.Booking{AbstractObject: utils.AbstractObject{IsDraft: true}}
assert.True(t, b.CanDelete())
b.IsDraft = false
assert.False(t, b.CanDelete())
}
func TestNewAccessor(t *testing.T) {
req := &tools.APIRequest{}
accessor := booking.NewAccessor(req)
assert.NotNil(t, accessor)
assert.Equal(t, tools.BOOKING, accessor.Type)
assert.Equal(t, req, accessor.Request)
}

View File

@@ -71,7 +71,7 @@ func (ao *CollaborativeArea) Clear(peerID string) {
ao.CollaborativeAreaRule.CreatedAt = time.Now().UTC()
}
func (ao *CollaborativeArea) VerifyAuth(request *tools.APIRequest) bool {
func (ao *CollaborativeArea) VerifyAuth(callName string, request *tools.APIRequest) bool {
if (ao.AllowedPeersGroup != nil || config.GetConfig().Whitelist) && request != nil {
if grps, ok := ao.AllowedPeersGroup[request.PeerID]; ok || config.GetConfig().Whitelist {
if slices.Contains(grps, "*") || (!ok && config.GetConfig().Whitelist) {
@@ -84,7 +84,7 @@ func (ao *CollaborativeArea) VerifyAuth(request *tools.APIRequest) bool {
}
}
}
return ao.AbstractObject.VerifyAuth(request)
return ao.AbstractObject.VerifyAuth(callName, request)
}
func (d *CollaborativeArea) GetAccessor(request *tools.APIRequest) utils.Accessor {
@@ -97,7 +97,7 @@ func (d *CollaborativeArea) Trim() *CollaborativeArea {
func (d *CollaborativeArea) StoreDraftDefault() {
d.AllowedPeersGroup = map[string][]string{
d.CreatorID: []string{"*"},
d.CreatorID: {"*"},
}
d.IsDraft = false
}

View File

@@ -17,7 +17,7 @@ import (
// SharedWorkspace is a struct that represents a collaborative area
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
workflowAccessor utils.Accessor
@@ -27,10 +27,11 @@ type collaborativeAreaMongoAccessor struct {
func NewAccessor(request *tools.APIRequest) *collaborativeAreaMongoAccessor {
return &collaborativeAreaMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
AbstractAccessor: utils.AbstractAccessor[*CollaborativeArea]{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request,
Type: tools.COLLABORATIVE_AREA,
New: func() *CollaborativeArea { return &CollaborativeArea{} },
},
workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request),
workflowAccessor: (&w.Workflow{}).GetAccessor(request),
@@ -63,13 +64,18 @@ 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
func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
_, id := (&peer.Peer{}).IsMySelf() // get the local peer
data.(*CollaborativeArea).Clear(id) // set the creator
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})) // get the local peer
if err != nil || pp == nil {
return data, 404, err
}
data.(*CollaborativeArea).Clear(pp.GetID()) // set the creator
// retrieve or proper peer
if data.(*CollaborativeArea).CollaborativeAreaRule != nil {
data.(*CollaborativeArea).CollaborativeAreaRule = &CollaborativeAreaRule{}
}
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = id
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = pp.GetID()
d, code, err := utils.GenericStoreOne(data.(*CollaborativeArea).Trim(), a)
if code == 200 {
a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows
@@ -79,11 +85,6 @@ func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DB
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 {
var new []T
res, code, _ := a.Search(&dbs.Filters{
@@ -125,23 +126,10 @@ func (a *collaborativeAreaMongoAccessor) enrich(sharedWorkspace *CollaborativeAr
return sharedWorkspace
}
func (a *collaborativeAreaMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*CollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return a.enrich(d.(*CollaborativeArea), false, a.Request), 200, nil
}, 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)
func (a *collaborativeAreaMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), isDraft, a.Request)
}
}
/*
@@ -254,6 +242,8 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
// 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
func (a *collaborativeAreaMongoAccessor) deleteToPeer(shared *CollaborativeArea) {
a.contactPeer(shared, tools.POST)
@@ -271,7 +261,9 @@ func (a *collaborativeAreaMongoAccessor) contactPeer(shared *CollaborativeArea,
paccess := (&peer.Peer{})
for k := range shared.AllowedPeersGroup {
if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: k}}).IsMySelf(); ok || (shared.IsSent && meth == tools.POST) || (!shared.IsSent && meth != tools.POST) {
if ok, _ := utils.IsMySelf(k, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok || (shared.IsSent && meth == tools.POST) || (!shared.IsSent && meth != tools.POST) {
continue
}
shared.IsSent = meth == tools.POST

View File

@@ -24,6 +24,6 @@ func (d *Rule) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (d *Rule) VerifyAuth(request *tools.APIRequest) bool {
func (d *Rule) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}

View File

@@ -1,62 +1,23 @@
package rule
import (
"cloud.o-forge.io/core/oc-lib/dbs"
"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 ruleMongoAccessor struct {
utils.AbstractAccessor
utils.AbstractAccessor[*Rule]
}
// New creates a new instance of the ruleMongoAccessor
func NewAccessor(request *tools.APIRequest) *ruleMongoAccessor {
return &ruleMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
AbstractAccessor: utils.AbstractAccessor[*Rule]{
Logger: logs.CreateLogger(tools.RULE.String()), // Create a logger with the data type
Request: request,
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
import (
"cloud.o-forge.io/core/oc-lib/dbs"
"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 shallowSharedWorkspaceMongoAccessor struct {
utils.AbstractAccessor
utils.AbstractAccessor[*ShallowCollaborativeArea]
}
func NewAccessor(request *tools.APIRequest) *shallowSharedWorkspaceMongoAccessor {
return &shallowSharedWorkspaceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
AbstractAccessor: utils.AbstractAccessor[*ShallowCollaborativeArea]{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request, // Set the caller
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

@@ -0,0 +1,17 @@
package models
type MetricsSnapshot struct {
From string `json:"origin"`
Metrics []Metric `json:"metrics"`
}
type Metric struct {
Name string `json:"name"`
Value float64 `json:"value"`
Error error `json:"error"`
}
type MetricResume struct {
Delta float64 `json:"delta"`
LastValue float64 `json:"last_value"`
}

0
models/common/planner.go Normal file → Executable file
View File

11
models/common/pricing/interfaces.go Normal file → Executable file
View File

@@ -3,18 +3,25 @@ package pricing
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/tools"
)
type PricedItemITF interface {
GetID() string
GetInstanceID() string
GetType() tools.DataType
IsPurchased() bool
IsPurchasable() bool
IsBooked() bool
GetQuantity() int
AddQuantity(amount int)
GetBookingMode() booking.BookingMode
GetCreatorID() string
SelectPricing() PricingProfileITF
GetLocationStart() *time.Time
SetLocationStart(start time.Time)
SetLocationEnd(end time.Time)
GetLocationEnd() *time.Time
GetExplicitDurationInS() float64
GetPrice() (float64, error)
GetPriceHT() (float64, error)
}

33
models/common/pricing/pricing_profile.go Normal file → Executable file
View File

@@ -5,9 +5,11 @@ import (
)
type PricingProfileITF interface {
GetPrice(quantity float64, val float64, start time.Time, end time.Time, params ...string) (float64, error)
IsPurchased() bool
IsBooked() bool
IsPurchasable() bool
GetPurchase() BuyingStrategy
GetOverrideStrategyValue() int
GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variation []*PricingVariation, params ...string) (float64, error)
}
type RefundType int
@@ -32,10 +34,37 @@ type AccessPricingProfile[T Strategy] struct { // only use for acces such as : D
RefundRatio int32 `json:"refund_ratio" bson:"refund_ratio" default:"0"` // RefundRatio is the refund ratio if missing
}
func (a AccessPricingProfile[T]) IsBooked() bool {
return a.Pricing.BuyingStrategy == SUBSCRIPTION
}
func (a AccessPricingProfile[T]) IsPurchasable() bool {
return a.Pricing.BuyingStrategy == PERMANENT
}
func (a AccessPricingProfile[T]) GetPurchase() BuyingStrategy {
return a.Pricing.BuyingStrategy
}
func (a AccessPricingProfile[T]) GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variations []*PricingVariation, params ...string) (float64, error) {
return a.Pricing.GetPriceHT(quantity, val, start, &end, variations)
}
func (b *AccessPricingProfile[T]) GetOverrideStrategyValue() int {
return -1
}
func GetDefaultPricingProfile() PricingProfileITF {
return &AccessPricingProfile[TimePricingStrategy]{
Pricing: PricingStrategy[TimePricingStrategy]{
Price: 0,
Currency: "EUR",
BuyingStrategy: PERMANENT,
TimePricingStrategy: ONCE,
},
}
}
type ExploitPrivilegeStrategy int
const (

107
models/common/pricing/pricing_strategy.go Normal file → Executable file
View File

@@ -7,20 +7,65 @@ import (
"time"
)
type BillingStrategy int // BAM BAM
// should except... on
const (
BILL_ONCE BillingStrategy = iota // is a permanent buying ( predictible )
BILL_PER_WEEK
BILL_PER_MONTH
BILL_PER_YEAR
)
func (t BillingStrategy) IsBillingStrategyAllowed(bs int) (BillingStrategy, bool) {
switch t {
case BILL_ONCE:
return BILL_ONCE, bs == 0
case BILL_PER_WEEK:
case BILL_PER_MONTH:
case BILL_PER_YEAR:
return t, bs != 0
}
return t, false
}
func (t BillingStrategy) String() string {
return [...]string{"BILL_ONCE", "BILL_PER_WEEK", "BILL_PER_MONTH", "BILL_PER_YEAR"}[t]
}
func BillingStrategyList() []BillingStrategy {
return []BillingStrategy{BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR}
}
type BuyingStrategy int
// should except... on
const (
UNLIMITED BuyingStrategy = iota
SUBSCRIPTION
PAY_PER_USE
PERMANENT BuyingStrategy = iota // is a permanent buying ( predictible )
UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible )
SUBSCRIPTION // a defined subscription ( predictible )
// PAY_PER_USE // per request. ( unpredictible )
)
func (t BuyingStrategy) String() string {
return [...]string{"UNLIMITED", "SUBSCRIPTION", "PAY PER USE"}[t]
return [...]string{"PERMANENT", "UNDEFINED_SUBSCRIPTION", "SUBSCRIPTION"}[t]
}
func (t BuyingStrategy) IsBillingStrategyAllowed(bs BillingStrategy) (BillingStrategy, bool) {
switch t {
case PERMANENT:
return BILL_ONCE, bs == BILL_ONCE
case UNDEFINED_SUBSCRIPTION:
return BILL_PER_MONTH, bs != BILL_ONCE
case SUBSCRIPTION:
/*case PAY_PER_USE:
return bs, true*/
}
return bs, false
}
func BuyingStrategyList() []BuyingStrategy {
return []BuyingStrategy{UNLIMITED, SUBSCRIPTION, PAY_PER_USE}
return []BuyingStrategy{PERMANENT, UNDEFINED_SUBSCRIPTION, SUBSCRIPTION}
}
type Strategy interface {
@@ -44,6 +89,10 @@ func (t TimePricingStrategy) String() string {
return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t]
}
func TimePricingStrategyListStr() []string {
return []string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}
}
func TimePricingStrategyList() []TimePricingStrategy {
return []TimePricingStrategy{ONCE, PER_SECOND, PER_MINUTE, PER_HOUR, PER_DAY, PER_WEEK, PER_MONTH}
}
@@ -101,6 +150,7 @@ func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInS
return 0, errors.New("pricing strategy not found")
}
// may suppress in pricing strategy -> to set in map
type PricingStrategy[T Strategy] struct {
Price float64 `json:"price" bson:"price" default:"0"` // Price is the Price of the pricing
Currency string `json:"currency" bson:"currency" default:"USD"` // Currency is the currency of the pricing
@@ -109,11 +159,37 @@ type PricingStrategy[T Strategy] struct {
OverrideStrategy T `json:"override_strategy" bson:"override_strategy" default:"-1"` // Modulation is the modulation of the pricing
}
func (p PricingStrategy[T]) GetPrice(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time) (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 {
return BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
} else if p.BuyingStrategy == UNLIMITED {
price, err := BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
if err != nil {
return 0, err
}
if variations != nil {
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.Price, nil
} else if p.BuyingStrategy == PERMANENT {
if variations != nil {
price := p.Price
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.Price, nil
}
if variations != nil {
price := p.Price
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.Price * float64(amountOfData), nil
}
@@ -129,3 +205,18 @@ func (p PricingStrategy[T]) GetTimePricingStrategy() TimePricingStrategy {
func (p PricingStrategy[T]) GetOverrideStrategy() T {
return p.OverrideStrategy
}
type PricingVariation struct {
Inflate bool `json:"inflate" bson:"price"` // Price is the Price of the pricing
Percentage float64 `json:"percent" bson:"percent"` // Currency is the currency of the pricing // Modulation is the modulation of the pricing
Priority int `json:"priority" bson:"priority"`
}
func (pv *PricingVariation) GetPriceHT(priceHT float64) float64 {
value := (priceHT * pv.Percentage) / 100
if pv.Inflate {
return priceHT + value
} else {
return priceHT - value
}
}

View File

@@ -0,0 +1,129 @@
package pricing_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
)
type DummyStrategy int
func (d DummyStrategy) GetStrategy() string { return "DUMMY" }
func (d DummyStrategy) GetStrategyValue() int { return int(d) }
func TestBuyingStrategy_String(t *testing.T) {
assert.Equal(t, "PERMANENT", pricing.PERMANENT.String())
assert.Equal(t, "UNDEFINED_SUBSCRIPTION", pricing.UNDEFINED_SUBSCRIPTION.String())
assert.Equal(t, "SUBSCRIPTION", pricing.SUBSCRIPTION.String())
}
func TestBuyingStrategyList(t *testing.T) {
list := pricing.BuyingStrategyList()
assert.Equal(t, 3, len(list))
assert.Contains(t, list, pricing.SUBSCRIPTION)
}
func TestTimePricingStrategy_String(t *testing.T) {
assert.Equal(t, "ONCE", pricing.ONCE.String())
assert.Equal(t, "PER SECOND", pricing.PER_SECOND.String())
assert.Equal(t, "PER MONTH", pricing.PER_MONTH.String())
}
func TestTimePricingStrategyList(t *testing.T) {
list := pricing.TimePricingStrategyList()
assert.Equal(t, 7, len(list))
assert.Contains(t, list, pricing.PER_DAY)
}
func TestTimePricingStrategy_Methods(t *testing.T) {
ts := pricing.PER_MINUTE
assert.Equal(t, "PER_MINUTE", ts.GetStrategy())
assert.Equal(t, 2, ts.GetStrategyValue())
}
func Test_getAverageTimeInSecond_WithEnd(t *testing.T) {
start := time.Now()
end := start.Add(30 * time.Minute)
_, err := pricing.BookingEstimation(pricing.PER_MINUTE, 2.0, 1200, start, &end)
assert.NoError(t, err)
}
func Test_getAverageTimeInSecond_WithoutEnd(t *testing.T) {
start := time.Now()
// getAverageTimeInSecond is tested via BookingEstimation
price, err := pricing.BookingEstimation(pricing.PER_HOUR, 10.0, 100, start, nil)
assert.NoError(t, err)
assert.True(t, price > 0)
}
func TestBookingEstimation(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
strategies := map[pricing.TimePricingStrategy]float64{
pricing.ONCE: 50,
pricing.PER_HOUR: 10,
pricing.PER_MINUTE: 1,
pricing.PER_SECOND: 0.1,
pricing.PER_DAY: 100,
pricing.PER_WEEK: 500,
pricing.PER_MONTH: 2000,
}
for strategy, price := range strategies {
t.Run(strategy.String(), func(t *testing.T) {
cost, err := pricing.BookingEstimation(strategy, price, 3600, start, &end)
assert.NoError(t, err)
assert.True(t, cost >= 0)
})
}
_, err := pricing.BookingEstimation(999, 10, 3600, start, &end)
assert.Error(t, err)
}
func TestPricingStrategy_Getters(t *testing.T) {
ps := pricing.PricingStrategy[DummyStrategy]{
Price: 20,
Currency: "USD",
BuyingStrategy: pricing.SUBSCRIPTION,
TimePricingStrategy: pricing.PER_MINUTE,
OverrideStrategy: DummyStrategy(1),
}
assert.Equal(t, pricing.SUBSCRIPTION, ps.GetBuyingStrategy())
assert.Equal(t, pricing.PER_MINUTE, ps.GetTimePricingStrategy())
assert.Equal(t, DummyStrategy(1), ps.GetOverrideStrategy())
}
func TestPricingStrategy_GetPriceHT(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
// SUBSCRIPTION case
ps := pricing.PricingStrategy[DummyStrategy]{
Price: 5,
BuyingStrategy: pricing.SUBSCRIPTION,
TimePricingStrategy: pricing.PER_HOUR,
}
p, err := ps.GetPriceHT(2, 3600, start, &end, nil)
assert.NoError(t, err)
assert.True(t, p > 0)
// UNLIMITED case
ps.BuyingStrategy = pricing.PERMANENT
p, err = ps.GetPriceHT(10, 0, start, &end, nil)
assert.NoError(t, err)
assert.Equal(t, 5.0, p)
// UNDEFINED_SUBSCRIPTION case: price * quantity
ps.BuyingStrategy = pricing.UNDEFINED_SUBSCRIPTION
p, err = ps.GetPriceHT(3, 0, start, &end, nil)
assert.NoError(t, err)
assert.Equal(t, 15.0, p)
}

18
models/live/interfaces.go Executable file
View File

@@ -0,0 +1,18 @@
package live
import (
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type LiveInterface interface {
utils.DBObject
GetMonitorPath() string
GetResourcesID() []string
SetResourcesID(string)
GetResourceAccessor(request *tools.APIRequest) utils.Accessor
GetResource() resources.ResourceInterface
GetResourceInstance() resources.ResourceInstanceITF
SetResourceInstance(res resources.ResourceInterface, i resources.ResourceInstanceITF) resources.ResourceInterface
}

71
models/live/live.go Normal file
View File

@@ -0,0 +1,71 @@
package live
import (
"slices"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/biter777/countries"
)
/*
* LiveDatacenter is a struct that represents a compute units in your datacenters
*/
type Credentials struct {
Login string `json:"login,omitempty" bson:"login,omitempty"`
Pass string `json:"password,omitempty" bson:"password,omitempty"`
Token string `json:"token,omitempty" bson:"token,omitempty"`
}
type Certs struct {
AuthorityCertificate string `json:"authority_certificate,omitempty" bson:"authority_certificate,omitempty"`
ClientCertificate string `json:"client_certificate,omitempty" bson:"client_certificate,omitempty"`
}
type LiveCerts struct {
Host string `json:"host,omitempty" bson:"host,omitempty"`
Port string `json:"port,omitempty" bson:"port,omitempty"`
Certificates *Certs `json:"certs,omitempty" bson:"certs,omitempty"`
Credentials *Credentials `json:"creds,omitempty" bson:"creds,omitempty"`
}
// TODO in the future multiple type of certs depending of infra type
type AbstractLive struct {
utils.AbstractObject
Certs LiveCerts `json:"certs,omitempty" bson:"certs,omitempty"`
MonitorPath string `json:"monitor_path,omitempty" bson:"monitor_path,omitempty"`
Location resources.GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
ResourcesID []string `json:"resources_id" bson:"resources_id"`
}
func (d *AbstractLive) GetMonitorPath() string {
return d.MonitorPath
}
func (d *AbstractLive) GetResourcesID() []string {
return d.ResourcesID
}
func (d *AbstractLive) SetResourcesID(id string) {
if !slices.Contains(d.ResourcesID, id) {
d.ResourcesID = append(d.ResourcesID, id)
}
}
func (r *AbstractLive) GetResourceType() tools.DataType {
return tools.INVALID
}
func (r *AbstractLive) StoreDraftDefault() {
r.IsDraft = true
}
func (r *AbstractLive) CanDelete() bool {
return r.IsDraft // only draft ComputeUnits can be deleted
}

View File

@@ -0,0 +1,50 @@
package live
import (
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* LiveDatacenter is a struct that represents a compute units in your datacenters
*/
type LiveDatacenter struct {
AbstractLive
StorageType enum.StorageType `bson:"storage_type" json:"storage_type" default:"-1"` // Type is the type of the storage
Acronym string `bson:"acronym,omitempty" json:"acronym,omitempty"` // Acronym is the acronym of the storage
Architecture string `json:"architecture,omitempty" bson:"architecture,omitempty"` // Architecture is the architecture
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*resources.ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
}
func (d *LiveDatacenter) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveDatacenter](tools.LIVE_DATACENTER, request) // Create a new instance of the accessor
}
func (d *LiveDatacenter) GetResourceAccessor(request *tools.APIRequest) utils.Accessor {
return resources.NewAccessor[*resources.ComputeResource](tools.COMPUTE_RESOURCE, request, func() utils.DBObject { return &resources.ComputeResource{} })
}
func (d *LiveDatacenter) GetResource() resources.ResourceInterface {
return &resources.ComputeResource{}
}
func (d *LiveDatacenter) GetResourceInstance() resources.ResourceInstanceITF {
return &resources.ComputeResourceInstance{}
}
func (d *LiveDatacenter) SetResourceInstance(res resources.ResourceInterface, i resources.ResourceInstanceITF) resources.ResourceInterface {
r := res.(*resources.ComputeResource)
r.Instances = append(r.Instances, i.(*resources.ComputeResourceInstance))
return r
}

View File

@@ -0,0 +1,92 @@
package live
import (
"encoding/json"
"errors"
"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 computeUnitsMongoAccessor[T LiveInterface] struct {
utils.AbstractAccessor[LiveInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the computeUnitsMongoAccessor
func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *computeUnitsMongoAccessor[T] {
return &computeUnitsMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor[LiveInterface]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
Type: t,
New: func() LiveInterface {
switch t {
case tools.LIVE_DATACENTER:
return &LiveDatacenter{}
case tools.LIVE_STORAGE:
return &LiveStorage{}
}
return &LiveDatacenter{}
},
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *computeUnitsMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
// is a publisher... that become a resources.
if data.IsDrafted() {
return nil, 422, errors.New("can't publish a drafted compute units")
}
live := data.(T)
if live.GetMonitorPath() == "" || live.GetID() != "" {
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 {
return nil, code, err
} else {
live = res.(T)
}
resAccess := live.GetResourceAccessor(a.Request)
instance := live.GetResourceInstance()
b, _ := json.Marshal(live)
json.Unmarshal(b, instance)
if len(live.GetResourcesID()) > 0 {
for _, r := range live.GetResourcesID() {
// TODO dependent of a existing resource
res, code, err := resAccess.LoadOne(r)
if err == nil {
return nil, code, err
}
existingResource := live.GetResource()
b, _ := json.Marshal(res)
json.Unmarshal(b, existingResource)
live.SetResourceInstance(existingResource, instance)
resAccess.UpdateOne(existingResource, existingResource.GetID())
}
if live.GetID() != "" {
return a.LoadOne(live.GetID())
} else {
return a.StoreOne(live)
}
} else {
r := live.GetResource()
b, _ := json.Marshal(live)
json.Unmarshal(b, &r)
live.SetResourceInstance(r, instance)
res, code, err := resAccess.StoreOne(r)
if err != nil {
return nil, code, err
}
live.SetResourcesID(res.GetID())
if live.GetID() != "" {
return a.UpdateOne(live, live.GetID())
} else {
return a.StoreOne(live)
}
}
}

View File

@@ -0,0 +1,46 @@
package live
import (
"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/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* LiveStorage is a struct that represents a compute units in your datacenters
*/
type LiveStorage struct {
AbstractLive
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
Path string `bson:"path,omitempty" json:"path,omitempty"` // Path is the store folders in the source
Local bool `bson:"local" json:"local"`
SecurityLevel string `bson:"security_level,omitempty" json:"security_level,omitempty"`
SizeType enum.StorageSize `bson:"size_type" json:"size_type" default:"0"` // SizeType is the type of the storage size
SizeGB int64 `bson:"size,omitempty" json:"size,omitempty"` // Size is the size of the storage
Encryption bool `bson:"encryption,omitempty" json:"encryption,omitempty"` // Encryption is a flag that indicates if the storage is encrypted
Redundancy string `bson:"redundancy,omitempty" json:"redundancy,omitempty"` // Redundancy is the redundancy of the storage
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
}
func (d *LiveStorage) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveStorage](tools.LIVE_STORAGE, request) // Create a new instance of the accessor
}
func (d *LiveStorage) GetResourceAccessor(request *tools.APIRequest) utils.Accessor {
return resources.NewAccessor[*resources.ComputeResource](tools.STORAGE_RESOURCE, request, func() utils.DBObject { return &resources.StorageResource{} })
}
func (d *LiveStorage) GetResource() resources.ResourceInterface {
return &resources.StorageResource{}
}
func (d *LiveStorage) GetResourceInstance() resources.ResourceInstanceITF {
return &resources.StorageResourceInstance{}
}
func (d *LiveStorage) SetResourceInstance(res resources.ResourceInterface, i resources.ResourceInstanceITF) resources.ResourceInterface {
r := res.(*resources.StorageResource)
r.Instances = append(r.Instances, i.(*resources.StorageResourceInstance))
return r
}

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

@@ -2,6 +2,8 @@ package models
import (
"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/live"
"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"
@@ -21,12 +23,13 @@ import (
This package contains the models used in the application
It's used to create the models dynamically
*/
var models = map[string]func() utils.DBObject{
var ModelsCatalog = map[string]func() utils.DBObject{
tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} },
tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} },
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
tools.STORAGE_RESOURCE.String(): func() utils.DBObject { return &resource.StorageResource{} },
tools.PROCESSING_RESOURCE.String(): func() utils.DBObject { return &resource.ProcessingResource{} },
tools.NATIVE_TOOL.String(): func() utils.DBObject { return &resource.NativeTool{} },
tools.WORKFLOW.String(): func() utils.DBObject { return &w2.Workflow{} },
tools.WORKFLOW_EXECUTION.String(): func() utils.DBObject { return &workflow_execution.WorkflowExecution{} },
tools.WORKSPACE.String(): func() utils.DBObject { return &w3.Workspace{} },
@@ -38,22 +41,30 @@ var models = map[string]func() utils.DBObject{
tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} },
tools.ORDER.String(): func() utils.DBObject { return &order.Order{} },
tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} },
tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} },
tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
tools.BILL.String(): func() utils.DBObject { return &bill.Bill{} },
}
// Model returns the model object based on the model type
func Model(model int) utils.DBObject {
log := logs.GetLogger()
if _, ok := models[tools.FromInt(model)]; ok {
return models[tools.FromInt(model)]()
if model < 0 || model >= len(tools.Str) {
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
}
// GetModelsNames returns the names of the models
func GetModelsNames() []string {
names := []string{}
for name := range models {
for name := range ModelsCatalog {
names = append(names, name)
}
return names

View File

@@ -1,19 +1,13 @@
package order
import (
"errors"
"fmt"
"sync"
"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/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"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"
)
@@ -23,12 +17,12 @@ import (
type Order struct {
utils.AbstractObject
OrderBy string `json:"order_by" bson:"order_by" validate:"required"`
WorkflowID string `json:"workflow_id" bson:"workflow_id" validate:"required"`
WorkflowExecutionIDs []string `json:"workflow_execution_ids" bson:"workflow_execution_ids" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
ExecutionsID string `json:"executions_id" bson:"executions_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"`
Bookings []*booking.Booking `json:"bookings" bson:"bookings"`
Billing map[pricing.BillingStrategy][]*booking.Booking `json:"billing" bson:"billing"`
}
func (r *Order) StoreDraftDefault() {
@@ -46,287 +40,14 @@ func (r *Order) CanDelete() bool {
return r.IsDraft // only draft order can be deleted
}
func (o *Order) DraftOrder(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
// set the draft order from the model
if err := o.draftStoreFromModel(scheduler, request); err != nil {
return err
}
return nil
}
func (o *Order) Pay(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
if _, err := o.draftBookOrder(scheduler, request); err != nil {
return err
}
o.Status = enum.PENDING
_, code, err := o.GetAccessor(request).UpdateOne(o, o.GetID())
if code != 200 || err != nil {
return errors.New("could not update the order" + fmt.Sprintf("%v", err))
}
if err := o.pay(request); err != nil { // pay the order
return err
} else {
o.IsDraft = false
}
for _, exec := range scheduler.WorkflowExecution {
exec.IsDraft = false
_, code, err := utils.GenericUpdateOne(exec, exec.GetID(),
workflow_execution.NewAccessor(request), &workflow_execution.WorkflowExecution{})
if code != 200 || err != nil {
return errors.New("could not update the workflow execution" + fmt.Sprintf("%v", err))
}
}
_, code, err = o.GetAccessor(request).UpdateOne(o, o.GetID())
if code != 200 || err != nil {
return errors.New("could not update the order" + fmt.Sprintf("%v", err))
}
/*
TODO : TEMPORARY SET BOOKINGS TO UNDRAFT TO AVOID DELETION
BUT NEXT ONLY WHO IS PAYED WILL BE ALLOWED TO CHANGE IT
*/
return nil
}
func (o *Order) draftStoreFromModel(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
if request == nil {
return errors.New("no request found")
}
fmt.Println("Drafting order", scheduler.Workflow)
if scheduler.Workflow == nil || scheduler.Workflow.Graph == nil { // if the workflow has no graph, return an error
return errors.New("no graph found")
}
o.SetName()
o.WorkflowID = scheduler.Workflow.GetID()
o.IsDraft = true
o.OrderBy = request.PeerID
o.WorkflowExecutionIDs = []string{} // create an array of ids
for _, exec := range scheduler.WorkflowExecution {
o.WorkflowExecutionIDs = append(o.WorkflowExecutionIDs, exec.GetID())
}
// set the name of the order
resourcesByPeer := map[string][]pricing.PricedItemITF{} // create a map of resources by peer
processings := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsProcessing, request) // get the processing items
datas := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsData, request) // get the data items
storages := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsStorage, request) // get the storage items
workflows := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsWorkflow, request) // get the workflow items
for _, items := range []map[string]pricing.PricedItemITF{processings, datas, storages, workflows} {
for _, item := range items {
if _, ok := resourcesByPeer[item.GetCreatorID()]; !ok {
resourcesByPeer[item.GetCreatorID()] = []pricing.PricedItemITF{}
}
resourcesByPeer[item.GetCreatorID()] = append(resourcesByPeer[item.GetCreatorID()], item)
}
}
for peerID, resources := range resourcesByPeer {
peerOrder := &PeerOrder{
Status: enum.DRAFTED,
PeerID: peerID,
}
peerOrder.GenerateID()
for _, resource := range resources {
peerOrder.AddItem(resource, len(resources)) // TODO SPECIALS REF ADDITIONALS NOTES
}
if o.SubOrders == nil {
o.SubOrders = map[string]*PeerOrder{}
}
o.SubOrders[peerOrder.GetID()] = peerOrder
}
// search an order with same user name and same session id
err := o.sumUpBill(request)
if err != nil {
return err
}
// should store the order
res, code, err := o.GetAccessor(request).Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"workflow_id": {{Operator: dbs.EQUAL.String(), Value: o.WorkflowID}},
"order_by": {{Operator: dbs.EQUAL.String(), Value: request.PeerID}},
},
}, "", o.IsDraft)
if code != 200 || err != nil {
return errors.New("could not search the order" + fmt.Sprintf("%v", err))
}
if len(res) > 0 {
_, code, err := utils.GenericUpdateOne(o, res[0].GetID(), o.GetAccessor(request), o)
if code != 200 || err != nil {
return errors.New("could not update the order" + fmt.Sprintf("%v", err))
}
} else {
_, code, err := utils.GenericStoreOne(o, o.GetAccessor(request))
if code != 200 || err != nil {
return errors.New("could not store the order" + fmt.Sprintf("%v", err))
}
}
return nil
}
func (o *Order) draftBookOrder(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) ([]*booking.Booking, error) {
draftedBookings := []*booking.Booking{}
if request == nil {
return draftedBookings, errors.New("no request found")
}
for _, exec := range scheduler.WorkflowExecution {
_, priceds, _, err := scheduler.Workflow.Planify(exec.ExecDate, exec.EndDate, request)
if err != nil {
return draftedBookings, errors.New("could not planify the workflow" + fmt.Sprintf("%v", err))
}
bookings := exec.Book(scheduler.UUID, scheduler.Workflow.UUID, priceds)
for _, booking := range bookings {
_, err := (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "",
tools.BOOKING, tools.POST, booking.Serialize(booking), request.Caller)
if err != nil {
return draftedBookings, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err))
}
draftedBookings = append(draftedBookings, booking)
}
}
return draftedBookings, nil
}
func (o *Order) Quantity() int {
return len(o.WorkflowExecutionIDs)
return len(o.Purchases) + len(o.Purchases)
}
func (d *Order) SetName() {
func (d *Order) SetName(_ string) {
d.Name = d.UUID + "_order_" + "_" + time.Now().UTC().Format("2006-01-02T15:04:05")
}
func (d *Order) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *Order) sumUpBill(request *tools.APIRequest) error {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return err
}
d.Total += b.Total
}
return nil
}
// TO FINISH
func (d *Order) pay(request *tools.APIRequest) error {
responses := make(chan *PeerOrder, len(d.SubOrders))
var wg *sync.WaitGroup
wg.Add(len(d.SubOrders))
for _, b := range d.SubOrders {
go b.Pay(request, responses, wg)
}
wg.Wait()
errs := ""
gotAnUnpaid := false
count := 0
for range responses {
res := <-responses
count++
if res != nil {
if res.Error != "" {
errs += res.Error
}
if res.Status != enum.PAID {
gotAnUnpaid = true
}
d.Status = enum.PARTIAL
d.SubOrders[res.GetID()] = res
if count == len(d.SubOrders) && !gotAnUnpaid {
d.Status = enum.PAID
}
}
}
if errs != "" {
return errors.New(errs)
}
return nil
}
type PeerOrder struct {
utils.AbstractObject
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING
go func() {
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER ON RETURN UPDATE STATUS
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID {
for _, b := range d.Items {
if !b.Item.IsPurchased() {
continue
}
accessor := purchase_resource.NewAccessor(request)
accessor.StoreOne(&purchase_resource.PurchaseResource{
ResourceID: b.Item.GetID(),
ResourceType: b.Item.GetType(),
EndDate: b.Item.GetLocationEnd(),
})
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPrice(request) // missing something
if err != nil {
return err
}
d.Total += tot
}
return nil
}
func (d *PeerOrder) AddItem(item pricing.PricedItemITF, quantity int) {
d.Items = append(d.Items, &PeerItemOrder{
Quantity: quantity,
Item: item,
})
}
func (d *PeerOrder) SetName() {
d.Name = d.UUID + "_order_" + d.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05")
}
type PeerItemOrder struct {
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item pricing.PricedItemITF `json:"item,omitempty" bson:"item,omitempty"`
}
func (d *PeerItemOrder) GetPrice(request *tools.APIRequest) (float64, error) {
accessor := purchase_resource.NewAccessor(request)
search, code, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: d.Item.GetID()}},
},
}, "", d.Purchase.IsDraft)
if code == 200 && len(search) > 0 {
for _, s := range search {
if s.(*purchase_resource.PurchaseResource).EndDate == nil || time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
return 0, nil
}
}
}
p, err := d.Item.GetPrice()
if err != nil {
return 0, err
}
return p * float64(d.Quantity), nil
}
// WTF HOW TO SELECT THE RIGHT PRICE ???
// SHOULD SET A BUYING STATUS WHEN PAYMENT IS VALIDATED

View File

@@ -1,64 +1,24 @@
package order
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/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
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
func NewAccessor(request *tools.APIRequest) *orderMongoAccessor {
return &orderMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type
Request: request,
Type: tools.ORDER,
AbstractAccessor: utils.AbstractAccessor[*Order]{
Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type
Request: request,
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 nil, 404, errors.New("Not implemented")
}
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

@@ -0,0 +1,92 @@
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

@@ -2,42 +2,63 @@ package peer
import (
"fmt"
"strings"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
// now write a go enum for the state partner with self, blacklist, partner
type PeerState int
type PeerRelation int
const (
NONE PeerState = iota
NONE PeerRelation = iota
SELF
PARTNER
BLACKLIST
PENDING_PARTNER
)
func (m PeerState) String() string {
return [...]string{"NONE", "SELF", "PARTNER", "BLACKLIST"}[m]
var path = []string{"unknown", "self", "partner", "blacklist", "partner"}
func GetRelationPath(str string) int {
for i, p := range path {
if str == p {
return i
}
}
return -1
}
func (m PeerState) EnumIndex() int {
func (m PeerRelation) Path() string {
return path[m]
}
func (m PeerRelation) String() string {
return strings.ToUpper(path[m])
}
func (m PeerRelation) EnumIndex() int {
return int(m)
}
// Peer is a struct that represents a peer
type Peer struct {
utils.AbstractObject
Url string `json:"url" bson:"url" validate:"required"` // Url is the URL of the peer (base64url)
Verify bool `json:"verify" bson:"verify"`
PeerID string `json:"peer_id" bson:"peer_id" validate:"required"`
APIUrl string `json:"api_url" bson:"api_url" validate:"required"` // Url is the URL of the peer (base64url)
StreamAddress string `json:"stream_address" bson:"stream_address" validate:"required"` // Url is the URL of the peer (base64url)
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
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"`
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
}
func (ao *Peer) VerifyAuth(request *tools.APIRequest) bool {
func (ao *Peer) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}
@@ -66,18 +87,8 @@ func (ao *Peer) RemoveExecution(exec PeerExecution) {
ao.FailedExecution = new
}
// IsMySelf checks if the peer is the local peer
func (p *Peer) IsMySelf() (bool, string) {
d, code, err := NewAccessor(nil).Search(nil, SELF.String(), p.IsDraft)
if code != 200 || err != nil || len(d) == 0 {
return false, ""
}
id := d[0].GetID()
return p.UUID == id, id
}
// LaunchPeerExecution launches an execution on a peer
func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (map[string]interface{}, error) {
p.UUID = peerID
return cache.LaunchPeerExecution(peerID, dataID, dt, method, body, caller) // Launch the execution on the peer through the cache
}

View File

@@ -28,40 +28,20 @@ type PeerCache struct {
}
// urlFormat formats the URL of the peer with the data type API function
func (p *PeerCache) urlFormat(hostUrl string, dt tools.DataType) string {
// localhost is replaced by the local peer URL
// because localhost must collide on a web request security protocol
/*localhost := ""
if strings.Contains(hostUrl, "localhost") {
localhost = "localhost"
}
if strings.Contains(hostUrl, "127.0.0.1") {
localhost = "127.0.0.1"
}
if localhost != "" {
r := regexp.MustCompile("(" + localhost + ":[0-9]+)")
t := r.FindString(hostUrl)
if t != "" {
hostUrl = strings.Replace(hostUrl, t, dt.API()+":8080/oc", -1)
} else {
hostUrl = strings.ReplaceAll(hostUrl, localhost, dt.API()+":8080/oc")
}
} else {*/
hostUrl = hostUrl + "/" + strings.ReplaceAll(dt.API(), "oc-", "")
//}
fmt.Println("Contacting", hostUrl)
return hostUrl
func urlFormat(hostUrl string, dt tools.DataType) string {
return hostUrl + "/" + strings.ReplaceAll(dt.String(), "oc-", "")
}
// checkPeerStatus checks the status of a peer
func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool) {
func CheckPeerStatus(peerID string, appName string) (*Peer, bool) {
api := tools.API{}
access := NewShallowAccessor()
res, code, _ := access.LoadOne(peerID) // Load the peer from db
if code != 200 { // no peer no party
return nil, false
}
url := p.urlFormat(res.(*Peer).Url, tools.PEER) + "/status" // Format the URL
url := urlFormat(res.(*Peer).APIUrl, tools.PEER) + "/status" // Format the URL
fmt.Println(url)
state, services := api.CheckRemotePeer(url)
res.(*Peer).ServicesState = services // Update the services states of the peer
access.UpdateOne(res, peerID) // Update the peer in the db
@@ -71,47 +51,47 @@ func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool)
// LaunchPeerExecution launches an execution on a peer
// The method contacts the path described by : peer.Url + datatype path (from enums) + replacement of id by dataID
func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
fmt.Println("Launching peer execution on", caller.URLS, dt, method)
methods := caller.URLS[dt] // Get the methods url of the data type
dt tools.DataType, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (map[string]interface{}, error) {
fmt.Println("Launching peer execution on", caller.GetUrls(), dt, method)
methods := caller.GetUrls()[dt] // Get the methods url of the data type
if m, ok := methods[method]; !ok || m == "" {
return nil, errors.New("Requested method " + method.String() + " not declared in HTTPCaller")
return map[string]interface{}{}, errors.New("Requested method " + method.String() + " not declared in HTTPCaller")
}
path := methods[method] // Get the path corresponding to the action we want to execute
path = strings.ReplaceAll(path, ":id", dataID) // Replace the id in the path in case of a DELETE / UPDATE method (it's a standard naming in OC)
url := ""
// Check the status of the peer
if mypeer, ok := p.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
pexec := &PeerExecution{
Method: method.String(),
Url: p.urlFormat((mypeer.Url), dt) + path, // the url is constitued of : host URL + resource path + action path (ex : mypeer.com/datacenter/resourcetype/path/to/action)
Url: urlFormat((mypeer.APIUrl), dt) + path, // the url is constitued of : host URL + resource path + action path (ex : mypeer.com/datacenter/resourcetype/path/to/action)
Body: body,
DataType: dt.EnumIndex(),
DataID: dataID,
}
mypeer.AddExecution(*pexec)
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
return nil, errors.New("peer is " + peerID + " not reachable")
return map[string]interface{}{}, errors.New("peer is " + peerID + " not reachable")
} else {
if mypeer == nil {
return nil, errors.New("peer " + peerID + " not found")
return map[string]interface{}{}, errors.New("peer " + peerID + " not found")
}
// If the peer is reachable, launch the execution
url = p.urlFormat((mypeer.Url), dt) + path // Format the URL
url = urlFormat((mypeer.APIUrl), dt) + path // Format the URL
tmp := mypeer.FailedExecution // Get the failed executions list
mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
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)
}
}
return nil, p.exec(url, method, body, caller) // Execute the method
return p.Exec(url, method, body, caller) // Execute the method
}
// exec executes the method on the peer
func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) error {
func (p *PeerCache) Exec(url string, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (map[string]interface{}, error) {
var b []byte
var err error
if method == tools.POST { // Execute the POST method if it's a POST method
@@ -123,16 +103,16 @@ func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, call
if method == tools.DELETE { // Execute the DELETE method if it's a DELETE method
b, err = caller.CallDelete(url, "")
}
if err != nil {
return err
}
var m map[string]interface{}
if err != nil {
return m, err
}
err = json.Unmarshal(b, &m)
if err != nil {
return err
return m, err
}
if e, ok := m["error"]; ok && e != "<nil>" && e != "" { // Check if there is an error in the response
return errors.New(fmt.Sprintf("%v", m["error"]))
return m, errors.New(fmt.Sprintf("%v", m["error"]))
}
return nil
return m, nil
}

View File

@@ -10,80 +10,51 @@ import (
)
type peerMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
overrideAuth bool
utils.AbstractAccessor[*Peer] // AbstractAccessor contains the basic fields of an accessor (model, caller)
OverrideAuth bool
}
// New creates a new instance of the peerMongoAccessor
func NewShallowAccessor() *peerMongoAccessor {
return &peerMongoAccessor{
overrideAuth: true,
AbstractAccessor: utils.AbstractAccessor{
OverrideAuth: true,
AbstractAccessor: utils.AbstractAccessor[*Peer]{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Type: tools.PEER,
New: func() *Peer { return &Peer{} },
},
}
}
func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
return &peerMongoAccessor{
overrideAuth: false,
AbstractAccessor: utils.AbstractAccessor{
OverrideAuth: false,
AbstractAccessor: utils.AbstractAccessor[*Peer]{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Request: request,
Type: tools.PEER,
New: func() *Peer { return &Peer{} },
},
}
}
func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool {
return !wfa.overrideAuth
return !wfa.OverrideAuth
}
/*
* Nothing special here, just the basic CRUD operations
*/
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 {
func (a *peerMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
if i, err := strconv.Atoi(search); err == nil {
m := map[string][]dbs.Filter{ // search by name if no filters are provided
"relation": {{Operator: dbs.EQUAL.String(), Value: i}},
}
if i == PARTNER.EnumIndex() {
m["verify"] = []dbs.Filter{{Operator: dbs.EQUAL.String(), Value: false}}
}
return &dbs.Filters{
Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"state": {{Operator: dbs.EQUAL.String(), Value: i}},
},
Or: m,
}
} else {
if search == "*" {
@@ -93,6 +64,7 @@ func (a *peerMongoAccessor) getDefaultFilter(search string) *dbs.Filters {
Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"url": {{Operator: dbs.LIKE.String(), Value: search}},
"peer_id": {{Operator: dbs.LIKE.String(), Value: search}},
},
}
}

View File

@@ -0,0 +1,109 @@
package peer_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- PeerRelation ----
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 TestPeerRelation_Path(t *testing.T) {
assert.Equal(t, "unknown", peer.NONE.Path())
assert.Equal(t, "self", peer.SELF.Path())
assert.Equal(t, "partner", peer.PARTNER.Path())
assert.Equal(t, "blacklist", peer.BLACKLIST.Path())
}
func TestPeerRelation_EnumIndex(t *testing.T) {
assert.Equal(t, 0, peer.NONE.EnumIndex())
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 TestGetRelationPath(t *testing.T) {
assert.Equal(t, 1, peer.GetRelationPath("self"))
assert.Equal(t, 2, peer.GetRelationPath("partner"))
assert.Equal(t, 3, peer.GetRelationPath("blacklist"))
assert.Equal(t, -1, peer.GetRelationPath("nonexistent"))
}
// ---- Peer model ----
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 TestPeer_CanDelete(t *testing.T) {
p := &peer.Peer{}
assert.False(t, p.CanDelete())
}
func TestPeer_GetAccessor(t *testing.T) {
p := &peer.Peer{}
req := &tools.APIRequest{}
acc := p.GetAccessor(req)
assert.NotNil(t, acc)
}
func TestPeer_AddExecution_Deduplication(t *testing.T) {
p := &peer.Peer{}
exec := peer.PeerExecution{Method: "POST", Url: "http://peer/data", Body: "body1"}
p.AddExecution(exec)
assert.Len(t, p.FailedExecution, 1)
// 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 TestPeer_RemoveExecution(t *testing.T) {
p := &peer.Peer{}
exec1 := peer.PeerExecution{Method: "POST", Url: "http://peer/a", Body: nil}
exec2 := peer.PeerExecution{Method: "DELETE", Url: "http://peer/b", Body: nil}
p.AddExecution(exec1)
p.AddExecution(exec2)
assert.Len(t, p.FailedExecution, 2)
p.RemoveExecution(exec1)
assert.Len(t, p.FailedExecution, 1)
assert.Equal(t, exec2, p.FailedExecution[0])
}
func TestPeer_RemoveExecution_NotFound(t *testing.T) {
p := &peer.Peer{}
exec := peer.PeerExecution{Method: "POST", Url: "http://peer/x", Body: nil}
p.AddExecution(exec)
other := peer.PeerExecution{Method: "DELETE", Url: "http://other/x", Body: nil}
p.RemoveExecution(other)
assert.Len(t, p.FailedExecution, 1) // unchanged
}
func TestPeer_RemoveExecution_Empty(t *testing.T) {
p := &peer.Peer{}
// Should not panic on empty list
exec := peer.PeerExecution{Method: "GET", Url: "http://peer/x", Body: nil}
p.RemoveExecution(exec)
assert.Empty(t, p.FailedExecution)
}

84
models/resources/compute.go Normal file → Executable file
View File

@@ -10,6 +10,7 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
/*
@@ -30,16 +31,18 @@ func (r *ComputeResource) GetType() string {
return tools.COMPUTE_RESOURCE.String()
}
func (abs *ComputeResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *ComputeResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.COMPUTE_RESOURCE {
return nil
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)
if err != nil {
return nil, err
}
p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
priced := p.(*PricedResource)
return &PricedComputeResource{
PricedResource: *priced,
}
}, nil
}
type ComputeNode struct {
@@ -61,9 +64,24 @@ type ComputeResourceInstance struct {
Nodes []*ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
}
func NewComputeResourceInstance(name string, peerID string) ResourceInstanceITF {
return &ComputeResourceInstance{
ResourceInstance: ResourceInstance[*ComputeResourcePartnership]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
type ComputeResourcePartnership struct {
ResourcePartnerShip[*ComputeResourcePricingProfile]
MaxAllowedCPUsCores map[string]int `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MinGaranteedCPUsCores map[string]float64 `json:"garanteed_cpus,omitempty" bson:"garanteed_cpus,omitempty"`
MinGaranteedGPUsMemoryGB map[string]float64 `json:"garanteed_gpus,omitempty" bson:"garanteed_gpus,omitempty"`
MinGaranteedRAMSize float64 `json:"garanteed_ram,omitempty" bson:"garanteed_ram,omitempty"`
MaxAllowedCPUsCores map[string]float64 `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MaxAllowedGPUsMemoryGB map[string]float64 `json:"allowed_gpus,omitempty" bson:"allowed_gpus,omitempty"`
MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"`
}
@@ -76,8 +94,19 @@ type ComputeResourcePricingProfile struct {
RAMPrice float64 `json:"ram_price" bson:"ram_price" default:"-1"` // RAMPrice is the price of the RAM
}
func (p *ComputeResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *ComputeResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *ComputeResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
return p.Pricing.BuyingStrategy
}
func (p *ComputeResourcePricingProfile) IsBooked() bool {
if p.Pricing.BuyingStrategy == pricing.PERMANENT {
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
}
func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
@@ -86,7 +115,7 @@ func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
// NOT A PROPER QUANTITY
// amountOfData is the number of CPUs, GPUs or RAM dependings on the params
func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, params ...string) (float64, error) {
func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, variation []*pricing.PricingVariation, params ...string) (float64, error) {
if len(params) < 1 {
return 0, errors.New("params must be set")
}
@@ -96,7 +125,7 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
if _, ok := p.CPUsPrices[model]; ok {
p.Pricing.Price = p.CPUsPrices[model]
}
r, err := p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
r, err := p.Pricing.GetPriceHT(amountOfData, explicitDuration, start, &end, variation)
if err != nil {
return 0, err
}
@@ -107,7 +136,7 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
if _, ok := p.GPUsPrices[model]; ok {
p.Pricing.Price = p.GPUsPrices[model]
}
r, err := p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
r, err := p.Pricing.GetPriceHT(amountOfData, explicitDuration, start, &end, variation)
if err != nil {
return 0, err
}
@@ -117,7 +146,7 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
if p.RAMPrice >= 0 {
p.Pricing.Price = p.RAMPrice
}
r, err := p.Pricing.GetPrice(float64(amountOfData), explicitDuration, start, &end)
r, err := p.Pricing.GetPriceHT(float64(amountOfData), explicitDuration, start, &end, variation)
if err != nil {
return 0, err
}
@@ -138,33 +167,36 @@ func (r *PricedComputeResource) GetType() tools.DataType {
return tools.COMPUTE_RESOURCE
}
func (r *PricedComputeResource) GetPrice() (float64, error) {
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
func (r *PricedComputeResource) GetPriceHT() (float64, error) {
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
now := time.Now()
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
price := float64(0)
for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} {
for model, amountOfData := range l {
cpus, err := pricing.GetPrice(float64(amountOfData), r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd, "cpus", model)
cpus, err := pricing.GetPriceHT(float64(amountOfData),
r.BookingConfiguration.ExplicitBookingDurationS, *r.BookingConfiguration.UsageStart,
*r.BookingConfiguration.UsageEnd, r.Variations, "cpus", model)
if err != nil {
return 0, err
}
price += cpus
}
}
ram, err := pricing.GetPrice(r.RAMLocated, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd, "ram")
ram, err := pricing.GetPriceHT(r.RAMLocated, r.BookingConfiguration.ExplicitBookingDurationS,
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations, "ram")
if err != nil {
return 0, err
}

73
models/resources/data.go Normal file → Executable file
View File

@@ -2,13 +2,13 @@ package resources
import (
"errors"
"fmt"
"time"
"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/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
/*
@@ -37,16 +37,18 @@ func (r *DataResource) GetType() string {
return tools.DATA_RESOURCE.String()
}
func (abs *DataResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.DATA_RESOURCE {
return nil
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)
if err != nil {
return nil, err
}
p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
priced := p.(*PricedResource)
return &PricedDataResource{
PricedResource: *priced,
}
}, nil
}
type DataInstance struct {
@@ -54,6 +56,17 @@ type DataInstance struct {
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
}
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
return &DataInstance{
ResourceInstance: ResourceInstance[*DataResourcePartnership]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
func (ri *DataInstance) StoreDraftDefault() {
found := false
for _, p := range ri.ResourceInstance.Env {
@@ -82,7 +95,7 @@ type DataResourcePartnership struct {
type DataResourcePricingStrategy int
const (
PER_DOWNLOAD DataResourcePricingStrategy = iota
PER_DOWNLOAD DataResourcePricingStrategy = iota + 6
PER_TB_DOWNLOADED
PER_GB_DOWNLOADED
PER_MB_DOWNLOADED
@@ -90,7 +103,9 @@ const (
)
func (t DataResourcePricingStrategy) String() string {
return [...]string{"PER DOWNLOAD", "PER TB DOWNLOADED", "PER GB DOWNLOADED", "PER MB DOWNLOADED", "PER KB DOWNLOADED"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DOWNLOAD", "PER TB DOWNLOADED", "PER GB DOWNLOADED", "PER MB DOWNLOADED", "PER KB DOWNLOADED"}...)
return l[t]
}
func DataResourcePricingStrategyList() []DataResourcePricingStrategy {
@@ -102,7 +117,9 @@ func ToDataResourcePricingStrategy(i int) DataResourcePricingStrategy {
}
func (t DataResourcePricingStrategy) GetStrategy() string {
return [...]string{"PER_DOWNLOAD", "PER_GB", "PER_MB", "PER_KB"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}...)
return l[t]
}
func (t DataResourcePricingStrategy) GetStrategyValue() int {
@@ -133,12 +150,13 @@ func (p *DataResourcePricingProfile) GetOverrideStrategyValue() int {
return p.Pricing.OverrideStrategy.GetStrategyValue()
}
func (p *DataResourcePricingProfile) GetPrice(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
func (p *DataResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *DataResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *DataResourcePricingProfile) IsBooked() bool {
// TODO WHAT ABOUT PAY PER USE... it's a complicate CASE
return p.Pricing.BuyingStrategy != pricing.PERMANENT
}
type PricedDataResource struct {
@@ -150,23 +168,22 @@ func (r *PricedDataResource) GetType() tools.DataType {
return tools.DATA_RESOURCE
}
func (r *PricedDataResource) GetPrice() (float64, error) {
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
func (r *PricedDataResource) GetPriceHT() (float64, error) {
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
now := time.Now()
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
var err error
amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 {
@@ -175,5 +192,7 @@ func (r *PricedDataResource) GetPrice() (float64, error) {
return 0, err
}
}
return pricing.GetPrice(amountOfData, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd)
return pricing.GetPriceHT(amountOfData, r.BookingConfiguration.ExplicitBookingDurationS,
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
}

15
models/resources/interfaces.go Normal file → Executable file
View File

@@ -1,6 +1,8 @@
package resources
import (
"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/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
@@ -9,11 +11,14 @@ import (
type ResourceInterface interface {
utils.DBObject
Trim()
ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF
FilterPeer(peerID string) *dbs.Filters
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)
GetType() string
GetSelectedInstance() utils.DBObject
ClearEnv() utils.DBObject
SetAllowedInstances(request *tools.APIRequest)
SetAllowedInstances(request *tools.APIRequest, instance_id ...string)
AddInstances(instance ResourceInstanceITF)
GetSelectedInstance(index *int) ResourceInstanceITF
}
type ResourceInstanceITF interface {
@@ -22,6 +27,8 @@ type ResourceInstanceITF interface {
GetName() string
StoreDraftDefault()
ClearEnv()
FilterInstance(peerID string)
GetProfile(peerID string, partnershipIndex *int, buying *int, strategy *int) pricing.PricingProfileITF
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string)
ClearPeerGroups()
@@ -31,4 +38,6 @@ type ResourcePartnerITF interface {
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() map[string][]string
ClearPeerGroups()
GetProfile(buying *int, strategy *int) pricing.PricingProfileITF
FilterPartnership(peerID string)
}

3
models/resources/models.go Normal file → Executable file
View File

@@ -11,12 +11,14 @@ type ResourceSet struct {
Processings []string `bson:"processings,omitempty" json:"processings,omitempty"`
Computes []string `bson:"computes,omitempty" json:"computes,omitempty"`
Workflows []string `bson:"workflows,omitempty" json:"workflows,omitempty"`
NativeTool []string `bson:"native,omitempty" json:"native,omitempty"`
DataResources []*DataResource `bson:"-" json:"data_resources,omitempty"`
StorageResources []*StorageResource `bson:"-" json:"storage_resources,omitempty"`
ProcessingResources []*ProcessingResource `bson:"-" json:"processing_resources,omitempty"`
ComputeResources []*ComputeResource `bson:"-" json:"compute_resources,omitempty"`
WorkflowResources []*WorkflowResource `bson:"-" json:"workflow_resources,omitempty"`
NativeTools []*NativeTool `bson:"-" json:"native_tools,omitempty"`
}
func (r *ResourceSet) Clear() {
@@ -62,4 +64,5 @@ type ItemResource struct {
Storage *StorageResource `bson:"storage,omitempty" json:"storage,omitempty"`
Compute *ComputeResource `bson:"compute,omitempty" json:"compute,omitempty"`
Workflow *WorkflowResource `bson:"workflow,omitempty" json:"workflow,omitempty"`
NativeTool *NativeTool `bson:"native_tools,omitempty" json:"native_tools,omitempty"`
}

View File

@@ -0,0 +1,77 @@
package resources
import (
"encoding/json"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"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/tools"
)
/*
* NativeT ools is a struct that represents Native Functionnality of OPENCLOUD
*/
type NativeTool struct {
AbstractResource
Kind int `json:"kind" bson:"kind" validate:"required"`
Params map[string]interface{}
}
func (d *NativeTool) SetName(name string) {
d.Name = name
}
func (d *NativeTool) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*NativeTool](tools.NATIVE_TOOL, request, func() utils.DBObject { return &NativeTool{} })
}
func (r *NativeTool) AddInstances(instance ResourceInstanceITF) {
}
func (r *NativeTool) GetType() string {
return tools.NATIVE_TOOL.String()
}
func (d *NativeTool) ClearEnv() utils.DBObject {
return d
}
func (d *NativeTool) Trim() {
/* EMPTY */
}
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) {
/* EMPTY */
}
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{
Name: w.Name,
Logo: w.Logo,
ResourceID: w.UUID,
ResourceType: t,
Quantity: 1,
CreatorID: w.CreatorID,
}, nil
}
func (r *NativeTool) GetSelectedInstance(selected *int) ResourceInstanceITF {
return nil
}
func InitNative() {
for _, kind := range []native_tools.NativeToolsEnum{native_tools.WORKFLOW_EVENT} {
newNative := &NativeTool{}
access := newNative.GetAccessor(&tools.APIRequest{Admin: true})
l, _, err := access.Search(nil, kind.String(), false)
if err != nil || len(l) == 0 {
newNative.Name = kind.String()
newNative.Kind = int(kind)
b, _ := json.Marshal(kind.Params())
var m map[string]interface{}
json.Unmarshal(b, &m)
newNative.Params = m
access.StoreOne(newNative)
}
}
}

View File

@@ -0,0 +1,23 @@
package native_tools
type NativeToolsEnum int
const (
WORKFLOW_EVENT NativeToolsEnum = iota
)
var Params = [...]interface{}{
WorkflowEventParams{},
}
var Str = [...]string{
"WORKFLOW_EVENT",
}
func (d NativeToolsEnum) Params() interface{} {
return Str[d]
}
func (d NativeToolsEnum) String() string {
return Str[d]
}

View File

@@ -0,0 +1,19 @@
package native_tools
import (
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
)
/*
* Workflow Event is a struct that represents a native functiunality.
*/
type WorkflowEventParams struct {
WorkflowResourceID string `json:"workflow_execution_id" bson:"workflow_execution_id" validate:"required"`
BookingMode *booking.BookingMode `json:"booking_mode" bson:"booking_mode"`
}
func (wep *WorkflowEventParams) GetBuyingStrategy() pricing.BillingStrategy {
return pricing.BILL_ONCE
}

128
models/resources/priced_resource.go Normal file → Executable file
View File

@@ -2,31 +2,55 @@ package resources
import (
"errors"
"fmt"
"time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/tools"
)
type BookingConfiguration struct {
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
Mode booking.BookingMode `json:"mode,omitempty" bson:"mode,omitempty"`
}
type PricedResource struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
PricingProfiles []pricing.PricingProfileITF `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
SelectedPricing *pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
SelectedPricing pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
BookingConfiguration *BookingConfiguration `json:"booking_configuration,omitempty" bson:"booking_configuration,omitempty"`
Variations []*pricing.PricingVariation `json:"pricing_variations" bson:"pricing_variations"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_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"`
}
func (abs *PricedResource) GetQuantity() int {
return abs.Quantity
}
func (abs *PricedResource) AddQuantity(amount int) {
abs.Quantity += amount
}
func (abs *PricedResource) SelectPricing() pricing.PricingProfileITF {
return abs.SelectedPricing
}
func (abs *PricedResource) GetID() string {
return abs.ResourceID
}
func (abs *PricedResource) GetInstanceID() string {
return abs.InstanceID
}
func (abs *PricedResource) GetType() tools.DataType {
return abs.ResourceType
}
@@ -35,59 +59,87 @@ func (abs *PricedResource) GetCreatorID() string {
return abs.CreatorID
}
func (abs *PricedResource) IsPurchased() bool {
func (abs *PricedResource) IsPurchasable() bool {
if abs.SelectedPricing == nil {
return false
}
return (*abs.SelectedPricing).IsPurchased()
return (abs.SelectedPricing).IsPurchasable()
}
func (abs *PricedResource) IsBooked() bool {
if abs.SelectedPricing == nil {
return false
}
return (abs.SelectedPricing).IsBooked()
}
func (abs *PricedResource) GetLocationEnd() *time.Time {
return abs.UsageEnd
if abs.BookingConfiguration == nil {
return nil
}
return abs.BookingConfiguration.UsageEnd
}
func (abs *PricedResource) GetLocationStart() *time.Time {
return abs.UsageStart
if abs.BookingConfiguration == nil {
return nil
}
return abs.BookingConfiguration.UsageStart
}
func (abs *PricedResource) SetLocationStart(start time.Time) {
abs.UsageStart = &start
if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{}
}
abs.BookingConfiguration.UsageStart = &start
}
func (abs *PricedResource) SetLocationEnd(end time.Time) {
abs.UsageEnd = &end
if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{}
}
abs.BookingConfiguration.UsageEnd = &end
}
func (abs *PricedResource) GetBookingMode() booking.BookingMode {
if abs.BookingConfiguration == nil {
return booking.WHEN_POSSIBLE
}
return abs.BookingConfiguration.Mode
}
func (abs *PricedResource) GetExplicitDurationInS() float64 {
if abs.ExplicitBookingDurationS == 0 {
if abs.UsageEnd == nil && abs.UsageStart == nil {
if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{}
}
if abs.BookingConfiguration.ExplicitBookingDurationS == 0 {
if abs.BookingConfiguration.UsageEnd == nil && abs.BookingConfiguration.UsageStart == nil {
return time.Duration(1 * time.Hour).Seconds()
}
if abs.UsageEnd == nil {
add := abs.UsageStart.Add(time.Duration(1 * time.Hour))
abs.UsageEnd = &add
if abs.BookingConfiguration.UsageEnd == nil {
add := abs.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour))
abs.BookingConfiguration.UsageEnd = &add
}
return abs.UsageEnd.Sub(*abs.UsageStart).Seconds()
return abs.BookingConfiguration.UsageEnd.Sub(*abs.BookingConfiguration.UsageStart).Seconds()
}
return abs.ExplicitBookingDurationS
return abs.BookingConfiguration.ExplicitBookingDurationS
}
func (r *PricedResource) GetPrice() (float64, error) {
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
func (r *PricedResource) GetPriceHT() (float64, error) {
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID)
}
pricing := *r.SelectedPricing
return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd)
pricing := r.SelectedPricing
return pricing.GetPriceHT(1, 0, *r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
}

36
models/resources/processing.go Normal file → Executable file
View File

@@ -8,6 +8,7 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
type ProcessingUsage struct {
@@ -26,6 +27,7 @@ type ProcessingUsage struct {
*/
type ProcessingResource struct {
AbstractInstanciatedResource[*ProcessingInstance]
IsEvent bool `json:"is_event,omitempty" bson:"is_event,omitempty"`
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
IsService bool `json:"is_service,omitempty" bson:"is_service,omitempty"` // IsService is a flag that indicates if the processing is a service
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"` // Usage is the usage of the processing
@@ -47,6 +49,21 @@ type ProcessingInstance struct {
Access *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
}
func NewProcessingInstance(name string, peerID string) ResourceInstanceITF {
return &ProcessingInstance{
ResourceInstance: ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
type ProcessingResourcePartnership struct {
ResourcePartnerShip[*ProcessingResourcePricingProfile]
}
type PricedProcessingResource struct {
PricedResource
IsService bool
@@ -57,16 +74,19 @@ func (r *PricedProcessingResource) GetType() tools.DataType {
}
func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
if a.ExplicitBookingDurationS == 0 {
if a.IsService || a.UsageStart == nil {
if a.BookingConfiguration == nil {
a.BookingConfiguration = &BookingConfiguration{}
}
if a.BookingConfiguration.ExplicitBookingDurationS == 0 {
if a.IsService || a.BookingConfiguration.UsageStart == nil {
if a.IsService {
return -1
}
return time.Duration(1 * time.Hour).Seconds()
}
return a.UsageEnd.Sub(*a.UsageStart).Seconds()
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
}
return a.ExplicitBookingDurationS
return a.BookingConfiguration.ExplicitBookingDurationS
}
func (d *ProcessingResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
@@ -77,10 +97,10 @@ 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
}
func (p *ProcessingResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *ProcessingResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *ProcessingResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.GetPrice(amountOfData, val, start, &end)
func (p *ProcessingResourcePricingProfile) IsBooked() bool {
return p.Pricing.BuyingStrategy != pricing.PERMANENT
}

View File

@@ -9,8 +9,15 @@ import (
type PurchaseResource struct {
utils.AbstractObject
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
DestPeerID string `json:"dest_peer_id" bson:"dest_peer_id"`
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
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
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"`
}

View File

@@ -3,23 +3,23 @@ package purchase_resource
import (
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"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 purchaseResourceMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
type PurchaseResourceMongoAccessor struct {
utils.AbstractAccessor[*PurchaseResource] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
return &purchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
return &PurchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*PurchaseResource]{
Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type
Request: request,
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
*/
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) {
return utils.GenericLoadOne[*PurchaseResource](id, func(d utils.DBObject) (utils.DBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (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) {
utils.GenericDeleteOne(id, a)
return nil, 404, nil
@@ -53,15 +37,7 @@ func (a *purchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int,
}, a)
}
func (a *purchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
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 {
func (a *PurchaseResourceMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(d.GetID(), a)

View File

@@ -0,0 +1,56 @@
package purchase_resource_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
func TestGetAccessor(t *testing.T) {
req := &tools.APIRequest{}
res := &purchase_resource.PurchaseResource{}
accessor := res.GetAccessor(req)
assert.NotNil(t, accessor)
assert.Equal(t, tools.PURCHASE_RESOURCE, accessor.(*purchase_resource.PurchaseResourceMongoAccessor).Type)
}
func TestCanUpdate(t *testing.T) {
set := &purchase_resource.PurchaseResource{ResourceID: "id"}
r := &purchase_resource.PurchaseResource{
AbstractObject: utils.AbstractObject{IsDraft: true},
}
can, updated := r.CanUpdate(set)
assert.True(t, can)
assert.Equal(t, set, updated)
r.IsDraft = false
can, _ = r.CanUpdate(set)
assert.False(t, can)
}
func TestCanDelete(t *testing.T) {
now := time.Now().UTC()
past := now.Add(-1 * time.Hour)
future := now.Add(1 * time.Hour)
t.Run("nil EndDate", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{}
assert.False(t, r.CanDelete())
})
t.Run("EndDate in past", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{EndDate: &past}
assert.True(t, r.CanDelete())
})
t.Run("EndDate in future", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{EndDate: &future}
assert.False(t, r.CanDelete())
})
}

295
models/resources/resource.go Normal file → Executable file
View File

@@ -1,33 +1,52 @@
package resources
import (
"encoding/json"
"errors"
"slices"
"cloud.o-forge.io/core/oc-lib/config"
"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/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"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/biter777/countries"
"github.com/google/uuid"
)
// AbstractResource is the struct containing all of the attributes commons to all ressources
type AbstractResource struct {
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
Logo string `json:"logo,omitempty" bson:"logo,omitempty" validate:"required"` // Logo is the logo 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
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"`
SelectedInstanceIndex *int `json:"selected_instance_index,omitempty" bson:"selected_instance_index,omitempty"` // SelectedInstance is the selected instance
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
Logo string `json:"logo,omitempty" bson:"logo,omitempty" validate:"required"` // Logo is the logo 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
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"`
AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"`
}
func (r *AbstractResource) GetSelectedInstance() utils.DBObject {
func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
return nil
}
func (r *AbstractResource) GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation {
if len(r.AllowedBookingModes) == 0 {
return map[booking.BookingMode]*pricing.PricingVariation{
booking.PLANNED: {
Percentage: 0,
}, booking.WHEN_POSSIBLE: {
Percentage: 0,
},
}
}
return r.AllowedBookingModes
}
func (r *AbstractResource) GetType() string {
return tools.INVALID.String()
}
@@ -48,27 +67,63 @@ func (r *AbstractResource) CanDelete() bool {
}
type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
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
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
}
func (abs *AbstractInstanciatedResource[T]) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *AbstractInstanciatedResource[T]) AddInstances(instance ResourceInstanceITF) {
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) {
instances := map[string]string{}
profiles := []pricing.PricingProfileITF{}
for _, instance := range abs.Instances {
instances[instance.GetID()] = instance.GetName()
profiles = instance.GetPricingsProfiles(request.PeerID, request.Groups)
var profile pricing.PricingProfileITF
var inst ResourceInstanceITF
if t := abs.GetSelectedInstance(selectedInstance); t != nil {
inst = t
instances[t.GetID()] = t.GetName()
profile = t.GetProfile(request.PeerID, selectedPartnership, selectedBuyingStrategy, selectedStrategy)
} else {
for i, instance := range abs.Instances { // TODO why it crush before ?
if i == 0 {
inst = instance
}
instances[instance.GetID()] = instance.GetName()
profiles := instance.GetPricingsProfiles(request.PeerID, request.Groups)
if len(profiles) > 0 {
profile = profiles[0]
break
}
}
}
if profile == nil {
/*if ok, _ := utils.IsMySelf(request.PeerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {*/
profile = pricing.GetDefaultPricingProfile()
/*} else {
return nil, errors.New("no pricing profile found")
}*/
}
variations := []*pricing.PricingVariation{}
if selectedBookingModeIndex != nil && abs.AllowedBookingModes[booking.BookingMode(*selectedBookingModeIndex)] != nil {
variations = append(variations, abs.AllowedBookingModes[booking.BookingMode(*selectedBookingModeIndex)])
}
return &PricedResource{
Name: abs.Name,
Logo: abs.Logo,
ResourceID: abs.UUID,
InstanceID: inst.GetID(),
ResourceType: t,
Quantity: 1,
InstancesRefs: instances,
PricingProfiles: profiles,
SelectedPricing: profile,
Variations: variations,
CreatorID: abs.CreatorID,
}
}, nil
}
func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
@@ -78,9 +133,9 @@ func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
return abs
}
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() utils.DBObject {
if r.SelectedInstanceIndex != nil && len(r.Instances) > *r.SelectedInstanceIndex {
return r.Instances[*r.SelectedInstanceIndex]
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) ResourceInstanceITF {
if selected != nil && len(r.Instances) > *selected {
return r.Instances[*selected]
}
if len(r.Instances) > 0 {
return r.Instances[0]
@@ -88,29 +143,34 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() utils.DBObject {
return nil
}
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest) {
if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" {
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) {
if (request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin {
return
}
abs.Instances = verifyAuthAction[T](abs.Instances, request)
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
}
func (d *AbstractInstanciatedResource[T]) Trim() {
d.Type = d.GetType()
if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: d.CreatorID}}).IsMySelf(); !ok {
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(request *tools.APIRequest) bool {
return len(verifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request)
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(callName string, request *tools.APIRequest) bool {
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{}
for _, instance := range baseInstance {
if len(instanceID) > 0 && !slices.Contains(instanceID, instance.GetID()) {
continue
}
_, peerGroups := instance.GetPeerGroups()
for _, peers := range peerGroups {
if request == nil {
@@ -118,11 +178,14 @@ func verifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.AP
}
if grps, ok := peers[request.PeerID]; ok || config.GetConfig().Whitelist {
if (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance)
// TODO filter Partners + Profiles...
continue
}
for _, grp := range grps {
if slices.Contains(request.Groups, grp) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance)
}
}
@@ -137,11 +200,6 @@ type GeoPoint struct {
Longitude float64 `json:"longitude,omitempty" bson:"longitude,omitempty"`
}
type Credentials struct {
Login string `json:"login,omitempty" bson:"login,omitempty"`
Pass string `json:"password,omitempty" bson:"password,omitempty"`
}
type ResourceInstance[T ResourcePartnerITF] struct {
utils.AbstractObject
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
@@ -150,7 +208,31 @@ type ResourceInstance[T ResourcePartnerITF] struct {
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,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"`
}
// TODO should kicks all selection
func NewInstance[T ResourcePartnerITF](name string) *ResourceInstance[T] {
return &ResourceInstance[T]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
Partnerships: []T{},
}
}
func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
partnerships := []T{}
for _, p := range ri.Partnerships {
if p.GetPeerGroups()[peerID] != nil {
p.FilterPartnership(peerID)
partnerships = append(partnerships, p)
}
}
ri.Partnerships = partnerships
}
func (ri *ResourceInstance[T]) ClearEnv() {
@@ -159,11 +241,31 @@ func (ri *ResourceInstance[T]) ClearEnv() {
ri.Outputs = []models.Param{}
}
func (ri *ResourceInstance[T]) GetProfile(peerID string, partnershipIndex *int, buyingIndex *int, strategyIndex *int) pricing.PricingProfileITF {
if partnershipIndex != nil && len(ri.Partnerships) > *partnershipIndex {
prts := ri.Partnerships[*partnershipIndex]
return prts.GetProfile(buyingIndex, strategyIndex)
}
if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {
return pricing.GetDefaultPricingProfile()
}
return nil
}
func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
pricings := []pricing.PricingProfileITF{}
for _, p := range ri.Partnerships {
pricings = append(pricings, p.GetPricingsProfiles(peerID, groups)...)
}
if len(pricings) == 0 {
if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {
pricings = append(pricings, pricing.GetDefaultPricingProfile())
}
}
return pricings
}
@@ -174,6 +276,20 @@ func (ri *ResourceInstance[T]) GetPeerGroups() ([]ResourcePartnerITF, []map[stri
partners = append(partners, p)
groups = append(groups, p.GetPeerGroups())
}
if len(groups) == 0 {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
}))
if err != nil || pp == nil {
return partners, groups
}
groups = []map[string][]string{
{
pp.GetID(): {"*"},
},
}
// TODO make allow all only for self.
}
return partners, groups
}
@@ -186,37 +302,118 @@ func (ri *ResourceInstance[T]) ClearPeerGroups() {
type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"`
PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
PricingProfiles []T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
PricingProfiles map[int]map[int]T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
// to upgrade pricing profiles. to be a map BuyingStrategy, map of Strategy
}
func (ri *ResourcePartnerShip[T]) FilterPartnership(peerID string) {
if ri.PeerGroups[peerID] == nil {
ri.PeerGroups = map[string][]string{}
} else {
ri.PeerGroups = map[string][]string{
peerID: ri.PeerGroups[peerID],
}
}
}
func (ri *ResourcePartnerShip[T]) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
if buying != nil && strategy != nil {
if strat, ok := ri.PricingProfiles[*buying]; ok {
if profile, ok := strat[*strategy]; ok {
return profile
}
}
}
return nil
}
/*
Le pricing doit être selectionné lors d'un scheduling...
le type de paiement défini le type de stratégie de paiement
note : il faut rajouté - une notion de facturation
Une order est l'ensemble de la commande... un booking une réservation, une purchase un acte d'achat.
Une bill (facture) représente alors... l'emission d'une facture à un instant T en but d'être honorée, envoyée ... etc.
*/
func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
profiles := []pricing.PricingProfileITF{}
if ri.PeerGroups[peerID] != nil {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
}
if slices.Contains(groups, "*") {
if ri.PeerGroups[peerID] == nil {
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) || slices.Contains(groups, "*") {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
for _, i := range ri {
profiles = append(profiles, i)
}
}
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
}
return profiles
}
}
if len(profiles) == 0 {
if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {
profiles = append(profiles, pricing.GetDefaultPricingProfile())
}
}
return profiles
}
func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string {
if len(rp.PeerGroups) == 0 {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
}))
if err != nil || pp == nil {
return rp.PeerGroups
}
return map[string][]string{
pp.GetID(): {"*"},
}
}
return rp.PeerGroups
}
func (rp *ResourcePartnerShip[T]) ClearPeerGroups() {
rp.PeerGroups = map[string][]string{}
}
func ToResource(
dt int,
payload []byte,
) (ResourceInterface, error) {
switch dt {
case tools.PROCESSING_RESOURCE.EnumIndex():
var data ProcessingResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.WORKFLOW_RESOURCE.EnumIndex():
var data WorkflowResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.DATA_RESOURCE.EnumIndex():
var data DataResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.STORAGE_RESOURCE.EnumIndex():
var data StorageResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.COMPUTE_RESOURCE.EnumIndex():
var data ComputeResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
}
return nil, errors.New("can't found any data resources matching")
}

88
models/resources/resource_accessor.go Normal file → Executable file
View File

@@ -1,6 +1,7 @@
package resources
import (
"errors"
"slices"
"cloud.o-forge.io/core/oc-lib/dbs"
@@ -9,21 +10,42 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
type resourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject
type ResourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject
}
// New creates a new instance of the computeMongoAccessor
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *resourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE, tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE}, t) {
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *ResourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{
tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE,
tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE,
tools.DATA_RESOURCE, tools.NATIVE_TOOL,
}, t) {
return nil
}
return &resourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{
return &ResourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor[ResourceInterface]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
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,
}
@@ -32,53 +54,57 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
/*
* 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 utils.DBObject, id string) (utils.DBObject, int, error) {
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")
}
set.(T).Trim()
return utils.GenericUpdateOne(set, id, dca, dca.generateData())
if d, c, err := utils.GenericUpdateOne(set, id, dca, dca.generateData()); err != nil {
return d, c, err
} else {
return utils.GenericUpdateOne(set, id, dca, dca.generateData())
}
}
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 {
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)
}
func (dca *resourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't copy/publish a non existing computing units resource not reported onto compute units catalog")
}
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) {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
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) {
if filters == nil && search == "*" {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request)
return d
}, 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 {
d.(T).SetAllowedInstances(wfa.Request)
return d
}, 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{
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}},

77
models/resources/storage.go Normal file → Executable file
View File

@@ -10,6 +10,7 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
/*
@@ -30,22 +31,24 @@ func (r *StorageResource) GetType() string {
return tools.STORAGE_RESOURCE.String()
}
func (abs *StorageResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *StorageResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.STORAGE_RESOURCE {
return nil
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)
if err != nil {
return nil, err
}
p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
priced := p.(*PricedResource)
return &PricedStorageResource{
PricedResource: *priced,
}
}, nil
}
type StorageResourceInstance struct {
ResourceInstance[*StorageResourcePartnership]
Credentials *Credentials `json:"credentials,omitempty" bson:"credentials,omitempty"`
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
Path string `bson:"path,omitempty" json:"path,omitempty"` // Path is the store folders in the source
Local bool `bson:"local" json:"local"`
SecurityLevel string `bson:"security_level,omitempty" json:"security_level,omitempty"`
SizeType enum.StorageSize `bson:"size_type" json:"size_type" default:"0"` // SizeType is the type of the storage size
@@ -55,8 +58,18 @@ type StorageResourceInstance struct {
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
}
func NewStorageResourceInstance(name string, peerID string) ResourceInstanceITF {
return &StorageResourceInstance{
ResourceInstance: ResourceInstance[*StorageResourcePartnership]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
func (ri *StorageResourceInstance) ClearEnv() {
ri.Credentials = nil
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
@@ -105,7 +118,7 @@ func (t PrivilegeStoragePricingStrategy) String() string {
type StorageResourcePricingStrategy int
const (
PER_DATA_STORED StorageResourcePricingStrategy = iota
PER_DATA_STORED StorageResourcePricingStrategy = iota + 6
PER_TB_STORED
PER_GB_STORED
PER_MB_STORED
@@ -117,11 +130,15 @@ func StorageResourcePricingStrategyList() []StorageResourcePricingStrategy {
}
func (t StorageResourcePricingStrategy) String() string {
return [...]string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}...)
return l[t]
}
func (t StorageResourcePricingStrategy) GetStrategy() string {
return [...]string{"PER_DATA_STORED", "PER_GB_STORED", "PER_MB_STORED", "PER_KB_STORED"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}...)
return l[t]
}
func (t StorageResourcePricingStrategy) GetStrategyValue() int {
@@ -152,12 +169,15 @@ type StorageResourcePricingProfile struct {
pricing.ExploitPricingProfile[StorageResourcePricingStrategy] // ExploitPricingProfile is the pricing profile of a storage it means that we exploit the resource for an amount of continuous time
}
func (p *StorageResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *StorageResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *StorageResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.GetPrice(amountOfData, val, start, &end)
func (p *StorageResourcePricingProfile) IsBooked() bool {
if p.Pricing.BuyingStrategy == pricing.PERMANENT {
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
}
type PricedStorageResource struct {
@@ -169,23 +189,23 @@ func (r *PricedStorageResource) GetType() tools.DataType {
return tools.STORAGE_RESOURCE
}
func (r *PricedStorageResource) GetPrice() (float64, error) {
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
func (r *PricedStorageResource) GetPriceHT() (float64, error) {
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
fmt.Println("GetPriceHT", r.BookingConfiguration.UsageStart, r.BookingConfiguration.UsageEnd)
now := time.Now()
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
var err error
amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 {
@@ -194,5 +214,6 @@ func (r *PricedStorageResource) GetPrice() (float64, error) {
return 0, err
}
}
return pricing.GetPrice(amountOfData, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd)
return pricing.GetPriceHT(amountOfData, r.BookingConfiguration.ExplicitBookingDurationS,
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
}

View File

@@ -0,0 +1,111 @@
package resources_test
import (
"testing"
"time"
"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/resources"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestComputeResource_GetType(t *testing.T) {
r := &resources.ComputeResource{}
assert.Equal(t, tools.COMPUTE_RESOURCE.String(), r.GetType())
}
func TestComputeResource_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
cr := &resources.ComputeResource{}
accessor := cr.GetAccessor(req)
assert.NotNil(t, accessor)
}
func TestComputeResource_ConvertToPricedResource(t *testing.T) {
req := &tools.APIRequest{}
cr := &resources.ComputeResource{}
cr.UUID = "comp123"
cr.AbstractInstanciatedResource.UUID = cr.UUID
result, _ := cr.ConvertToPricedResource(tools.COMPUTE_RESOURCE, nil, nil, nil, nil, nil, req)
assert.NotNil(t, result)
assert.IsType(t, &resources.PricedComputeResource{}, result)
}
func TestComputeResourcePricingProfile_GetPriceHT_CPUs(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
profile := 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},
},
},
}
price, err := profile.GetPriceHT(2, 3600, start, end, []*pricing.PricingVariation{}, "cpus", "Xeon")
require.NoError(t, err)
assert.Greater(t, price, float64(0))
}
func TestComputeResourcePricingProfile_GetPriceHT_InvalidParams(t *testing.T) {
profile := resources.ComputeResourcePricingProfile{}
_, err := profile.GetPriceHT(1, 3600, time.Now(), time.Now(), []*pricing.PricingVariation{})
assert.Error(t, err)
assert.Equal(t, "params must be set", err.Error())
}
func TestPricedComputeResource_GetPriceHT(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
r := resources.PricedComputeResource{
PricedResource: resources.PricedResource{
ResourceID: "comp456",
SelectedPricing: &MockPricingProfile{ReturnCost: 1.0},
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
ExplicitBookingDurationS: 3600,
},
},
CPUsLocated: map[string]float64{"Xeon": 2},
GPUsLocated: map[string]float64{"Tesla": 1},
RAMLocated: 4,
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Greater(t, price, float64(0))
}
func TestPricedComputeResource_GetPriceHT_MissingProfile(t *testing.T) {
r := resources.PricedComputeResource{
PricedResource: resources.PricedResource{
ResourceID: "comp789",
},
}
_, err := r.GetPriceHT()
require.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
}
func TestPricedComputeResource_FillWithDefaultProcessingUsage(t *testing.T) {
usage := &resources.ProcessingUsage{
CPUs: map[string]*models.CPU{"t": {Model: "Xeon", Cores: 4}},
GPUs: map[string]*models.GPU{"t1": {Model: "Tesla"}},
RAM: &models.RAM{SizeGb: 16},
}
r := &resources.PricedComputeResource{
CPUsLocated: make(map[string]float64),
GPUsLocated: make(map[string]float64),
RAMLocated: 0,
}
r.FillWithDefaultProcessingUsage(usage)
assert.Equal(t, float64(4), r.CPUsLocated["Xeon"])
assert.Equal(t, float64(1), r.GPUsLocated["Tesla"])
assert.Equal(t, float64(16), r.RAMLocated)
}

View File

@@ -0,0 +1,122 @@
package resources_test
import (
"testing"
"time"
"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/resources"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDataResource_GetType(t *testing.T) {
d := &resources.DataResource{}
assert.Equal(t, tools.DATA_RESOURCE.String(), d.GetType())
}
func TestDataResource_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
acc := (&resources.DataResource{}).GetAccessor(req)
assert.NotNil(t, acc)
}
func TestDataResource_ConvertToPricedResource(t *testing.T) {
d := &resources.DataResource{}
d.UUID = "123"
res, _ := d.ConvertToPricedResource(tools.DATA_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.IsType(t, &resources.PricedDataResource{}, res)
nilRes, _ := d.ConvertToPricedResource(tools.PROCESSING_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.Nil(t, nilRes)
}
func TestDataInstance_StoreDraftDefault(t *testing.T) {
di := &resources.DataInstance{
Source: "test-src",
ResourceInstance: resources.ResourceInstance[*resources.DataResourcePartnership]{
Env: []models.Param{},
},
}
di.StoreDraftDefault()
assert.Len(t, di.ResourceInstance.Env, 1)
assert.Equal(t, "source", di.ResourceInstance.Env[0].Attr)
assert.Equal(t, "test-src", di.ResourceInstance.Env[0].Value)
// Call again, should not duplicate
di.StoreDraftDefault()
assert.Len(t, di.ResourceInstance.Env, 1)
}
func TestDataResourcePricingStrategy_GetQuantity(t *testing.T) {
tests := []struct {
strategy resources.DataResourcePricingStrategy
input float64
expected float64
}{
{resources.PER_DOWNLOAD, 1, 1},
{resources.PER_TB_DOWNLOADED, 1, 1000},
{resources.PER_GB_DOWNLOADED, 2.5, 2.5},
{resources.PER_MB_DOWNLOADED, 1, 0.001},
{resources.PER_KB_DOWNLOADED, 1, 0.000001},
}
for _, tt := range tests {
q, err := tt.strategy.GetQuantity(tt.input)
require.NoError(t, err)
assert.InDelta(t, tt.expected, q, 1e-9)
}
_, err := resources.DataResourcePricingStrategy(999).GetQuantity(1)
assert.Error(t, err)
}
func TestDataResourcePricingProfile_IsPurchased(t *testing.T) {
profile := &resources.DataResourcePricingProfile{}
profile.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
assert.True(t, profile.IsPurchasable())
}
func TestPricedDataResource_GetPriceHT(t *testing.T) {
now := time.Now()
later := now.Add(1 * time.Hour)
mockPrice := 42.0
pricingProfile := &resources.DataResourcePricingProfile{AccessPricingProfile: pricing.AccessPricingProfile[resources.DataResourcePricingStrategy]{
Pricing: pricing.PricingStrategy[resources.DataResourcePricingStrategy]{Price: 42.0}},
}
pricingProfile.Pricing.OverrideStrategy = resources.PER_GB_DOWNLOADED
r := &resources.PricedDataResource{
PricedResource: resources.PricedResource{
SelectedPricing: pricingProfile,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &now,
UsageEnd: &later,
},
},
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Equal(t, mockPrice, price)
}
func TestPricedDataResource_GetPriceHT_NoProfiles(t *testing.T) {
r := &resources.PricedDataResource{
PricedResource: resources.PricedResource{
ResourceID: "test-resource",
},
}
_, err := r.GetPriceHT()
assert.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
}
func TestPricedDataResource_GetType(t *testing.T) {
r := &resources.PricedDataResource{}
assert.Equal(t, tools.DATA_RESOURCE, r.GetType())
}

View File

@@ -0,0 +1,149 @@
package resources_test
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
// ---- Mock PricingProfile ----
type MockPricingProfile struct {
pricing.PricingProfileITF
Purchased bool
ReturnErr bool
ReturnCost float64
}
func (m *MockPricingProfile) IsPurchasable() bool {
return m.Purchased
}
func (m *MockPricingProfile) GetPriceHT(amount float64, explicitDuration float64, start time.Time, end time.Time, variations []*pricing.PricingVariation, _ ...string) (float64, error) {
if m.ReturnErr {
return 0, errors.New("mock error")
}
return m.ReturnCost, nil
}
// ---- Tests ----
func TestGetIDAndCreatorAndType(t *testing.T) {
r := resources.PricedResource{
ResourceID: "res-123",
CreatorID: "user-abc",
ResourceType: tools.DATA_RESOURCE,
}
assert.Equal(t, "res-123", r.GetID())
assert.Equal(t, "user-abc", r.GetCreatorID())
assert.Equal(t, tools.DATA_RESOURCE, r.GetType())
}
func TestIsPurchased(t *testing.T) {
t.Run("nil selected pricing returns false", func(t *testing.T) {
r := &resources.PricedResource{}
assert.False(t, r.IsPurchasable())
})
t.Run("returns true if pricing profile is purchased", func(t *testing.T) {
mock := &MockPricingProfile{Purchased: true}
r := &resources.PricedResource{SelectedPricing: mock}
assert.True(t, r.IsPurchasable())
})
}
func TestGetAndSetLocationStartEnd(t *testing.T) {
r := &resources.PricedResource{}
now := time.Now()
r.SetLocationStart(now)
r.SetLocationEnd(now.Add(2 * time.Hour))
assert.Equal(t, now, *r.GetLocationStart())
assert.Equal(t, now.Add(2*time.Hour), *r.GetLocationEnd())
}
func TestGetExplicitDurationInS(t *testing.T) {
t.Run("uses explicit duration if set", func(t *testing.T) {
r := &resources.PricedResource{BookingConfiguration: &resources.BookingConfiguration{
ExplicitBookingDurationS: 3600,
},
}
assert.Equal(t, 3600.0, r.GetExplicitDurationInS())
})
t.Run("computes duration from start and end", func(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
r := &resources.PricedResource{
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start, UsageEnd: &end,
},
}
assert.InDelta(t, 7200.0, r.GetExplicitDurationInS(), 0.1)
})
t.Run("defaults to 1 hour when times not set", func(t *testing.T) {
r := &resources.PricedResource{}
assert.InDelta(t, 3600.0, r.GetExplicitDurationInS(), 0.1)
})
}
func TestGetPriceHT(t *testing.T) {
t.Run("returns error if no pricing profile", func(t *testing.T) {
r := &resources.PricedResource{ResourceID: "no-profile"}
price, err := r.GetPriceHT()
require.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
assert.Equal(t, 0.0, price)
})
t.Run("defaults BookingConfiguration when nil", func(t *testing.T) {
mock := &MockPricingProfile{ReturnCost: 42.0}
r := &resources.PricedResource{
SelectedPricing: mock,
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Equal(t, 42.0, price)
})
t.Run("returns error if profile GetPriceHT fails", func(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
mock := &MockPricingProfile{ReturnErr: true}
r := &resources.PricedResource{
SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
},
}
price, err := r.GetPriceHT()
require.Error(t, err)
assert.Equal(t, 0.0, price)
})
t.Run("uses SelectedPricing if set", func(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
mock := &MockPricingProfile{ReturnCost: 10.0}
r := &resources.PricedResource{
SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
},
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Equal(t, 10.0, price)
})
}

View File

@@ -0,0 +1,113 @@
package resources_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"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"
"github.com/stretchr/testify/assert"
)
func TestProcessingResource_GetType(t *testing.T) {
r := &ProcessingResource{}
assert.Equal(t, tools.PROCESSING_RESOURCE.String(), r.GetType())
}
func TestPricedProcessingResource_GetType(t *testing.T) {
r := &PricedProcessingResource{}
assert.Equal(t, tools.PROCESSING_RESOURCE, r.GetType())
}
func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) {
now := time.Now()
after := now.Add(2 * time.Hour)
tests := []struct {
name string
input PricedProcessingResource
expected float64
}{
{
name: "Service without explicit duration",
input: PricedProcessingResource{
IsService: true,
},
expected: -1,
},
{
name: "Nil start time, non-service",
input: PricedProcessingResource{
PricedResource: PricedResource{
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: nil,
},
},
},
expected: float64((1 * time.Hour).Seconds()),
},
{
name: "Duration computed from start and end",
input: PricedProcessingResource{
PricedResource: PricedResource{
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &now,
UsageEnd: &after,
},
},
},
expected: float64((2 * time.Hour).Seconds()),
},
{
name: "Explicit duration takes precedence",
input: PricedProcessingResource{
PricedResource: PricedResource{
BookingConfiguration: &resources.BookingConfiguration{
ExplicitBookingDurationS: 1337,
},
},
},
expected: 1337,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, test.input.GetExplicitDurationInS())
})
}
}
func TestProcessingResource_GetAccessor(t *testing.T) {
request := &tools.APIRequest{}
r := &ProcessingResource{}
acc := r.GetAccessor(request)
assert.NotNil(t, acc)
}
func TestProcessingResourcePricingProfile_GetPriceHT(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
mockPricing := pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{
Price: 100.0,
},
}
profile := &ProcessingResourcePricingProfile{AccessPricingProfile: mockPricing}
price, err := profile.GetPriceHT(0, 0, start, end, []*pricing.PricingVariation{})
assert.NoError(t, err)
assert.Equal(t, 100.0, price)
}
func TestProcessingResourcePricingProfile_IsPurchased(t *testing.T) {
purchased := &ProcessingResourcePricingProfile{
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{
BuyingStrategy: pricing.PERMANENT,
},
},
}
assert.True(t, purchased.IsPurchasable())
}

View File

@@ -0,0 +1,117 @@
package resources_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
type MockInstance struct {
ID string
Name string
resources.ResourceInstance[*MockPartner]
}
func (m *MockInstance) GetID() string { return m.ID }
func (m *MockInstance) GetName() string { return m.Name }
func (m *MockInstance) ClearEnv() {}
func (m *MockInstance) ClearPeerGroups() {}
func (m *MockPartner) FilterPartnership(peerID string) {}
func (m *MockInstance) GetProfile(peerID string, a *int, b *int, c *int) pricing.PricingProfileITF {
return nil
}
func (m *MockInstance) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
return nil
}
func (m *MockInstance) GetPeerGroups() ([]resources.ResourcePartnerITF, []map[string][]string) {
return nil, []map[string][]string{
{"peer1": {"group1"}},
}
}
type MockPartner struct {
groups map[string][]string
}
func (m *MockPartner) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
return nil
}
func (m *MockPartner) GetPeerGroups() map[string][]string {
return m.groups
}
func (m *MockPartner) ClearPeerGroups() {}
func (m *MockPartner) GetPricingsProfiles(string, []string) []pricing.PricingProfileITF {
return nil
}
type MockDBObject struct {
utils.AbstractObject
isDraft bool
}
func (m *MockDBObject) IsDrafted() bool {
return m.isDraft
}
func TestGetSelectedInstance_WithValidIndex(t *testing.T) {
index := 1
inst1 := &MockInstance{ID: "1"}
inst2 := &MockInstance{ID: "2"}
resource := &resources.AbstractInstanciatedResource[*MockInstance]{
AbstractResource: resources.AbstractResource{},
Instances: []*MockInstance{inst1, inst2},
}
result := resource.GetSelectedInstance(&index)
assert.Equal(t, inst2, result)
}
func TestGetSelectedInstance_NoIndex(t *testing.T) {
inst := &MockInstance{ID: "1"}
resource := &resources.AbstractInstanciatedResource[*MockInstance]{
Instances: []*MockInstance{inst},
}
result := resource.GetSelectedInstance(nil)
assert.Equal(t, inst, result)
}
func TestCanUpdate_WhenOnlyStateDiffers(t *testing.T) {
resource := &resources.AbstractResource{AbstractObject: utils.AbstractObject{IsDraft: false}}
set := &MockDBObject{isDraft: true}
canUpdate, updated := resource.CanUpdate(set)
assert.True(t, canUpdate)
assert.Equal(t, set, updated)
}
func TestVerifyAuthAction_WithMatchingGroup(t *testing.T) {
inst := &MockInstance{
ResourceInstance: resources.ResourceInstance[*MockPartner]{
Partnerships: []*MockPartner{
{groups: map[string][]string{"peer1": {"group1"}}},
},
},
}
req := &tools.APIRequest{PeerID: "peer1", Groups: []string{"group1"}}
result := resources.VerifyAuthAction([]*MockInstance{inst}, req)
assert.Len(t, result, 1)
}
type FakeResource struct {
resources.AbstractInstanciatedResource[*MockInstance]
}
func (f *FakeResource) Trim() {}
func (f *FakeResource) SetAllowedInstances(*tools.APIRequest, ...string) {}
func (f *FakeResource) VerifyAuth(string, *tools.APIRequest) bool { return true }
func TestNewAccessor_ReturnsValid(t *testing.T) {
acc := resources.NewAccessor[*FakeResource](tools.COMPUTE_RESOURCE, &tools.APIRequest{}, func() utils.DBObject {
return &FakeResource{}
})
assert.NotNil(t, acc)
}

View File

@@ -0,0 +1,105 @@
package resources_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources"
)
func TestStorageResource_GetType(t *testing.T) {
res := &resources.StorageResource{}
assert.Equal(t, tools.STORAGE_RESOURCE.String(), res.GetType())
}
func TestStorageResource_GetAccessor(t *testing.T) {
res := &resources.StorageResource{}
req := &tools.APIRequest{}
accessor := res.GetAccessor(req)
assert.NotNil(t, accessor)
}
func TestStorageResource_ConvertToPricedResource_ValidType(t *testing.T) {
res := &resources.StorageResource{}
res.AbstractInstanciatedResource.CreatorID = "creator"
res.AbstractInstanciatedResource.UUID = "res-id"
priced, _ := res.ConvertToPricedResource(tools.STORAGE_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.NotNil(t, priced)
assert.IsType(t, &resources.PricedStorageResource{}, priced)
}
func TestStorageResource_ConvertToPricedResource_InvalidType(t *testing.T) {
res := &resources.StorageResource{}
priced, _ := res.ConvertToPricedResource(tools.COMPUTE_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.Nil(t, priced)
}
func TestStorageResourceInstance_ClearEnv(t *testing.T) {
inst := &resources.StorageResourceInstance{
ResourceInstance: resources.ResourceInstance[*resources.StorageResourcePartnership]{
Env: []models.Param{{Attr: "A"}},
Inputs: []models.Param{{Attr: "B"}},
Outputs: []models.Param{{Attr: "C"}},
},
}
inst.ClearEnv()
assert.Empty(t, inst.Env)
assert.Empty(t, inst.Inputs)
assert.Empty(t, inst.Outputs)
}
func TestStorageResourceInstance_StoreDraftDefault(t *testing.T) {
inst := &resources.StorageResourceInstance{
Source: "my-source",
ResourceInstance: resources.ResourceInstance[*resources.StorageResourcePartnership]{
Env: []models.Param{},
},
}
inst.StoreDraftDefault()
assert.Len(t, inst.Env, 1)
assert.Equal(t, "source", inst.Env[0].Attr)
assert.Equal(t, "my-source", inst.Env[0].Value)
assert.True(t, inst.Env[0].Readonly)
}
func TestStorageResourcePricingStrategy_GetQuantity(t *testing.T) {
tests := []struct {
strategy resources.StorageResourcePricingStrategy
dataGB float64
expect float64
}{
{resources.PER_DATA_STORED, 1.2, 1.2},
{resources.PER_TB_STORED, 1.2, 1200},
{resources.PER_GB_STORED, 2.5, 2.5},
{resources.PER_MB_STORED, 1.0, 1000},
{resources.PER_KB_STORED, 0.1, 100000},
}
for _, tt := range tests {
q, err := tt.strategy.GetQuantity(tt.dataGB)
assert.NoError(t, err)
assert.Equal(t, tt.expect, q)
}
}
func TestStorageResourcePricingStrategy_GetQuantity_Invalid(t *testing.T) {
invalid := resources.StorageResourcePricingStrategy(99)
q, err := invalid.GetQuantity(1.0)
assert.Error(t, err)
assert.Equal(t, 0.0, q)
}
func TestPricedStorageResource_GetPriceHT_NoProfiles(t *testing.T) {
res := &resources.PricedStorageResource{
PricedResource: resources.PricedResource{
ResourceID: "res-id",
},
}
_, err := res.GetPriceHT()
assert.Error(t, err)
}

View File

@@ -0,0 +1,62 @@
package resources_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources"
)
func TestWorkflowResource_GetType(t *testing.T) {
w := &resources.WorkflowResource{}
assert.Equal(t, tools.WORKFLOW_RESOURCE.String(), w.GetType())
}
func TestWorkflowResource_ConvertToPricedResource(t *testing.T) {
w := &resources.WorkflowResource{
AbstractResource: resources.AbstractResource{
AbstractObject: utils.AbstractObject{
Name: "Test Workflow",
UUID: "workflow-uuid",
CreatorID: "creator-id",
},
Logo: "logo.png",
},
}
req := &tools.APIRequest{
PeerID: "peer-1",
Groups: []string{"group1"},
}
pr, _ := w.ConvertToPricedResource(tools.WORKFLOW_RESOURCE, nil, nil, nil, nil, nil, req)
assert.Equal(t, "creator-id", pr.GetCreatorID())
assert.Equal(t, tools.WORKFLOW_RESOURCE, pr.GetType())
}
func TestWorkflowResource_ClearEnv(t *testing.T) {
w := &resources.WorkflowResource{}
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) {
w := &resources.WorkflowResource{}
w.SetAllowedInstances(&tools.APIRequest{})
// no-op; just confirm no crash
}
func TestWorkflowResource_GetAccessor(t *testing.T) {
w := &resources.WorkflowResource{}
request := &tools.APIRequest{}
accessor := w.GetAccessor(request)
assert.NotNil(t, accessor)
}

19
models/resources/workflow.go Normal file → Executable file
View File

@@ -16,7 +16,10 @@ type WorkflowResource struct {
}
func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ComputeResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
}
func (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) {
}
func (r *WorkflowResource) GetType() string {
@@ -30,17 +33,23 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
func (d *WorkflowResource) Trim() {
/* EMPTY */
}
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest) {
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) {
/* EMPTY */
}
func (w *WorkflowResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
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) {
return &PricedResource{
Name: w.Name,
Logo: w.Logo,
ResourceID: w.UUID,
ResourceType: t,
Quantity: 1,
CreatorID: w.CreatorID,
}
}, nil // TODO ???
}
// TODO : as instanciated resource !

View File

@@ -0,0 +1,38 @@
package models
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
func TestModel_ReturnsValidInstances(t *testing.T) {
for name := range models.ModelsCatalog {
t.Run(name, func(t *testing.T) {
modelInt := tools.FromString(name)
obj := models.Model(modelInt)
assert.NotNil(t, obj, "Model() returned nil for valid model name %s", name)
})
}
}
func TestModel_UnknownModelReturnsNil(t *testing.T) {
invalidModelInt := -9999 // unlikely to be valid
obj := models.Model(invalidModelInt)
assert.Nil(t, obj)
}
func TestGetModelsNames_ReturnsAllKeys(t *testing.T) {
names := models.GetModelsNames()
assert.Len(t, names, len(models.ModelsCatalog))
seen := make(map[string]bool)
for _, name := range names {
seen[name] = true
}
for key := range models.ModelsCatalog {
assert.Contains(t, seen, key)
}
}

122
models/utils/abstracts.go Normal file → Executable file
View File

@@ -1,7 +1,10 @@
package utils
import (
"crypto/sha256"
"encoding/json"
"errors"
"slices"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
@@ -37,12 +40,47 @@ type AbstractObject struct {
UpdaterID string `json:"updater_id,omitempty" bson:"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"`
Signature []byte `bson:"signature,omitempty" json:"signature,omitempty"`
}
func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor {
return nil
}
func (r *AbstractObject) Unsign() {
r.Signature = nil
}
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) {
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) {
r.Name = name
}
func (r *AbstractObject) GenerateID() {
if r.UUID == "" {
r.UUID = uuid.New().String()
@@ -70,6 +108,10 @@ func (ao AbstractObject) GetID() string {
return ao.UUID
}
func (ao AbstractObject) GetSignature() []byte {
return ao.Signature
}
// GetName implements ShallowDBObject.
func (ao AbstractObject) GetName() string {
return ao.Name
@@ -83,17 +125,19 @@ func (ao *AbstractObject) UpToDate(user string, peer string, create bool) {
ao.UpdateDate = time.Now()
ao.UpdaterID = peer
ao.UserUpdaterID = user
if create {
if create && ao.CreatorID != "" {
ao.CreationDate = time.Now()
ao.CreatorID = peer
ao.UserCreatorID = user
}
}
func (ao *AbstractObject) VerifyAuth(request *tools.APIRequest) bool {
return ao.AccessMode == Public || (request != nil && ao.CreatorID == request.PeerID && request.PeerID != "")
func (ao *AbstractObject) VerifyAuth(callName string, request *tools.APIRequest) bool {
return (ao.AccessMode == Public && callName == "get") || (request != nil && (request.Admin || (ao.CreatorID == request.PeerID && request.PeerID != "")))
}
// TODO : check write per auth
func (ao *AbstractObject) GetObjectFilters(search string) *dbs.Filters {
if search == "*" {
search = ""
@@ -123,50 +167,104 @@ func (dma *AbstractObject) Serialize(obj DBObject) map[string]interface{} {
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
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
ResourceModelAccessor Accessor
New func() T
NotImplemented []string
}
func (r *AbstractAccessor) ShouldVerifyAuth() bool {
func (r *AbstractAccessor[T]) ShouldVerifyAuth() bool {
return true
}
func (r *AbstractAccessor) GetRequest() *tools.APIRequest {
func (r *AbstractAccessor[T]) GetRequest() *tools.APIRequest {
return r.Request
}
func (dma *AbstractAccessor) GetUser() string {
func (dma *AbstractAccessor[T]) GetUser() string {
if dma.Request == nil {
return ""
}
return dma.Request.Username
}
func (dma *AbstractAccessor) GetPeerID() string {
func (dma *AbstractAccessor[T]) GetPeerID() string {
if dma.Request == nil {
return ""
}
return dma.Request.PeerID
}
func (dma *AbstractAccessor) GetGroups() []string {
func (dma *AbstractAccessor[T]) GetGroups() []string {
if dma.Request == nil {
return []string{}
}
return dma.Request.Groups
}
func (dma *AbstractAccessor) GetLogger() *zerolog.Logger {
func (dma *AbstractAccessor[T]) GetLogger() *zerolog.Logger {
return &dma.Logger
}
func (dma *AbstractAccessor) GetType() tools.DataType {
func (dma *AbstractAccessor[T]) GetType() tools.DataType {
return dma.Type
}
func (dma *AbstractAccessor) GetCaller() *tools.HTTPCaller {
func (dma *AbstractAccessor[T]) GetCaller() *tools.HTTPCaller {
if dma.Request == nil {
return nil
}
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 DBObject, 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, a.New())
}
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[T](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
}
}

69
models/utils/common.go Normal file → Executable file
View File

@@ -2,9 +2,11 @@ package utils
import (
"errors"
"os"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"github.com/google/uuid"
mgb "go.mongodb.org/mongo-driver/mongo"
)
@@ -18,7 +20,7 @@ func VerifyAccess(a Accessor, id string) error {
if err != nil {
return err
}
if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
return errors.New("you are not allowed to access :" + a.GetType().String())
}
return nil
@@ -26,9 +28,14 @@ func VerifyAccess(a Accessor, id string) error {
// GenericLoadOne loads one object from the database (generic)
func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
data.GenerateID()
if data.GetID() == "" {
data.GenerateID()
}
data.SetID(data.GetID())
data.StoreDraftDefault()
data.UpToDate(a.GetUser(), a.GetPeerID(), true)
data.Unsign()
data.Sign()
f := dbs.Filters{
Or: map[string][]dbs.Filter{
"abstractresource.abstractobject.name": {{
@@ -41,7 +48,7 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
}},
},
}
if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
if a.ShouldVerifyAuth() && !data.VerifyAuth("store", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access : " + a.GetType().String())
}
if cursor, _, _ := a.Search(&f, "", data.IsDrafted()); len(cursor) > 0 {
@@ -49,7 +56,7 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
}
err := validate.Struct(data)
if err != nil {
return nil, 422, err
return nil, 422, errors.New("error when validating the received struct: " + err.Error())
}
id, code, err := mongo.MONGOService.StoreOne(data, data.GetID(), a.GetType().String())
if err != nil {
@@ -68,7 +75,7 @@ func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
if err != nil {
return nil, code, err
}
if a.ShouldVerifyAuth() && !res.VerifyAuth(a.GetRequest()) {
if a.ShouldVerifyAuth() && !res.VerifyAuth("delete", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access " + a.GetType().String())
}
_, code, err = mongo.MONGOService.DeleteOne(id, a.GetType().String())
@@ -92,7 +99,11 @@ func GenericUpdateOne(set DBObject, id string, a Accessor, new DBObject) (DBObje
}
set = newSet
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
if a.ShouldVerifyAuth() && !r.VerifyAuth(a.GetRequest()) {
if a.GetPeerID() == r.GetCreatorID() {
r.Unsign()
r.Sign()
}
if a.ShouldVerifyAuth() && !r.VerifyAuth("update", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
}
change := set.Serialize(set) // get the changes
@@ -109,14 +120,13 @@ func GenericUpdateOne(set DBObject, id string, a Accessor, new DBObject) (DBObje
return a.LoadOne(id)
}
func GenericLoadOne[T DBObject](id string, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
var data T
func GenericLoadOne[T DBObject](id string, data T, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String())
if err != nil {
return nil, code, err
}
res_mongo.Decode(&data)
if a.ShouldVerifyAuth() && !data.VerifyAuth(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 f(data)
@@ -132,7 +142,7 @@ func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft
return nil, 404, err
}
for _, r := range results {
if (a.ShouldVerifyAuth() && !r.VerifyAuth(a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) {
if (a.ShouldVerifyAuth() && !r.VerifyAuth("get", a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) {
continue
}
objs = append(objs, f(r))
@@ -164,3 +174,42 @@ func GenericRawUpdateOne(set DBObject, id string, a Accessor) (DBObject, int, er
}
return a.LoadOne(id)
}
func GetMySelf(wfa Accessor) (ShallowDBObject, error) {
id, err := GenerateNodeID()
if err != nil {
return nil, err
}
datas, _, _ := wfa.Search(nil, id, false)
if len(datas) > 0 {
return datas[0], nil
}
return nil, errors.New("peer not found")
}
func IsMySelf(peerID string, wfa Accessor) (bool, string) {
pp, err := GetMySelf(wfa)
if err != nil || pp == nil {
return false, ""
}
return peerID == pp.GetID(), pp.GetID()
}
func GenerateNodeID() (string, error) {
folderStatic := "/var/lib/opencloud-node"
if _, err := os.Stat(folderStatic); err == nil {
os.MkdirAll(folderStatic, 0644)
}
folderStatic += "/node_id"
if _, err := os.Stat(folderStatic); os.IsNotExist(err) {
hostname, err := os.Hostname()
if err != nil {
return "", err
}
id := uuid.NewSHA1(uuid.NameSpaceOID, []byte("oc-"+hostname))
err = os.WriteFile(folderStatic, []byte(id.String()), 0644)
return id.String(), err
}
data, err := os.ReadFile(folderStatic)
return string(data), err
}

9
models/utils/interfaces.go Normal file → Executable file
View File

@@ -18,18 +18,24 @@ type ShallowDBObject interface {
// DBObject is an interface that defines the basic methods for a DBObject
type DBObject interface {
GenerateID()
SetID(id string)
GetID() string
GetName() string
SetName(name string)
IsDrafted() bool
CanDelete() bool
StoreDraftDefault()
GetCreatorID() string
UpToDate(user string, peer string, create bool)
CanUpdate(set DBObject) (bool, DBObject)
VerifyAuth(request *tools.APIRequest) bool
VerifyAuth(callName string, request *tools.APIRequest) bool
Serialize(obj DBObject) map[string]interface{}
GetAccessor(request *tools.APIRequest) Accessor
Deserialize(j map[string]interface{}, obj DBObject) DBObject
Sign()
Unsign()
GetSignature() []byte
GetObjectFilters(search string) *dbs.Filters
}
// Accessor is an interface that defines the basic methods for an Accessor
@@ -49,4 +55,5 @@ type Accessor interface {
LoadAll(isDraft bool) ([]ShallowDBObject, int, error)
UpdateOne(set DBObject, id string) (DBObject, int, error)
Search(filters *dbs.Filters, search string, isDraft bool) ([]ShallowDBObject, int, error)
GetExec(isDraft bool) func(DBObject) ShallowDBObject
}

View File

@@ -0,0 +1,263 @@
package utils_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- AbstractObject ----
func TestAbstractObject_GetID(t *testing.T) {
obj := &utils.AbstractObject{UUID: "abc-123"}
assert.Equal(t, "abc-123", obj.GetID())
}
func TestAbstractObject_GetName(t *testing.T) {
obj := &utils.AbstractObject{Name: "test-name"}
assert.Equal(t, "test-name", obj.GetName())
}
func TestAbstractObject_GetCreatorID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "peer-xyz"}
assert.Equal(t, "peer-xyz", obj.GetCreatorID())
}
func TestAbstractObject_SetID(t *testing.T) {
obj := &utils.AbstractObject{}
obj.SetID("new-id")
assert.Equal(t, "new-id", obj.UUID)
}
func TestAbstractObject_SetName(t *testing.T) {
obj := &utils.AbstractObject{}
obj.SetName("hello")
assert.Equal(t, "hello", obj.Name)
}
func TestAbstractObject_GenerateID_WhenEmpty(t *testing.T) {
obj := &utils.AbstractObject{}
obj.GenerateID()
assert.NotEmpty(t, obj.UUID)
}
func TestAbstractObject_GenerateID_KeepsExisting(t *testing.T) {
obj := &utils.AbstractObject{UUID: "existing-id"}
obj.GenerateID()
assert.Equal(t, "existing-id", obj.UUID)
}
func TestAbstractObject_StoreDraftDefault(t *testing.T) {
obj := &utils.AbstractObject{IsDraft: true}
obj.StoreDraftDefault()
assert.False(t, obj.IsDraft)
}
func TestAbstractObject_IsDrafted(t *testing.T) {
obj := &utils.AbstractObject{IsDraft: true}
assert.True(t, obj.IsDrafted())
obj.IsDraft = false
assert.False(t, obj.IsDrafted())
}
func TestAbstractObject_CanDelete(t *testing.T) {
obj := &utils.AbstractObject{}
assert.True(t, obj.CanDelete())
}
func TestAbstractObject_CanUpdate(t *testing.T) {
obj := &utils.AbstractObject{UUID: "id-1"}
other := &utils.AbstractObject{UUID: "id-2"}
ok, returned := obj.CanUpdate(other)
assert.True(t, ok)
assert.Equal(t, other, returned)
}
func TestAbstractObject_Unsign(t *testing.T) {
obj := &utils.AbstractObject{Signature: []byte("sig")}
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)
}
func TestAbstractObject_GetObjectFilters_Search(t *testing.T) {
obj := &utils.AbstractObject{}
f := obj.GetObjectFilters("my-search")
assert.NotNil(t, f)
}
// ---- Serialize / Deserialize ----
func TestAbstractObject_SerializeDeserialize(t *testing.T) {
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())
}
// ---- GetAccessor ----
func TestAbstractObject_GetAccessor_ReturnsNil(t *testing.T) {
obj := &utils.AbstractObject{}
acc := obj.GetAccessor(nil)
assert.Nil(t, acc)
}
// ---- 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

@@ -15,6 +15,15 @@ type Graph struct {
Links []GraphLink `bson:"links" json:"links" default:"{}" validate:"required"` // Links is the list of links between elements in the graph
}
func NewGraph() *Graph {
return &Graph{
Partial: false,
Zoom: 1,
Items: map[string]GraphItem{},
Links: []GraphLink{},
}
}
func (g *Graph) Clear(id string) {
realItems := map[string]GraphItem{}
for k, it := range g.Items {
@@ -38,6 +47,10 @@ func (wf *Graph) IsProcessing(item GraphItem) bool {
return item.Processing != nil
}
func (wf *Graph) IsNativeTool(item GraphItem) bool {
return item.NativeTool != nil
}
func (wf *Graph) IsCompute(item GraphItem) bool {
return item.Compute != nil
}
@@ -55,7 +68,7 @@ func (wf *Graph) IsWorkflow(item GraphItem) bool {
}
func (g *Graph) GetAverageTimeRelatedToProcessingActivity(start time.Time, processings []*resources.ProcessingResource, resource resources.ResourceInterface,
f func(GraphItem) resources.ResourceInterface, request *tools.APIRequest) (float64, float64) {
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
longestDuration := float64(0)
@@ -67,7 +80,10 @@ func (g *Graph) GetAverageTimeRelatedToProcessingActivity(start time.Time, proce
} 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 := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, request)
priced, err := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil {
return 0, 0, err
}
if source != "" {
if priced.GetLocationStart() != nil {
near := float64(priced.GetLocationStart().Sub(start).Seconds())
@@ -88,15 +104,16 @@ func (g *Graph) GetAverageTimeRelatedToProcessingActivity(start time.Time, proce
}
}
if oneIsInfinite {
return nearestStart, -1
return nearestStart, -1, nil
}
return nearestStart, longestDuration
return nearestStart, longestDuration, nil
}
/*
* GetAverageTimeBeforeStart is a function that returns the average time before the start of a processing
*/
func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingID string, request *tools.APIRequest) float64 {
func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingID string,
instance int, partnership int, buying int, strategy int, bookingMode int, request *tools.APIRequest) (float64, error) {
currents := []float64{} // list of current time
for _, link := range g.Links { // for each link
var source string // source is the source of the link
@@ -112,13 +129,20 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
if r == nil { // if item is nil, continue
continue
}
priced := r.ConvertToPricedResource(dt, request)
priced, err := r.ConvertToPricedResource(dt, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil {
return 0, err
}
current := priced.GetExplicitDurationInS() // get the explicit duration of the item
if current < 0 { // if current is negative, its means that duration of a before could be infinite continue
return current
return current, nil
}
current += g.GetAverageTimeProcessingBeforeStart(current, source, request) // get the average time before start of the source
currents = append(currents, current) // append the current to the currents
add, err := g.GetAverageTimeProcessingBeforeStart(current, source, instance, partnership, buying, strategy, bookingMode, request)
if err != nil {
return 0, err
}
current += add // get the average time before start of the source
currents = append(currents, current) // append the current to the currents
}
var max float64 // get the max time to wait dependancies to finish
for _, current := range currents {
@@ -126,12 +150,14 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
max = current
}
}
return max
return max, nil
}
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
if item, ok := g.Items[id]; ok {
if item.Data != nil {
return tools.NATIVE_TOOL, item.NativeTool
} else if item.Data != nil {
return tools.DATA_RESOURCE, item.Data
} else if item.Compute != nil {
return tools.COMPUTE_RESOURCE, item.Compute

View File

@@ -1,19 +1,37 @@
package workflow
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"mime/multipart"
"strings"
"time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"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/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"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/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
type ConfigItem map[string]int
func (c ConfigItem) Get(key string) *int {
i := 0
if ins, ok := c[key]; ok {
i = ins
}
return &i
}
/*
* Workflow is a struct that represents a workflow
* it defines the native workflow
@@ -24,13 +42,335 @@ type Workflow struct {
Graph *graph.Graph `bson:"graph,omitempty" json:"graph,omitempty"` // Graph UI & logic representation of the workflow
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
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 {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *Workflow) GetResources(dt tools.DataType) []resources.ResourceInterface {
itf := []resources.ResourceInterface{}
switch dt {
case tools.NATIVE_TOOL:
for _, d := range d.NativeTools {
itf = append(itf, d)
}
return itf
case tools.DATA_RESOURCE:
for _, d := range d.DataResources {
itf = append(itf, d)
}
return itf
case tools.PROCESSING_RESOURCE:
for _, d := range d.ProcessingResources {
itf = append(itf, d)
}
return itf
case tools.COMPUTE_RESOURCE:
for _, d := range d.ComputeResources {
itf = append(itf, d)
}
return itf
case tools.WORKFLOW_RESOURCE:
for _, d := range d.WorkflowResources {
itf = append(itf, d)
}
return itf
case tools.STORAGE_RESOURCE:
for _, d := range d.StorageResources {
itf = append(itf, d)
}
return itf
}
return itf
}
func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.APIRequest) (*Workflow, error) {
if plantUML == nil {
return d, errors.New("no file available to export")
}
defer plantUML.Close()
d.Datas = []string{}
d.Storages = []string{}
d.Processings = []string{}
d.Computes = []string{}
d.Workflows = []string{}
d.DataResources = []*resources.DataResource{}
d.StorageResources = []*resources.StorageResource{}
d.ProcessingResources = []*resources.ProcessingResource{}
d.ComputeResources = []*resources.ComputeResource{}
d.WorkflowResources = []*resources.WorkflowResource{}
d.Graph = graph.NewGraph()
resourceCatalog := map[string]func() resources.ResourceInterface{
"Processing": func() resources.ResourceInterface {
return &resources.ProcessingResource{
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ProcessingInstance]{
Instances: []*resources.ProcessingInstance{},
},
}
},
"Storage": func() resources.ResourceInterface {
return &resources.StorageResource{
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.StorageResourceInstance]{
Instances: []*resources.StorageResourceInstance{},
},
}
},
"Data": func() resources.ResourceInterface {
return &resources.DataResource{
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.DataInstance]{
Instances: []*resources.DataInstance{},
},
}
},
"ComputeUnit": func() resources.ResourceInterface {
return &resources.ComputeResource{
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ComputeResourceInstance]{
Instances: []*resources.ComputeResourceInstance{},
},
}
},
}
graphVarName := map[string]*graph.GraphItem{}
scanner := bufio.NewScanner(plantUML)
for scanner.Scan() {
line := 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 {
return d, err
}
d.generateResource(d.GetResources(tools.DATA_RESOURCE), request)
d.generateResource(d.GetResources(tools.PROCESSING_RESOURCE), request)
d.generateResource(d.GetResources(tools.STORAGE_RESOURCE), request)
d.generateResource(d.GetResources(tools.COMPUTE_RESOURCE), request)
d.generateResource(d.GetResources(tools.WORKFLOW_RESOURCE), request)
return d, nil
}
func (d *Workflow) generateResource(datas []resources.ResourceInterface, request *tools.APIRequest) error {
for _, d := range datas {
access := d.GetAccessor(request)
if _, code, err := access.LoadOne(d.GetID()); err != nil && code == 200 {
continue
}
access.StoreOne(d)
}
return nil
}
func (d *Workflow) extractLink(line string, graphVarName map[string]*graph.GraphItem, pattern string, reverse bool) error {
splitted := strings.Split(line, pattern)
if len(splitted) < 2 {
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{
Source: graph.Position{
ID: graphVarName[splitted[0]].ID,
X: 0,
Y: 0,
},
Destination: graph.Position{
ID: graphVarName[splitted[1]].ID,
X: 0,
Y: 0,
},
}
if reverse {
tmp := link.Destination
link.Destination = link.Source
link.Source = tmp
}
splittedComments := strings.Split(line, "'")
if len(splittedComments) <= 1 {
return errors.New("Can't deserialize Object, there's no commentary")
}
comment := strings.ReplaceAll(splittedComments[1], "'", "") // for now it's a json.
json.Unmarshal([]byte(comment), link)
d.Graph.Links = append(d.Graph.Links, *link)
return nil
}
func (d *Workflow) extractResourcePlantUML(line string, resource resources.ResourceInterface, dataName string, peerID string) (string, *graph.GraphItem, error) {
splittedFunc := strings.Split(line, "(")
if len(splittedFunc) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no func")
}
splittedParams := strings.Split(splittedFunc[1], ",")
if len(splittedFunc) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no params")
}
varName := splittedParams[0]
splitted := strings.Split(splittedParams[1], "\"")
if len(splitted) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no name")
}
resource.SetName(splitted[1])
splittedComments := strings.Split(line, "'")
if len(splittedComments) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no commentary")
}
comment := strings.ReplaceAll(splittedComments[1], "'", "") // for now it's a json.
instance := d.getNewInstance(dataName, splitted[1], peerID)
if instance == nil {
return "", nil, errors.New("No instance found.")
}
resource.AddInstances(instance)
json.Unmarshal([]byte(comment), instance)
// deserializer les instances... une instance doit par défaut avoir certaines valeurs d'accès.
graphID := uuid.New()
graphItem := &graph.GraphItem{
ID: graphID.String(),
}
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 {
case "Data":
d.Datas = append(d.Datas, resource.GetID())
d.DataResources = append(d.DataResources, resource.(*resources.DataResource))
graphItem.Data = resource.(*resources.DataResource)
case "Processing":
d.Processings = append(d.Processings, resource.GetID())
d.ProcessingResources = append(d.ProcessingResources, resource.(*resources.ProcessingResource))
graphItem.Processing = resource.(*resources.ProcessingResource)
case "Event":
access := resources.NewAccessor[*resources.NativeTool](tools.NATIVE_TOOL, &tools.APIRequest{
Admin: true,
}, func() utils.DBObject { return &resources.NativeTool{} })
t, _, err := access.Search(nil, "WORKFLOW_EVENT", false)
if err == nil && len(t) > 0 {
d.NativeTool = append(d.NativeTool, t[0].GetID())
graphItem.NativeTool = t[0].(*resources.NativeTool)
}
case "Storage":
d.Storages = append(d.Storages, resource.GetID())
d.StorageResources = append(d.StorageResources, resource.(*resources.StorageResource))
graphItem.Storage = resource.(*resources.StorageResource)
case "ComputeUnit":
d.Computes = append(d.Computes, resource.GetID())
d.ComputeResources = append(d.ComputeResources, resource.(*resources.ComputeResource))
graphItem.Compute = resource.(*resources.ComputeResource)
default:
return graphItem
}
return graphItem
}
func (d *Workflow) getNewInstance(dataName string, name string, peerID string) resources.ResourceInstanceITF {
switch dataName {
case "Data":
return resources.NewDataInstance(name, peerID)
case "Processing":
return resources.NewProcessingInstance(name, peerID)
case "Storage":
return resources.NewStorageResourceInstance(name, peerID)
case "ComputeUnit":
return resources.NewComputeResourceInstance(name, peerID)
default:
return nil
}
}
type Deps struct {
Source string
Dest string
}
func (w *Workflow) IsDependancy(id string) []Deps {
dependancyOfIDs := []Deps{}
for _, link := range w.Graph.Links {
if _, ok := w.Graph.Items[link.Destination.ID]; !ok {
continue
}
source := w.Graph.Items[link.Destination.ID].Processing
if id == link.Source.ID && source != nil {
dependancyOfIDs = append(dependancyOfIDs, Deps{Source: source.GetName(), Dest: link.Destination.ID})
}
sourceWF := w.Graph.Items[link.Destination.ID].Workflow
if id == link.Source.ID && sourceWF != nil {
dependancyOfIDs = append(dependancyOfIDs, Deps{Source: sourceWF.GetName(), Dest: link.Destination.ID})
}
}
return dependancyOfIDs
}
func (w *Workflow) GetFirstItems() []graph.GraphItem {
return w.GetGraphItems(func(item graph.GraphItem) bool {
return len(w.GetDependencies(w.GetID())) == 0
})
}
func (w *Workflow) GetDependencies(id string) (dependencies []Deps) {
for _, link := range w.Graph.Links {
if _, ok := w.Graph.Items[link.Source.ID]; !ok {
continue
}
source := w.Graph.Items[link.Source.ID].Processing
if id == link.Destination.ID && source != nil {
dependencies = append(dependencies, Deps{Source: source.GetName(), Dest: link.Source.ID})
continue
}
}
return
}
func (w *Workflow) GetGraphItems(f func(item graph.GraphItem) bool) (list_datas []graph.GraphItem) {
for _, item := range w.Graph.Items {
if f(item) {
@@ -40,16 +380,29 @@ func (w *Workflow) GetGraphItems(f func(item graph.GraphItem) bool) (list_datas
return
}
func (w *Workflow) GetPricedItem(f func(item graph.GraphItem) bool, request *tools.APIRequest) map[string]pricing.PricedItemITF {
func (w *Workflow) GetPricedItem(
f func(item graph.GraphItem) bool, request *tools.APIRequest,
instance int,
partnership int,
buying int,
strategy int,
bookingMode int,
buyingStrategy int,
pricingStrategy int) (map[string]pricing.PricedItemITF, error) {
list_datas := map[string]pricing.PricedItemITF{}
for _, item := range w.Graph.Items {
if f(item) {
dt, res := item.GetResource()
ord := res.ConvertToPricedResource(dt, request)
ord, err := res.ConvertToPricedResource(dt, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil {
return list_datas, err
}
list_datas[res.GetID()] = ord
}
}
return list_datas
return list_datas, nil
}
type Related struct {
@@ -72,19 +425,17 @@ func (w *Workflow) GetByRelatedProcessing(processingID string, g func(item graph
_, node = item.GetResource() // we are looking for the storage as destination
}
if processingID == nodeID && node != nil { // if the storage is linked to the processing
if _, ok := related[processingID]; !ok {
related[processingID] = Related{}
}
rel := related[node.GetID()]
relID := node.GetID()
rel := Related{}
rel.Node = node
rel.Links = append(rel.Links, link)
related[processingID] = rel
related[relID] = rel
}
}
return related
}
func (ao *Workflow) VerifyAuth(request *tools.APIRequest) bool {
func (ao *Workflow) VerifyAuth(callName string, request *tools.APIRequest) bool {
isAuthorized := false
if len(ao.Shared) > 0 {
for _, shared := range ao.Shared {
@@ -92,13 +443,14 @@ func (ao *Workflow) VerifyAuth(request *tools.APIRequest) bool {
if code != 200 || shared == nil {
isAuthorized = false
} else {
isAuthorized = shared.VerifyAuth(request)
isAuthorized = shared.VerifyAuth(callName, request)
}
}
}
return ao.AbstractObject.VerifyAuth(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)
*/
@@ -128,69 +480,140 @@ func (wfa *Workflow) CheckBooking(caller *tools.HTTPCaller) (bool, error) {
return true, nil
}
func (wf *Workflow) Planify(start time.Time, end *time.Time, request *tools.APIRequest) (float64, map[tools.DataType]map[string]pricing.PricedItemITF, *Workflow, error) {
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) {
priceds := map[tools.DataType]map[string]pricing.PricedItemITF{}
ps, priceds, err := plan[*resources.ProcessingResource](tools.PROCESSING_RESOURCE, wf, priceds, request, wf.Graph.IsProcessing,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
return start.Add(time.Duration(wf.Graph.GetAverageTimeProcessingBeforeStart(0, res.GetID(), request)) * time.Second), priced.GetExplicitDurationInS()
}, func(started time.Time, duration float64) *time.Time {
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) {
d, err := wf.Graph.GetAverageTimeProcessingBeforeStart(0, res.GetID(),
*instances.Get(res.GetID()), *partnerships.Get(res.GetID()), *buyings.Get(res.GetID()), *strategies.Get(res.GetID()),
bookingMode, request)
if err != nil {
return start, 0, err
}
return start.Add(time.Duration(d) * time.Second), priced.GetExplicitDurationInS(), nil
}, func(started time.Time, duration float64) (*time.Time, error) {
s := started.Add(time.Duration(duration))
return &s
return &s, nil
})
if err != nil {
return 0, priceds, nil, err
return false, 0, priceds, nil, err
}
if _, priceds, err = plan[resources.ResourceInterface](tools.DATA_RESOURCE, wf, priceds, request, wf.Graph.IsData,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
return start, 0
}, func(started time.Time, duration float64) *time.Time {
return end
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) {
return start, 0, nil
}, func(started time.Time, duration float64) (*time.Time, error) {
return end, nil
}); err != nil {
return 0, priceds, nil, err
return false, 0, priceds, nil, err
}
for k, f := range map[tools.DataType]func(graph.GraphItem) bool{tools.STORAGE_RESOURCE: wf.Graph.IsStorage, tools.COMPUTE_RESOURCE: wf.Graph.IsCompute} {
if _, priceds, err = plan[resources.ResourceInterface](k, wf, priceds, request, f,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
nearestStart, longestDuration := wf.Graph.GetAverageTimeRelatedToProcessingActivity(start, ps, res, func(i graph.GraphItem) (r resources.ResourceInterface) {
if _, priceds, err = plan[resources.ResourceInterface](tools.DATA_RESOURCE, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request,
wf.Graph.IsData, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
return start, 0, nil
}, func(started time.Time, duration float64) (*time.Time, error) {
return end, nil
}); err != nil {
return false, 0, priceds, nil, err
}
for k, f := range map[tools.DataType]func(graph.GraphItem) bool{tools.STORAGE_RESOURCE: wf.Graph.IsStorage,
tools.COMPUTE_RESOURCE: wf.Graph.IsCompute} {
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) {
nearestStart, longestDuration, err := wf.Graph.GetAverageTimeRelatedToProcessingActivity(start, ps, res, func(i graph.GraphItem) (r resources.ResourceInterface) {
if f(i) {
_, r = i.GetResource()
}
return r
}, request)
return start.Add(time.Duration(nearestStart) * time.Second), longestDuration
}, func(started time.Time, duration float64) *time.Time {
}, *instances.Get(res.GetID()), *partnerships.Get(res.GetID()),
*buyings.Get(res.GetID()), *strategies.Get(res.GetID()), bookingMode, request)
if err != nil {
return start, longestDuration, err
}
return start.Add(time.Duration(nearestStart) * time.Second), longestDuration, nil
}, func(started time.Time, duration float64) (*time.Time, error) {
s := started.Add(time.Duration(duration))
return &s
return &s, nil
}); err != nil {
return 0, priceds, nil, err
return false, 0, priceds, nil, err
}
}
longest := common.GetPlannerLongestTime(end, priceds, request)
if _, priceds, err = plan[resources.ResourceInterface](tools.WORKFLOW_RESOURCE, wf, priceds, request, wf.Graph.IsWorkflow,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
if _, priceds, err = plan[resources.ResourceInterface](tools.WORKFLOW_RESOURCE, instances, partnerships, buyings, strategies,
bookingMode, wf, priceds, request, wf.Graph.IsWorkflow,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
start := start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds, request)) * time.Second)
longest := float64(-1)
r, code, err := res.GetAccessor(request).LoadOne(res.GetID())
if code != 200 || err != nil {
return start, longest
return start, longest, err
}
if neoLongest, _, _, err := r.(*Workflow).Planify(start, end, request); err != nil {
return start, longest
_, neoLongest, priceds2, _, err := r.(*Workflow).Planify(start, end, instances, partnerships, buyings, strategies, bookingMode, request)
// should ... import priced
if err != nil {
return start, longest, err
} else if neoLongest > longest {
longest = neoLongest
}
return start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds, request)) * time.Second), longest
}, func(start time.Time, longest float64) *time.Time {
for k, v := range priceds2 {
if priceds[k] == nil {
priceds[k] = map[string]pricing.PricedItemITF{}
}
for k2, v2 := range v {
if priceds[k][k2] != nil {
v2.AddQuantity(priceds[k][k2].GetQuantity())
}
}
}
return start.Add(time.Duration(common.GetPlannerNearestStart(start, priceds, request)) * time.Second), longest, nil
}, func(start time.Time, longest float64) (*time.Time, error) {
s := start.Add(time.Duration(longest) * time.Second)
return &s
return &s, nil
}); err != nil {
return 0, priceds, nil, err
return false, 0, priceds, nil, err
}
return longest, priceds, wf, nil
isPreemptible := true
for _, first := range wf.GetFirstItems() {
_, res := first.GetResource()
if res.GetBookingModes()[booking.PREEMPTED] == nil {
isPreemptible = false
break
}
}
return isPreemptible, longest, priceds, wf, nil
}
func plan[T resources.ResourceInterface](dt tools.DataType, wf *Workflow, priceds map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest,
f func(graph.GraphItem) bool, start func(resources.ResourceInterface, pricing.PricedItemITF) (time.Time, float64), end func(time.Time, float64) *time.Time) ([]T, map[tools.DataType]map[string]pricing.PricedItemITF, error) {
// 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
// ex: if the same Minio storage is represented by several nodes in the graph, in [tools.STORAGE_RESSOURCE] its UUID will be mapped to
// the list of GraphItem ID that correspond to the ID of each node
func (w *Workflow) GetItemsByResources() map[tools.DataType]map[string][]string {
res := make(map[tools.DataType]map[string][]string)
dtMethodMap := map[tools.DataType]func() []graph.GraphItem{
tools.STORAGE_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsStorage) },
tools.DATA_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsData) },
tools.COMPUTE_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsCompute) },
tools.PROCESSING_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsProcessing) },
tools.WORKFLOW_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsWorkflow) },
}
for dt, meth := range dtMethodMap {
res[dt] = make(map[string][]string)
items := meth()
for _, i := range items {
_, r := i.GetResource()
rId := r.GetID()
res[dt][rId] = append(res[dt][rId], i.ID)
}
}
return res
}
func plan[T resources.ResourceInterface](
dt tools.DataType, instances ConfigItem, partnerships ConfigItem, buyings ConfigItem, strategies ConfigItem, bookingMode int, wf *Workflow, priceds map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest,
f func(graph.GraphItem) bool,
start func(resources.ResourceInterface, pricing.PricedItemITF) (time.Time, float64, error),
end func(time.Time, float64) (*time.Time, error)) ([]T, map[tools.DataType]map[string]pricing.PricedItemITF, error) {
resources := []T{}
for _, item := range wf.GetGraphItems(f) {
if priceds[dt] == nil {
@@ -200,19 +623,34 @@ func plan[T resources.ResourceInterface](dt tools.DataType, wf *Workflow, priced
if realItem == nil {
return resources, priceds, errors.New("could not load the processing resource")
}
priced := realItem.ConvertToPricedResource(dt, request)
started, duration := start(realItem, priced)
priced, err := realItem.ConvertToPricedResource(dt, instances.Get(realItem.GetID()),
partnerships.Get(realItem.GetID()), buyings.Get(realItem.GetID()), strategies.Get(realItem.GetID()), &bookingMode, request)
if err != nil {
return resources, priceds, err
}
// Should be commented once the Pricing selection feature has been implemented, related to the commit d35ad440fa77763ec7f49ab34a85e47e75581b61
// if priced.SelectPricing() == nil {
// return resources, priceds, errors.New("no pricings are selected... can't proceed")
// }
started, duration, err := start(realItem, priced)
if err != nil {
return resources, priceds, err
}
priced.SetLocationStart(started)
if duration >= 0 {
if e := end(started, duration); e != nil {
if e, err := end(started, duration); err == nil && e != nil {
priced.SetLocationEnd(*e)
}
}
if e := end(started, priced.GetExplicitDurationInS()); e != nil {
if e, err := end(started, priced.GetExplicitDurationInS()); err != nil && e != nil {
priced.SetLocationEnd(*e)
}
resources = append(resources, realItem.(T))
if priceds[dt][item.ID] != nil {
priced.AddQuantity(priceds[dt][item.ID].GetQuantity())
}
priceds[dt][item.ID] = priced
}
return resources, priceds, nil
}

View File

@@ -14,10 +14,10 @@ import (
)
type workflowMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
computeResourceAccessor utils.Accessor
collaborativeAreaAccessor utils.Accessor
workspaceAccessor utils.Accessor
utils.AbstractAccessor[*Workflow] // AbstractAccessor contains the basic fields of an accessor (model, caller)
computeResourceAccessor utils.Accessor
collaborativeAreaAccessor utils.Accessor
workspaceAccessor utils.Accessor
}
func NewAccessorHistory(request *tools.APIRequest) *workflowMongoAccessor {
@@ -34,10 +34,11 @@ func new(t tools.DataType, request *tools.APIRequest) *workflowMongoAccessor {
computeResourceAccessor: (&resources.ComputeResource{}).GetAccessor(request),
collaborativeAreaAccessor: (&shallow_collaborative_area.ShallowCollaborativeArea{}).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
Request: request,
Type: t,
New: func() *Workflow { return &Workflow{} },
},
}
}
@@ -68,7 +69,9 @@ func (a *workflowMongoAccessor) share(realData *Workflow, delete bool, caller *t
paccess := &peer.Peer{}
for _, p := range res.(*shallow_collaborative_area.ShallowCollaborativeArea).Peers {
paccess.UUID = p
if ok, _ := paccess.IsMySelf(); ok { // if the peer is the current peer, never share because it will create a loop
if ok, _ := utils.IsMySelf(p, paccess.GetAccessor(&tools.APIRequest{
Admin: true,
})); ok { // if the peer is the current peer, never share because it will create a loop
continue
}
if delete { // if the workflow is deleted, share the deletion orderResourceAccessor utils.Accessor
@@ -177,19 +180,16 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
}
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)
a.execute(w, false, true) // if no workspace is attached to the workflow, create it
return d, 200, nil
}, 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) {
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 {
@@ -203,17 +203,18 @@ func (a *workflowMongoAccessor) verifyResource(obj utils.DBObject) utils.DBObjec
continue
}
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{} })
} else if t == tools.PROCESSING_RESOURCE {
case tools.PROCESSING_RESOURCE:
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{} })
} else if t == tools.WORKFLOW_RESOURCE {
case tools.WORKFLOW_RESOURCE:
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{} })
} else {
default:
wf.Graph.Clear(resource.GetID())
}
if error := utils.VerifyAccess(access, resource.GetID()); error != nil {

View File

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

91
models/workflow_execution/workflow_execution.go Normal file → Executable file
View File

@@ -1,6 +1,7 @@
package workflow_execution
import (
"encoding/json"
"strings"
"time"
@@ -8,7 +9,9 @@ import (
"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/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson/primitive"
@@ -21,12 +24,22 @@ import (
*/
type WorkflowExecution struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
Priority int `json:"priority" bson:"priority"` // will best effort on default // Will Best Effort on priority
PeerBuyByGraph map[string]map[string][]string `json:"peer_buy_by_graph,omitempty" bson:"peer_buy_by_graph,omitempty"` // BookByResource is a map of the resource id and the list of the booking id
PeerBookByGraph map[string]map[string][]string `json:"peer_book_by_graph,omitempty" bson:"peer_book_by_graph,omitempty"` // BookByResource is a map of the resource id and the list of the booking id
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty"`
ExecDate time.Time `json:"execution_date,omitempty" bson:"execution_date,omitempty" validate:"required"` // ExecDate is the execution date of the workflow, is required
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"` // EndDate is the end date 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
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"`
SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
SelectedBuyings workflow.ConfigItem `json:"selected_buyings"`
SelectedStrategies workflow.ConfigItem `json:"selected_strategies"`
}
func (r *WorkflowExecution) StoreDraftDefault() {
@@ -105,21 +118,88 @@ func (d *WorkflowExecution) GetAccessor(request *tools.APIRequest) utils.Accesso
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *WorkflowExecution) VerifyAuth(request *tools.APIRequest) bool {
func (d *WorkflowExecution) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}
/*
booking is an activity reserved for not a long time investment.
... purchase is dependant of a one time buying.
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 {
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])...)
d.PurchasesState = map[string]bool{}
for _, p := range purchases {
p.SetID(uuid.NewString())
d.PurchasesState[p.GetID()] = false
}
return purchases
}
func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID string, wfID string, dt tools.DataType, priceds map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource {
items := []*purchase_resource.PurchaseResource{}
for itemID, priced := range priceds {
if !priced.IsPurchasable() || bs != pricing.BILL_ONCE { // buy only that must be buy
continue
}
if d.PeerBuyByGraph == nil {
d.PeerBuyByGraph = map[string]map[string][]string{}
}
if d.PeerBuyByGraph[priced.GetCreatorID()] == nil {
d.PeerBuyByGraph[priced.GetCreatorID()] = map[string][]string{}
}
if d.PeerBuyByGraph[priced.GetCreatorID()][itemID] == nil {
d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = []string{}
}
start := d.ExecDate
if s := priced.GetLocationStart(); s != nil {
start = *s
}
var m map[string]interface{}
b, _ := json.Marshal(priced)
json.Unmarshal(b, &m)
end := start.Add(time.Duration(priced.GetExplicitDurationInS()) * time.Second)
bookingItem := &purchase_resource.PurchaseResource{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID,
},
PricedItem: m,
ExecutionID: d.GetID(),
ExecutionsID: executionsID,
DestPeerID: priced.GetCreatorID(),
ResourceID: priced.GetID(),
InstanceID: priced.GetInstanceID(),
ResourceType: dt,
EndDate: &end,
}
items = append(items, bookingItem)
d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = append(
d.PeerBuyByGraph[priced.GetCreatorID()][itemID], bookingItem.GetID())
}
return items
}
func (d *WorkflowExecution) Book(executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*booking.Booking {
booking := d.bookEach(executionsID, wfID, tools.STORAGE_RESOURCE, priceds[tools.STORAGE_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.DATA_RESOURCE, priceds[tools.DATA_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])...)
for _, p := range booking {
p.SetID(uuid.NewString())
d.BookingsState[p.GetID()] = false
}
return booking
}
func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.DataType, priceds map[string]pricing.PricedItemITF) []*booking.Booking {
items := []*booking.Booking{}
for itemID, priced := range priceds {
if !priced.IsBooked() { // book only that must be booked
continue
}
if d.PeerBookByGraph == nil {
d.PeerBookByGraph = map[string]map[string][]string{}
}
@@ -134,14 +214,19 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
start = *s
}
end := start.Add(time.Duration(priced.GetExplicitDurationInS()) * time.Second)
var m map[string]interface{}
b, _ := json.Marshal(priced)
json.Unmarshal(b, &m)
bookingItem := &booking.Booking{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID,
},
PricedItem: m,
ExecutionsID: executionsID,
State: enum.SCHEDULED,
ResourceID: priced.GetID(),
InstanceID: priced.GetInstanceID(),
ResourceType: dt,
DestPeerID: priced.GetCreatorID(),
WorkflowID: wfID,

View File

@@ -11,38 +11,38 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
type workflowExecutionMongoAccessor struct {
utils.AbstractAccessor
type WorkflowExecutionMongoAccessor struct {
utils.AbstractAccessor[*WorkflowExecution]
shallow bool
}
func newShallowAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
return &workflowExecutionMongoAccessor{
func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{
shallow: true,
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request,
Type: tools.WORKFLOW_EXECUTION,
AbstractAccessor: utils.AbstractAccessor[*WorkflowExecution]{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request,
Type: tools.WORKFLOW_EXECUTION,
New: func() *WorkflowExecution { return &WorkflowExecution{} },
NotImplemented: []string{"DeleteOne", "StoreOne", "CopyOne"},
},
}
}
func NewAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
return &workflowExecutionMongoAccessor{
func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{
shallow: false,
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request,
Type: tools.WORKFLOW_EXECUTION,
AbstractAccessor: utils.AbstractAccessor[*WorkflowExecution]{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request,
Type: tools.WORKFLOW_EXECUTION,
New: func() *WorkflowExecution { return &WorkflowExecution{} },
NotImplemented: []string{"DeleteOne", "StoreOne", "CopyOne"},
},
}
}
func (wfa *workflowExecutionMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented")
}
func (wfa *workflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
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")
}
@@ -50,16 +50,8 @@ func (wfa *workflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id stri
return utils.GenericUpdateOne(&realSet, id, wfa, &WorkflowExecution{})
}
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) {
return utils.GenericLoadOne[*WorkflowExecution](id, func(d utils.DBObject) (utils.DBObject, int, error) {
func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*WorkflowExecution](id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now()
now = now.Add(time.Second * -60)
if d.(*WorkflowExecution).State == enum.DRAFT && !a.shallow && now.UTC().After(d.(*WorkflowExecution).ExecDate) {
@@ -73,16 +65,7 @@ func (a *workflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int
return d, 200, nil
}, a)
}
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 {
func (a *WorkflowExecutionMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now()
now = now.Add(time.Second * -60)
@@ -99,7 +82,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{
Or: map[string][]dbs.Filter{ // filter by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}},

View File

@@ -1,292 +0,0 @@
package workflow_execution
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
"github.com/robfig/cron"
"github.com/rs/zerolog"
)
/*
* WorkflowSchedule is a struct that contains the scheduling information of a workflow
* It contains the mode of the schedule (Task or Service), the name of the schedule, the start and end time of the schedule and the cron expression
*/
// it's a flying object only use in a session time. It's not stored in the database
type WorkflowSchedule struct {
UUID string `json:"id" validate:"required"` // ExecutionsID is the list of the executions id of the workflow
Workflow *workflow.Workflow `json:"workflow,omitempty"` // Workflow is the workflow dependancy of the schedule
WorkflowExecution []*WorkflowExecution `json:"workflow_executions,omitempty"` // WorkflowExecution is the list of executions of the workflow
Message string `json:"message,omitempty"` // Message is the message of the schedule
Warning string `json:"warning,omitempty"` // Warning is the warning message of the schedule
Start time.Time `json:"start" validate:"required,ltfield=End"` // Start is the start time of the schedule, is required and must be less than the End time
End *time.Time `json:"end,omitempty"` // End is the end time of the schedule, is required and must be greater than the Start time
DurationS float64 `json:"duration_s" default:"-1"` // End is the end time of the schedule
Cron string `json:"cron,omitempty"` // here the cron format : ss mm hh dd MM dw task
}
func NewScheduler(start string, end string, durationInS float64, cron string) *WorkflowSchedule {
s, err := time.Parse("2006-01-02T15:04:05", start)
if err != nil {
return nil
}
ws := &WorkflowSchedule{
UUID: uuid.New().String(),
Start: s,
DurationS: durationInS,
Cron: cron,
}
e, err := time.Parse("2006-01-02T15:04:05", end)
if err == nil {
ws.End = &e
}
return ws
}
func (ws *WorkflowSchedule) CheckBooking(wfID string, request *tools.APIRequest) (bool, *workflow.Workflow, []*WorkflowExecution, []*booking.Booking, error) {
l := logs.GetLogger().With().Str("SchedulerID", ws.UUID).Logger()
l.Debug().Msg("Checking booking")
if request.Caller == nil && request.Caller.URLS == nil && request.Caller.URLS[tools.BOOKING] == nil || request.Caller.URLS[tools.BOOKING][tools.GET] == "" {
return false, nil, []*WorkflowExecution{}, []*booking.Booking{}, errors.New("no caller defined")
}
access := workflow.NewAccessor(request)
res, code, err := access.LoadOne(wfID)
if code != 200 {
return false, nil, []*WorkflowExecution{}, []*booking.Booking{}, errors.New("could not load the workflow with id: " + err.Error())
}
wf := res.(*workflow.Workflow)
longest, priceds, wf, err := wf.Planify(ws.Start, ws.End, request)
if err != nil {
return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err
}
ws.DurationS = longest
ws.Message = "We estimate that the workflow will start at " + ws.Start.String() + " and last " + fmt.Sprintf("%v", ws.DurationS) + " seconds."
if ws.End != nil && ws.Start.Add(time.Duration(longest)*time.Second).After(*ws.End) {
ws.Warning = "The workflow may be too long to be executed in the given time frame, we will try to book it anyway\n"
}
l.Debug().Msg("Getting executions")
execs, err := ws.getExecutions(wf)
if err != nil {
return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err
}
bookings := []*booking.Booking{}
for i, exec := range execs {
l.Debug().Msg("looping throughs execs : " + string(i))
bookings = append(bookings, exec.Book(ws.UUID, wfID, priceds)...)
}
errCh := make(chan error, len(bookings))
var m sync.Mutex
for _, b := range bookings {
go getBooking(l, b, request, wf, execs, bookings, errCh, &m)
}
for i := 0; i < len(bookings); i++ {
if err := <-errCh; err != nil {
return false, wf, execs, bookings, err
}
}
return true, wf, execs, bookings, nil
}
func getBooking(l zerolog.Logger, b *booking.Booking, request *tools.APIRequest, wf *workflow.Workflow, execs []*WorkflowExecution, bookings []*booking.Booking, errCh chan error, m *sync.Mutex) {
m.Lock()
c, err := getCallerCopy(request, errCh)
if err != nil {
errCh <- err
return
}
m.Unlock()
bl := l.With().Str("booking", b.UUID).Logger()
meth := request.Caller.URLS[tools.BOOKING][tools.GET]
meth = strings.ReplaceAll(meth, ":id", b.ResourceID)
meth = strings.ReplaceAll(meth, ":start_date", b.ExpectedStartDate.Format("2006-01-02T15:04:05"))
meth = strings.ReplaceAll(meth, ":end_date", b.ExpectedEndDate.Format("2006-01-02T15:04:05"))
request.Caller.URLS[tools.BOOKING][tools.GET] = meth
bl.Debug().Msg("Get booking " + b.UUID + " on " + b.DestPeerID)
_, err = (&peer.Peer{}).LaunchPeerExecution(b.DestPeerID, b.ResourceID, tools.BOOKING, tools.GET, nil, &c)
bl.Debug().Msg("Received response from Get booking " + b.UUID + " on " + b.DestPeerID)
if err != nil {
errCh <- err
return
}
errCh <- nil
}
func getCallerCopy(request *tools.APIRequest, errCh chan error) (tools.HTTPCaller, error) {
var c tools.HTTPCaller
err := request.Caller.DeepCopy(c)
if err != nil {
errCh <- err
return tools.HTTPCaller{}, nil
}
c.URLS = request.Caller.URLS
return c, err
}
func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*WorkflowSchedule, *workflow.Workflow, []*WorkflowExecution, error) {
if request == nil {
return ws, nil, []*WorkflowExecution{}, errors.New("no request found")
}
c := request.Caller
if c == nil || c.URLS == nil || c.URLS[tools.BOOKING] == nil {
return ws, nil, []*WorkflowExecution{}, errors.New("no caller defined")
}
methods := c.URLS[tools.BOOKING]
if _, ok := methods[tools.GET]; !ok {
return ws, nil, []*WorkflowExecution{}, errors.New("no path found")
}
ok, wf, executions, bookings, err := ws.CheckBooking(wfID, request)
ws.WorkflowExecution = executions
if !ok || err != nil {
return ws, nil, executions, errors.New("could not book the workflow : " + fmt.Sprintf("%v", err))
}
ws.Workflow = wf
var errCh = make(chan error, len(bookings))
var m sync.Mutex
for _, booking := range bookings {
go ws.BookExecs(booking, request, errCh, &m)
}
for i := 0; i < len(bookings); i++ {
if err := <- errCh ; err != nil {
return ws, wf, executions, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err))
}
}
fmt.Println("Schedules")
for _, exec := range executions {
err := exec.PurgeDraft(request)
if err != nil {
return ws, nil, []*WorkflowExecution{}, errors.New("purge draft" + fmt.Sprintf("%v", err))
}
exec.StoreDraftDefault()
utils.GenericStoreOne(exec, NewAccessor(request))
}
fmt.Println("Schedules")
return ws, wf, executions, nil
}
func (ws *WorkflowSchedule) BookExecs(booking *booking.Booking, request *tools.APIRequest, errCh chan error, m *sync.Mutex) {
m.Lock()
c, err := getCallerCopy(request, errCh)
if err != nil {
errCh <- err
return
}
m.Unlock()
l := logs.GetLogger().With().Str("SchedulerID", ws.UUID).Logger()
l.Debug().Msg("Booking " + booking.UUID + " on " + booking.DestPeerID)
_, err = (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "",
tools.BOOKING, tools.POST, booking.Serialize(booking), &c)
l.Debug().Msg("Received answer for booking " + booking.UUID + " on " + booking.DestPeerID)
if err != nil {
errCh <- err
return
}
errCh <- nil
}
/*
BOOKING IMPLIED TIME, not of subscription but of execution
so is processing time execution time applied on computes
data can improve the processing time
time should implied a security time border (10sec) if not from the same executions
VERIFY THAT WE HANDLE DIFFERENCE BETWEEN LOCATION TIME && BOOKING
*/
/*
* getExecutions is a function that returns the executions of a workflow
* it returns an array of workflow_execution.WorkflowExecution
*/
func (ws *WorkflowSchedule) getExecutions(workflow *workflow.Workflow) ([]*WorkflowExecution, error) {
workflows_executions := []*WorkflowExecution{}
dates, err := ws.getDates()
if err != nil {
return workflows_executions, err
}
for _, date := range dates {
obj := &WorkflowExecution{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(), // set the uuid of the execution
Name: workflow.Name + "_execution_" + date.Start.String(), // set the name of the execution
},
ExecutionsID: ws.UUID,
ExecDate: date.Start, // set the execution date
EndDate: date.End, // set the end date
State: enum.DRAFT, // set the state to 1 (scheduled)
WorkflowID: workflow.GetID(), // set the workflow id dependancy of the execution
}
workflows_executions = append(workflows_executions, obj)
}
return workflows_executions, nil
}
func (ws *WorkflowSchedule) getDates() ([]Schedule, error) {
schedule := []Schedule{}
if len(ws.Cron) > 0 { // if cron is set then end date should be set
if ws.End == nil {
return schedule, errors.New("a cron task should have an end date")
}
if ws.DurationS <= 0 {
ws.DurationS = ws.End.Sub(ws.Start).Seconds()
}
cronStr := strings.Split(ws.Cron, " ") // split the cron string to treat it
if len(cronStr) < 6 { // if the cron string is less than 6 fields, return an error because format is : ss mm hh dd MM dw (6 fields)
return schedule, errors.New("Bad cron message: (" + ws.Cron + "). Should be at least ss mm hh dd MM dw")
}
subCron := strings.Join(cronStr[:6], " ")
// cron should be parsed as ss mm hh dd MM dw t (min 6 fields)
specParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) // create a new cron parser
sched, err := specParser.Parse(subCron) // parse the cron string
if err != nil {
return schedule, errors.New("Bad cron message: " + err.Error())
}
// loop through the cron schedule to set the executions
for s := sched.Next(ws.Start); !s.IsZero() && s.Before(*ws.End); s = sched.Next(s) {
e := s.Add(time.Duration(ws.DurationS) * time.Second)
schedule = append(schedule, Schedule{
Start: s,
End: &e,
})
}
} else { // if no cron, set the execution to the start date
schedule = append(schedule, Schedule{
Start: ws.Start,
End: ws.End,
})
}
return schedule, nil
}
type Schedule struct {
Start time.Time
End *time.Time
}
/*
* TODO : LARGEST GRAIN PLANIFYING THE WORKFLOW WHEN OPTION IS SET
* SET PROTECTION BORDER TIME
*/

View File

@@ -0,0 +1,229 @@
// File: workspace_accessor_test.go
package workspace_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/models/workspace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockWorkspaceAccessor struct {
mock.Mock
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) {
args := m.Called(data)
return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return safeDBObject(args.Get(0)), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
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) {
args := m.Called(filters, search, isDraft)
return safeShallowList(args.Get(0)), args.Int(1), args.Error(2)
}
func TestStoreOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{Name: "test_ws"}}
mockAcc.On("StoreOne", ws).Return(ws, 200, nil)
res, code, err := mockAcc.StoreOne(ws)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
mockAcc.AssertExpectations(t)
}
func TestStoreOne_Conflict(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{Name: "duplicate"}}
mockAcc.On("StoreOne", ws).Return(nil, 409, errors.New("a workspace with the same name already exists"))
res, code, err := mockAcc.StoreOne(ws)
assert.Error(t, err)
assert.Equal(t, 409, code)
assert.Nil(t, res)
}
func TestUpdateOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "123", IsDraft: false}}
mockAcc.On("UpdateOne", ws, "123").Return(ws, 200, nil)
res, code, err := mockAcc.UpdateOne(ws, "123")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
}
func TestUpdateOne_Error(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "999"}}
err := errors.New("update failed")
mockAcc.On("UpdateOne", ws, "999").Return(nil, 500, err)
res, code, err := mockAcc.UpdateOne(ws, "999")
assert.Error(t, err)
assert.Equal(t, 500, code)
assert.Nil(t, res)
}
func TestDeleteOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "321"}}
mockAcc.On("DeleteOne", "321").Return(ws, 200, nil)
res, code, err := mockAcc.DeleteOne("321")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
}
func TestDeleteOne_NotFound(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
err := errors.New("not found")
mockAcc.On("DeleteOne", "notfound").Return(nil, 404, err)
res, code, err := mockAcc.DeleteOne("notfound")
assert.Error(t, err)
assert.Equal(t, 404, code)
assert.Nil(t, res)
}
func TestLoadOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "loadid"}}
mockAcc.On("LoadOne", "loadid").Return(ws, 200, nil)
res, code, err := mockAcc.LoadOne("loadid")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
}
func TestLoadOne_Error(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
err := errors.New("db error")
mockAcc.On("LoadOne", "badid").Return(nil, 500, err)
res, code, err := mockAcc.LoadOne("badid")
assert.Error(t, err)
assert.Equal(t, 500, code)
assert.Nil(t, res)
}
func TestLoadAll_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "all1"}}
mockAcc.On("LoadAll", true).Return([]utils.ShallowDBObject{ws}, 200, nil)
res, code, err := mockAcc.LoadAll(true)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Len(t, res, 1)
}
func TestLoadAll_Empty(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("LoadAll", false).Return([]utils.ShallowDBObject{}, 200, nil)
res, code, err := mockAcc.LoadAll(false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Empty(t, res)
}
func TestSearch_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
filters := &dbs.Filters{}
mockAcc.On("Search", filters, "keyword", true).Return([]utils.ShallowDBObject{}, 200, nil)
res, code, err := mockAcc.Search(filters, "keyword", true)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, res)
}
func TestSearch_Error(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
filters := &dbs.Filters{}
err := errors.New("search failed")
mockAcc.On("Search", filters, "fail", false).Return(nil, 500, err)
res, code, err := mockAcc.Search(filters, "fail", false)
assert.Error(t, err)
assert.Equal(t, 500, code)
assert.Nil(t, res)
}
// Additional edge test cases
func TestStoreOne_InvalidType(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("StoreOne", mock.Anything).Return(nil, 400, errors.New("invalid type"))
res, code, err := mockAcc.StoreOne(&utils.AbstractObject{})
assert.Error(t, err)
assert.Equal(t, 400, code)
assert.Nil(t, res)
}
func TestUpdateOne_NilData(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("UpdateOne", nil, "id").Return(nil, 400, errors.New("nil data"))
res, code, err := mockAcc.UpdateOne(nil, "id")
assert.Error(t, err)
assert.Equal(t, 400, code)
assert.Nil(t, res)
}
func TestDeleteOne_NilID(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("DeleteOne", "").Return(nil, 400, errors.New("missing ID"))
res, code, err := mockAcc.DeleteOne("")
assert.Error(t, err)
assert.Equal(t, 400, code)
assert.Nil(t, res)
}

View File

@@ -20,13 +20,13 @@ func (d *Workspace) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (ao *Workspace) VerifyAuth(request *tools.APIRequest) bool {
func (ao *Workspace) VerifyAuth(callName string, request *tools.APIRequest) bool {
if ao.Shared != "" {
shared, code, _ := shallow_collaborative_area.NewAccessor(request).LoadOne(ao.Shared)
if code != 200 || shared == nil {
return false
}
return shared.VerifyAuth(request)
return shared.VerifyAuth(callName, request)
}
return ao.AbstractObject.VerifyAuth(request)
return ao.AbstractObject.VerifyAuth(callName, request)
}

View File

@@ -13,7 +13,7 @@ import (
// Workspace is a struct that represents a workspace
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
@@ -28,10 +28,11 @@ func NewAccessor(request *tools.APIRequest) *workspaceMongoAccessor {
// New creates a new instance of the workspaceMongoAccessor
func new(t tools.DataType, request *tools.APIRequest) *workspaceMongoAccessor {
return &workspaceMongoAccessor{
utils.AbstractAccessor{
utils.AbstractAccessor[*Workspace]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
Type: t,
New: func() *Workspace { return &Workspace{} },
},
}
}
@@ -87,13 +88,8 @@ func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject,
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) {
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())
return d, 200, nil
}, a)
@@ -130,7 +126,9 @@ func (a *workspaceMongoAccessor) share(realData *Workspace, method tools.METHOD,
paccess := &peer.Peer{}
for _, p := range res.(*shallow_collaborative_area.ShallowCollaborativeArea).Peers {
paccess.UUID = p
if ok, _ := paccess.IsMySelf(); ok { // If the peer is the current peer, never share because it will create a loop
if ok, _ := utils.IsMySelf(p, paccess.GetAccessor(&tools.APIRequest{
Admin: true,
})); ok { // If the peer is the current peer, never share because it will create a loop
continue
}
if method == tools.DELETE { // If the workspace is deleted, share the deletion

View File

@@ -7,6 +7,7 @@ import (
"cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs"
beego "github.com/beego/beego/v2/server/web"
)
@@ -15,6 +16,7 @@ type APIRequest struct {
PeerID string
Groups []string
Caller *HTTPCaller
Admin bool
}
/*
@@ -59,7 +61,9 @@ func (s State) String() string {
type API struct{}
func (a *API) Discovered(infos []*beego.ControllerInfo) {
respondToDiscovery := func(m map[string]interface{}) {
respondToDiscovery := func(resp NATSResponse) {
var m map[string]interface{}
json.Unmarshal(resp.Payload, &m)
if len(m) == 0 {
a.SubscribeRouter(infos)
}
@@ -89,8 +93,10 @@ func (a *API) GetState() (State, int, error) {
return ALIVE, 200, nil // If everything is up, return alive
}
func (a *API) ListenRouter(exec func(msg map[string]interface{})) {
go NewNATSCaller().ListenNats(DISCOVERY.GenerateKey("api"), exec)
func (a *API) ListenRouter(exec func(msg NATSResponse)) {
go NewNATSCaller().ListenNats(map[NATSMethod]func(msg NATSResponse){
DISCOVERY: exec,
})
}
func (a *API) SubscribeRouter(infos []*beego.ControllerInfo) {
@@ -109,14 +115,21 @@ func (a *API) SubscribeRouter(infos []*beego.ControllerInfo) {
}
}
}
nats.SetNATSPub("api", DISCOVERY, discovery)
b, _ := json.Marshal(discovery)
go nats.SetNATSPub(DISCOVERY, NATSResponse{
FromApp: beego.AppPath,
Datatype: -1,
Method: int(DISCOVERY),
Payload: b,
})
}
// CheckRemotePeer checks the state of a remote peer
func (a *API) CheckRemotePeer(url string) (State, map[string]int) {
// Check if the database is up
var resp APIStatusResponse
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
b, err := caller.CallPost(url, "", map[string]interface{}{}) // Call the status endpoint of the peer
if err != nil {
return DEAD, map[string]int{} // If the peer is not reachable, return dead
@@ -135,6 +148,7 @@ func (a *API) CheckRemotePeer(url string) (State, map[string]int) {
// CheckRemoteAPIs checks the state of remote APIs from your proper OC
func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error) {
// Check if the database is up
l := logs.GetLogger()
new := map[string]string{}
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
code := 0
@@ -143,8 +157,9 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
reachable := false
for _, api := range apis { // Check the state of each remote API in the list
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 {
l.Error().Msg(api.String() + " not reachable")
state = REDUCED_SERVICE // If a remote API is not reachable, return reduced service
continue
}
@@ -161,6 +176,7 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
reachable = true // If the remote API is reachable, set reachable to true cause we are not dead
}
if !reachable {
l.Error().Msg("Peer check returned no answers")
state = DEAD // If no remote API is reachable, return dead, nobody is alive
}
if code > 0 {

51
tools/crypto.go Normal file
View File

@@ -0,0 +1,51 @@
package tools
import (
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"cloud.o-forge.io/core/oc-lib/config"
"github.com/libp2p/go-libp2p/core/crypto"
)
func LoadKeyFromFilePrivate() (crypto.PrivKey, error) {
path := config.GetConfig().PrivateKeyPath
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
edKey, ok := keyAny.(ed25519.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an ed25519 key")
}
return crypto.UnmarshalEd25519PrivateKey(edKey)
}
func LoadKeyFromFilePublic() (crypto.PubKey, error) {
path := config.GetConfig().PublicKeyPath
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
keyAny, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
edKey, ok := keyAny.(ed25519.PublicKey)
if !ok {
return nil, fmt.Errorf("not an ed25519 key")
}
// Try to unmarshal as libp2p private key (supports ed25519, rsa, etc.)
return crypto.UnmarshalEd25519PublicKey(edKey)
}

View File

@@ -1,5 +1,11 @@
package tools
import (
"strings"
"cloud.o-forge.io/core/oc-lib/config"
)
type DataType int
// DataType - Enum for the different types of resources in db accessible from the outside
@@ -21,28 +27,39 @@ const (
WORKSPACE_HISTORY
ORDER
PURCHASE_RESOURCE
ADMIRALTY_SOURCE
ADMIRALTY_TARGET
ADMIRALTY_SECRET
ADMIRALTY_KUBECONFIG
ADMIRALTY_NODES
LIVE_DATACENTER
LIVE_STORAGE
BILL
NATIVE_TOOL
)
var NOAPI = ""
var CATALOGAPI = "oc-catalog"
var SHAREDAPI = "oc-shared"
var WORKFLOWAPI = "oc-workflow"
var WORKSPACEAPI = "oc-workspace"
var PEERSAPI = "oc-peer"
var DATACENTERAPI = "oc-datacenter"
var ADMIRALTY_SOURCEAPI = DATACENTERAPI+"/admiralty/source"
var ADMIRALTY_TARGETAPI = DATACENTERAPI+"/admiralty/target"
var ADMIRALTY_SECRETAPI = DATACENTERAPI+"/admiralty/secret"
var ADMIRALTY_KUBECONFIGAPI = DATACENTERAPI+"/admiralty/kubeconfig"
var ADMIRALTY_NODESAPI = DATACENTERAPI+"/admiralty/node"
var NOAPI = func() string {
return ""
}
var CATALOGAPI = func() string {
return config.GetConfig().InternalCatalogAPI
}
var SHAREDAPI = func() string {
return config.GetConfig().InternalSharedAPI
}
var WORKFLOWAPI = func() string {
return config.GetConfig().InternalWorkflowAPI
}
var WORKSPACEAPI = func() string {
return config.GetConfig().InternalWorkspaceAPI
}
var PEERSAPI = func() string {
return config.GetConfig().InternalPeerAPI
}
var DATACENTERAPI = func() string {
return config.GetConfig().InternalDatacenterAPI
}
var PURCHASEAPI = func() string {
return config.GetConfig().InternalCatalogAPI + "/purchase"
}
// Bind the standard API name to the data type
var DefaultAPI = [...]string{
var InnerDefaultAPI = [...]func() string{
NOAPI,
CATALOGAPI,
CATALOGAPI,
@@ -59,12 +76,11 @@ var DefaultAPI = [...]string{
NOAPI,
NOAPI,
NOAPI,
PURCHASEAPI,
DATACENTERAPI,
DATACENTERAPI,
NOAPI,
ADMIRALTY_SOURCEAPI,
ADMIRALTY_TARGETAPI,
ADMIRALTY_SECRETAPI,
ADMIRALTY_KUBECONFIGAPI,
ADMIRALTY_NODESAPI,
CATALOGAPI,
}
// Bind the standard data name to the data type
@@ -86,22 +102,33 @@ var Str = [...]string{
"workspace_history",
"order",
"purchase_resource",
"admiralty_source",
"admiralty_target",
"admiralty_secret",
"admiralty_kubeconfig",
"admiralty_node",
"live_datacenter",
"live_storage",
"bill",
"native_tool",
}
func FromString(comp string) int {
for i, str := range Str {
if str == comp {
return i
}
}
return -1
}
func FromInt(i int) string {
return Str[i]
}
func (d DataType) API() string { // API - Returns the API name of the data type
return DefaultAPI[d]
func (d DataType) InnerAPI() string { // API - Returns the API name of the data type
return InnerDefaultAPI[d]()
}
func (d DataType) String() string { // String - Returns the string name of the data type
if d < 0 {
return ""
}
return Str[d]
}
@@ -111,5 +138,63 @@ func (d DataType) EnumIndex() int {
}
func DataTypeList() []DataType {
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, ORDER, PURCHASE_RESOURCE,ADMIRALTY_SOURCE,ADMIRALTY_TARGET,ADMIRALTY_SECRET,ADMIRALTY_KUBECONFIG,ADMIRALTY_NODES}
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,
ORDER, PURCHASE_RESOURCE,
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL}
}
type PropalgationMessage struct {
DataType int `json:"datatype"`
Action PubSubAction `json:"action"`
Payload []byte `json:"payload"`
}
type PubSubAction int
const (
PB_SEARCH PubSubAction = iota
PB_SEARCH_RESPONSE
PB_CREATE
PB_UPDATE
PB_DELETE
PB_PLANNER
PB_CLOSE_PLANNER
PB_CONSIDERS
PB_ADMIRALTY_CONFIG
PB_MINIO_CONFIG
NONE
)
func GetActionString(ss string) PubSubAction {
switch ss {
case "search":
return PB_SEARCH
case "create":
return PB_CREATE
case "update":
return PB_UPDATE
case "delete":
return PB_DELETE
case "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
default:
return NONE
}
}
var path = []string{"search", "search_response", "create", "update", "delete", "planner", "close_planner", "considers", "admiralty_config", "minio_config"}
func (m PubSubAction) String() string {
return strings.ToUpper(path[m])
}

628
tools/kubernetes.go Normal file
View File

@@ -0,0 +1,628 @@
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) {
config := &rest.Config{
Host: host,
TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(ca),
CertData: []byte(cert),
KeyData: []byte(data),
},
}
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) {
config := &rest.Config{
Host: host,
TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(ca),
CertData: []byte(cert),
KeyData: []byte(data),
},
}
// Create clientset
clientset, err := kubernetes.NewForConfig(config)
fmt.Println("NewForConfig", clientset, err)
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 NewRemoteKubernetesService(url string, ca string, cert string, key string) (*KubernetesService, error) {
decodedCa, _ := base64.StdEncoding.DecodeString(ca)
decodedCert, _ := base64.StdEncoding.DecodeString(cert)
decodedKey, _ := base64.StdEncoding.DecodeString(key)
config := &rest.Config{
Host: url + ":6443",
TLSClientConfig: rest.TLSClientConfig{
CAData: decodedCa,
CertData: decodedCert,
KeyData: decodedKey,
},
}
// Create clientset
clientset, err := kubernetes.NewForConfig(config)
fmt.Println("NewForConfig", clientset, err)
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: url,
CA: string(decodedCa),
Cert: string(decodedCert),
Data: string(decodedKey),
}, 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...", k.Set)
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
}
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
}
fmt.Println(string(resp))
var targetDict map[string]interface{}
err = json.Unmarshal(resp, &targetDict)
if err != nil {
fmt.Println("TODO: handle the error when unmarshalling k8s API response")
return nil, err
}
b, _ := json.MarshalIndent(targetDict, "", " ")
fmt.Println(string(b))
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")
fmt.Println(err)
return nil, err
}
secretApplyConfig := apply.Secret("kube-secret-"+GetConcatenatedName(peerId, executionId),
executionId).
WithData(map[string][]byte{
"config": config,
},
)
// exists, err := k.GetKubeconfigSecret(context,executionId)
// if err != nil {
// fmt.Println("Error verifying if kube secret exists in namespace ", executionId)
// return nil, err
// }
// if exists != nil {
// fmt.Println("kube-secret already exists in namespace", executionId)
// fmt.Println("Overriding existing kube-secret with a newer resource")
// // TODO : implement DeleteKubeConfigSecret(executionID)
// deleted, err := k.DeleteKubeConfigSecret(executionId)
// _ = deleted
// _ = err
// }
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)
fmt.Println(err)
return nil, err
}
data, err := json.Marshal(resp)
if err != nil {
fmt.Println("Couldn't marshal resp from : ", data)
fmt.Println(err)
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)
fmt.Println(err)
return nil, err
}
data, err := json.Marshal(resp)
if err != nil {
fmt.Println("Couldn't marshal resp from : ", data)
fmt.Println(err)
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 {
o, err := json.Marshal(object)
fmt.Println("Error from k8s API when applying "+fmt.Sprint(string(o))+" 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")
fmt.Println(err)
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

@@ -2,25 +2,57 @@ package tools
import (
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/logs"
"github.com/nats-io/nats.go"
"github.com/rs/zerolog"
)
type NATSResponse struct {
FromApp string `json:"from_app"`
Datatype DataType `json:"datatype"`
User string `json:"user"`
Method int `json:"method"`
SearchAttr string `json:"search_attr"`
Payload []byte `json:"payload"`
}
// NATS Method Enum defines the different methods that can be used to interact with the NATS server
type NATSMethod int
var meths = []string{"remove execution", "create execution", "discovery",
"workflow event", "argo kube event", "create resource", "remove resource",
"propalgation event", "search event",
}
const (
REMOVE NATSMethod = iota
CREATE
REMOVE_EXECUTION NATSMethod = iota
CREATE_EXECTUTION
DISCOVERY
WORKFLOW_EVENT
ARGO_KUBE_EVENT
CREATE_RESOURCE
REMOVE_RESOURCE
PROPALGATION_EVENT
SEARCH_EVENT
)
func (n NATSMethod) String() string {
return meths[n]
}
// NameToMethod returns the NATSMethod enum value from a string
func NameToMethod(name string) NATSMethod {
for _, v := range [...]NATSMethod{REMOVE, CREATE} {
for _, v := range [...]NATSMethod{REMOVE_EXECUTION, CREATE_EXECTUTION, DISCOVERY, WORKFLOW_EVENT, ARGO_KUBE_EVENT,
CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT} {
if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) {
return v
}
@@ -29,13 +61,8 @@ func NameToMethod(name string) NATSMethod {
}
// GenerateKey generates a key for the NATSMethod usefull for standard key based on data name & method
func (d NATSMethod) GenerateKey(name string) string {
return name + "_" + d.String()
}
// String returns the string of the enum
func (d NATSMethod) String() string {
return [...]string{"remove", "create", "discovery"}[d]
func (d NATSMethod) GenerateKey() string {
return strings.ReplaceAll(d.String(), " ", "_")
}
type natsCaller struct{}
@@ -47,48 +74,78 @@ func NewNATSCaller() *natsCaller {
// on workflows' scheduling. Messages must contain
// workflow execution ID, to allow retrieval of execution infos
func (s *natsCaller) ListenNats(chanName string, exec func(msg map[string]interface{})) {
func (s *natsCaller) ListenNats(execs map[NATSMethod]func(NATSResponse)) {
log := logs.GetLogger()
if config.GetConfig().NATSUrl == "" {
log.Error().Msg(" -> NATS_SERVER is not set")
return
}
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
log.Error().Msg(" -> Could not reach NATS server : " + err.Error())
return
}
ch := make(chan *nats.Msg, 64)
subs, err := nc.ChanSubscribe(chanName, ch)
if err != nil {
log.Error().Msg("Error listening to NATS : " + err.Error())
}
defer subs.Unsubscribe()
for msg := range ch {
map_mess := map[string]interface{}{}
json.Unmarshal(msg.Data, &map_mess)
exec(map_mess)
for {
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
log.Error().Msg("Could not connect to NATS")
time.Sleep(1 * time.Minute)
continue
}
defer nc.Close()
var wg sync.WaitGroup
wg.Add(len(execs))
for k, v := range execs {
go s.listenForChange(log, nc, k, v, &wg)
}
wg.Wait()
break
}
}
// SetNATSPub sets a message to the NATS server
func (o *natsCaller) SetNATSPub(dataName string, method NATSMethod, data interface{}) string {
func (o *natsCaller) SetNATSPub(method NATSMethod, data NATSResponse) string {
if config.GetConfig().NATSUrl == "" {
return " -> NATS_SERVER is not set"
}
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
return " -> Could not reach NATS server : " + err.Error()
}
defer nc.Close()
js, err := json.Marshal(data)
if err != nil {
return " -> " + err.Error()
}
err = nc.Publish(method.GenerateKey(dataName), js) // Publish the message on the NATS server with a channel name based on the data name (or whatever start) and the method
if err != nil {
return " -> " + err.Error() // Return an error if the message could not be published
for {
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
fmt.Println("NATS Connect err", err)
time.Sleep(1 * time.Minute)
continue
}
defer nc.Close()
js, err := json.Marshal(data)
if err != nil {
fmt.Println("NATS Marshal err", err)
return " -> " + err.Error()
}
err = nc.Publish(method.GenerateKey(), js) // Publish the message on the NATS server with a channel name based on the data name (or whatever start) and the method
if err != nil {
fmt.Println("Publish Failed", err)
time.Sleep(1 * time.Minute)
continue
}
fmt.Println("Published on", method.GenerateKey())
break
}
return ""
}
// Goroutine listening to a NATS server for updates
// on workflows' scheduling. Messages must contain
// workflow execution ID, to allow retrieval of execution infos
func (o *natsCaller) listenForChange(logger zerolog.Logger, nc *nats.Conn, natsTools NATSMethod,
function func(NATSResponse), wg *sync.WaitGroup) {
defer wg.Done()
ch := make(chan *nats.Msg, 64)
logger.Info().Msg("Listening to " + natsTools.GenerateKey())
subs, err := nc.ChanSubscribe(natsTools.GenerateKey(), ch)
if err != nil {
logger.Error().Msg("Error listening to NATS : " + err.Error())
}
defer subs.Unsubscribe()
for msg := range ch {
var resp NATSResponse
json.Unmarshal(msg.Data, &resp)
logger.Info().Msg("Catching " + natsTools.String() + "... " + resp.FromApp + " - " + resp.Datatype.String())
function(resp)
}
}

View File

@@ -47,12 +47,17 @@ func ToMethod(str string) METHOD {
return GET
}
var HTTPCallerInstance = &HTTPCaller{} // Singleton instance of the HTTPCaller
type HTTPCallerITF interface {
GetUrls() map[DataType]map[METHOD]string
CallGet(url string, subpath string, types ...string) ([]byte, error)
CallPost(url string, subpath string, body interface{}, types ...string) ([]byte, error)
CallDelete(url string, subpath string) ([]byte, error)
}
type HTTPCaller struct {
URLS map[DataType]map[METHOD]string // Map of the different methods and their urls
Disabled bool // Disabled flag
LastResults map[string]interface{} // Used to store information regarding the last execution of a given method on a given data type
URLS map[DataType]map[METHOD]string // Map of the different methods and their urls
Disabled bool // Disabled flag
LastResults map[string]interface{} // Used to store information regarding the last execution of a given method on a given data type
}
// NewHTTPCaller creates a new instance of the HTTP Caller
@@ -63,8 +68,12 @@ func NewHTTPCaller(urls map[DataType]map[METHOD]string) *HTTPCaller {
}
}
// Creates a copy of the current caller, in order to have parallelized executions without race condition
func (c* HTTPCaller) DeepCopy(dst HTTPCaller) error {
func (c *HTTPCaller) GetUrls() map[DataType]map[METHOD]string {
return c.URLS
}
// Creates a copy of the current caller, in order to have parallelized executions without race condition
func (c *HTTPCaller) DeepCopy(dst HTTPCaller) error {
bytes, err := json.Marshal(c)
if err != nil {
return err
@@ -104,7 +113,7 @@ func (caller *HTTPCaller) CallDelete(url string, subpath string) ([]byte, error)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || req == nil || req.Body == nil {
if err != nil {
return nil, err
}
defer resp.Body.Close()
@@ -219,4 +228,4 @@ func (caller *HTTPCaller) StoreResp(resp *http.Response) error {
caller.LastResults["body"] = data
return nil
}
}

View File

@@ -0,0 +1,227 @@
package tools
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"cloud.o-forge.io/core/oc-lib/tools"
)
func TestMethodString(t *testing.T) {
tests := []struct {
method tools.METHOD
expected string
}{
{tools.GET, "GET"},
{tools.PUT, "PUT"},
{tools.POST, "POST"},
{tools.POSTCHECK, "POST"},
{tools.DELETE, "DELETE"},
{tools.STRICT_INTERNAL_GET, "INTERNALGET"},
{tools.STRICT_INTERNAL_PUT, "INTERNALPUT"},
{tools.STRICT_INTERNAL_POST, "INTERNALPOST"},
{tools.STRICT_INTERNAL_DELETE, "INTERNALDELETE"},
}
for _, test := range tests {
if test.method.String() != test.expected {
t.Errorf("Expected %s, got %s", test.expected, test.method.String())
}
}
}
func TestToMethod(t *testing.T) {
method := tools.ToMethod("INTERNALPUT")
if method != tools.STRICT_INTERNAL_PUT {
t.Errorf("Expected STRICT_INTERNAL_PUT, got %v", method)
}
defaultMethod := tools.ToMethod("INVALID")
if defaultMethod != tools.GET {
t.Errorf("Expected default GET, got %v", defaultMethod)
}
}
func TestEnumIndex(t *testing.T) {
if tools.GET.EnumIndex() != 0 {
t.Errorf("Expected index 0 for GET, got %d", tools.GET.EnumIndex())
}
}
func TestCallGet(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`"ok"`))
}))
defer ts.Close()
caller := &tools.HTTPCaller{}
body, err := caller.CallGet(ts.URL, "/test", "application/json")
if err != nil || string(body) != `"ok"` {
t.Errorf("Expected body to be ok, got %s", string(body))
}
}
func TestCallPost(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, r.Body)
}))
defer ts.Close()
caller := &tools.HTTPCaller{}
body, err := caller.CallPost(ts.URL, "/post", map[string]string{"key": "val"})
if err != nil || !strings.Contains(string(body), "key") {
t.Errorf("POST failed, body: %s", string(body))
}
}
func TestCallPut(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, r.Body)
}))
defer ts.Close()
caller := &tools.HTTPCaller{}
body, err := caller.CallPut(ts.URL, "/put", map[string]interface{}{"foo": "bar"})
if err != nil || !strings.Contains(string(body), "foo") {
t.Errorf("PUT failed, body: %s", string(body))
}
}
func TestCallDelete(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`deleted`))
}))
defer ts.Close()
caller := &tools.HTTPCaller{}
body, err := caller.CallDelete(ts.URL, "/delete")
if err != nil || string(body) != "deleted" {
t.Errorf("DELETE failed, body: %s", string(body))
}
}
func TestCallRaw(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
caller := &tools.HTTPCaller{}
resp, err := caller.CallRaw("POST", ts.URL, "/", map[string]interface{}{"a": 1}, "application/json", true)
if err != nil || resp.StatusCode != http.StatusOK {
t.Errorf("CallRaw failed")
}
}
func TestCallForm(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("Expected POST, got %s", r.Method)
}
}))
defer ts.Close()
caller := &tools.HTTPCaller{}
form := url.Values{}
form.Set("foo", "bar")
_, err := caller.CallForm("POST", ts.URL, "/form", form, "application/x-www-form-urlencoded", true)
if err != nil {
t.Errorf("CallForm error: %v", err)
}
}
func TestStoreResp(t *testing.T) {
resp := &http.Response{
Header: http.Header{},
StatusCode: 200,
Body: io.NopCloser(bytes.NewBuffer([]byte("body content"))),
}
caller := &tools.HTTPCaller{}
err := caller.StoreResp(resp)
if err != nil {
t.Errorf("StoreResp failed: %v", err)
}
if string(caller.LastResults["body"].([]byte)) != "body content" {
t.Errorf("Expected body content")
}
}
func TestNewHTTPCaller(t *testing.T) {
c := tools.NewHTTPCaller(nil)
if c.Disabled != false {
t.Errorf("Expected Disabled false")
}
}
func TestGetUrls(t *testing.T) {
urls := map[tools.DataType]map[tools.METHOD]string{}
c := tools.NewHTTPCaller(urls)
if c.GetUrls() == nil {
t.Errorf("GetUrls returned nil")
}
}
func TestDeepCopy(t *testing.T) {
original := tools.NewHTTPCaller(nil)
copy := tools.HTTPCaller{}
err := original.DeepCopy(copy)
if err != nil {
t.Errorf("DeepCopy failed: %v", err)
}
}
func TestCallPost_InvalidJSON(t *testing.T) {
caller := &tools.HTTPCaller{}
_, err := caller.CallPost("http://invalid", "/post", func() {})
if err == nil {
t.Error("Expected error when marshaling unsupported type")
}
}
func TestCallPut_ErrorOnNewRequest(t *testing.T) {
caller := &tools.HTTPCaller{}
_, err := caller.CallPut("http://[::1]:namedport", "/put", nil)
if err == nil {
t.Error("Expected error from invalid URL")
}
}
func TestCallGet_Error(t *testing.T) {
caller := &tools.HTTPCaller{}
_, err := caller.CallGet("http://[::1]:namedport", "/bad", "application/json")
if err == nil {
t.Error("Expected error from invalid URL")
}
}
func TestCallDelete_Error(t *testing.T) {
caller := &tools.HTTPCaller{}
_, err := caller.CallDelete("http://[::1]:namedport", "/bad")
if err == nil {
t.Error("Expected error from invalid URL")
}
}
func TestCallRaw_Error(t *testing.T) {
caller := &tools.HTTPCaller{}
_, err := caller.CallRaw("POST", "http://[::1]:namedport", "/raw", nil, "application/json", false)
if err == nil {
t.Error("Expected error from invalid URL")
}
}
func TestCallForm_Error(t *testing.T) {
caller := &tools.HTTPCaller{}
_, err := caller.CallForm("POST", "http://[::1]:namedport", "/form", url.Values{}, "application/json", false)
if err == nil {
t.Error("Expected error from invalid URL")
}
}