Compare commits
390 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26948da3c1 | |||
| 4e1b1164cc | |||
| 73b844f664 | |||
| cef23b5f30 | |||
| e6a9558cbf | |||
| 9bb3d897b3 | |||
| 47d487ea80 | |||
| a8b7d4d0bc | |||
| 7a12506531 | |||
| f926a42066 | |||
| e3fbe7688a | |||
| 318fd52289 | |||
| 26fc02c5b2 | |||
| f048b420d7 | |||
| 0b54d6640d | |||
| 7b3b9cb7bf | |||
| d9b1ad8dde | |||
| d6106dacde | |||
| 365a1d670c | |||
| 25880077d1 | |||
| 560c997bf1 | |||
| 747368c79a | |||
| e5e5706834 | |||
| b9ad5d5ea7 | |||
| e70e89b630 | |||
| 9c2663601a | |||
| 538496cd60 | |||
| a4366d3a09 | |||
| 51e2dcc404 | |||
| c208e2ccef | |||
| 5cda4fdd40 | |||
| b92634ccba | |||
| da237b1d26 | |||
| 94e3ebbdd9 | |||
| 6741e929cc | |||
| a08c9b084d | |||
| 17a45eb5d1 | |||
| 0c6efee276 | |||
| bbaea4fec4 | |||
| d57ee0b5e7 | |||
| 50a5e90f33 | |||
| 5cc04ee490 | |||
| 883c0bec3d | |||
| dc0041999d | |||
| a653f9495b | |||
| d7b2ef6ae1 | |||
| 878885c8c8 | |||
| c340146c8d | |||
| 92eb2663bc | |||
| 284533ad1d | |||
| dbbad79480 | |||
| 046bde17d4 | |||
| 6fe91eda87 | |||
| 526eaef33a | |||
| b7ee6d8e7f | |||
| 5dbe55e630 | |||
| 2e9f4cb9f4 | |||
| 3ad0a69f54 | |||
| 2a6d3880cd | |||
| 316ebc93f9 | |||
| 913d9b3dfb | |||
| 450e917227 | |||
| 54985bbc45 | |||
| 4f0714cb11 | |||
| a2f6f3c252 | |||
| 2bc4555793 | |||
| ad12f02a70 | |||
| 20cac09f9d | |||
| f3b5a54545 | |||
| c0722483b8 | |||
| 0aee593f29 | |||
| a4ab3285e3 | |||
| 45f2351b2f | |||
| 39cb1c715c | |||
| 87cf2cb12a | |||
| 4580200e80 | |||
| 6d0c78946e | |||
| 211339947c | |||
| b76b22a8fb | |||
| fa9893e150 | |||
| 14b449f547 | |||
| 5b197c91e0 | |||
| 5bdd2554a7 | |||
| ea2a98d84a | |||
| b82b955045 | |||
| 88d2e52628 | |||
| 9f861e5b8d | |||
| e4506f3b42 | |||
| 75d08aae7c | |||
| b288085f32 | |||
| bd3e81be0c | |||
| fafa1186c2 | |||
| 471eaff94c | |||
| c9fcabac6e | |||
| 478e68e6d4 | |||
| 5619010838 | |||
| f1a9214ac7 | |||
| e6eb516f39 | |||
| 1508cc3611 | |||
| 2abc035ec0 | |||
| c34b8c6703 | |||
| a62fbc6c7a | |||
| 6e28dce02c | |||
| fe3b185b60 | |||
| 6641d38d9d | |||
| 93ad8db9a8 | |||
| 4eb53917b8 | |||
| c7884f5cde | |||
| 5fca0480af | |||
| 28b5b7d39f | |||
| 5b7edb53a9 | |||
| 5976795d44 | |||
| 3d22ff40fb | |||
| 889656a95e | |||
| c66fbc809e | |||
| 1a37a1b4aa | |||
| d4ac398cdb | |||
| 4eb112bee3 | |||
| d1214fe622 | |||
| 6a907236fa | |||
| 85314baac3 | |||
| cec8033ddc | |||
| d0645f5ca7 | |||
| c39bc52312 | |||
| 0a87343e3e | |||
| 96beaade24 | |||
| 5753450965 | |||
| 7f8d697e4c | |||
| 94837f8d24 | |||
| e758144b46 | |||
| 72be3118b7 | |||
| 67778e1e47 | |||
| 562dfb18c1 | |||
| 2a2dd96870 | |||
| 333476e2c5 | |||
| 0fd2513278 | |||
| e79101f58d | |||
| b3dbc7687e | |||
| 8fd4f5faef | |||
| f7012e285f | |||
| 088b45b2cf | |||
| 1ac735cef1 | |||
| 65237f0d1f | |||
| 9b2f945176 | |||
| b110cbc260 | |||
| a4d81cbb67 | |||
| 9bf2c566e9 | |||
| 6d8efd137a | |||
| 40a986af41 | |||
| 4a076ba237 | |||
| deb819c5af | |||
| 55a039bd66 | |||
| a86e78841b | |||
| 48f034316b | |||
| 9e5102893f | |||
| 465b91fd6e | |||
| 12ba346427 | |||
| 2cdf15d722 | |||
| aeebd8b5b2 | |||
| e355af2bac | |||
| a335c905b3 | |||
| a30173921f | |||
| e28b79ac0d | |||
| 9645e71b54 | |||
| 9f514a133e | |||
| f5e1991324 | |||
| d7a8f2adaa | |||
| 7d9addf760 | |||
| 2c9c42dd51 | |||
| 97bfb0582a | |||
| 933b7147e9 | |||
| e03a0d3dd0 | |||
| 340f2a6301 | |||
| a426bdf655 | |||
| 2bfcfb5736 | |||
| 5d18512f67 | |||
| 66ee4156e2 | |||
| f1eaf497aa | |||
| b47b51126a | |||
| 473dc62660 | |||
| 334de8ca2e | |||
| ae7e297622 | |||
| 3e0f369850 | |||
| 6217618e6c | |||
| f033182382 | |||
| 542b0b73ab | |||
| 44812309db | |||
| cb3771c17a | |||
| f4e2d8057d | |||
| 959fce48ef | |||
| ce8ef70516 | |||
| d18b031a29 | |||
| 0f6aa1fe78 | |||
| a9ebad78f3 | |||
| 54aef164ba | |||
| ff830065ec | |||
| e039fa56b6 | |||
| e10bb55455 | |||
| f28e2c3620 | |||
| b08bbf51dd | |||
| 5d32b4646a | |||
| 25e4e67111 | |||
| 19b0f10e71 | |||
| 12c506e9a7 | |||
| 2871353635 | |||
| 59923ac5c1 | |||
| da8b8ec397 | |||
| 9afbbb5c82 | |||
| 9662ac6d67 | |||
| 0b41e2505e | |||
| fa5c3a3c60 | |||
| 842e09f22f | |||
| 403913d8cf | |||
| 7e8546bbea | |||
| 1895b7ac8a | |||
| 3bcf0da56a | |||
| b9c9b66780 | |||
| b767afb301 | |||
| bafeee0d05 | |||
| c2aa2fedaa | |||
| b9e7ce20b6 | |||
| 543d28e5d2 | |||
| 04d25c7ef1 | |||
| c8e98bea61 | |||
| 2cf960e4f7 | |||
| e32ba1e37a | |||
| 5798e16263 | |||
| 342451db25 | |||
| 425cd2a9ba | |||
| 9e30133628 | |||
| d9f646aac2 | |||
| ef916fe2d9 | |||
| 4258f6b580 | |||
| 06ad584b6a | |||
| c650f08b8e | |||
| c90b55f312 | |||
| 4f28b9b589 | |||
| 30e6c9a618 | |||
| 186ba3e689 | |||
| c1519f6b26 | |||
| 97cf629e27 | |||
| c0d89ea9e1 | |||
| 7ccd7fe16b | |||
| 886f9d15ba | |||
| d26789d64e | |||
| 7911cf29de | |||
| 743f4a6ff7 | |||
| d2c5d20318 | |||
| e3fe49c239 | |||
| 1c9d7b63c0 | |||
| d098d253d8 | |||
| ecb734187a | |||
| 3c052bf165 | |||
| 9af8d15672 | |||
| 4d57767005 | |||
| 9b4f9e420a | |||
| d772a703da | |||
| c69069449f | |||
| 643beacd4b | |||
| 802786daa7 | |||
| b35d4e2b36 | |||
| 1f93493965 | |||
| 055e6c70cd | |||
| 85a8857938 | |||
| bc94f2b188 | |||
| 6b12aa1713 | |||
| f3d7c65b18 | |||
| d06c9e9337 | |||
| 0308b4ea10 | |||
| b71b1e741d | |||
| 8f5f3e331d | |||
| 2d1d76767c | |||
| 00bcca379f | |||
| b987286759 | |||
| c72954d2f7 | |||
| bcfd43e140 | |||
| a4512e4da6 | |||
| 1c3b9218f7 | |||
| 7c5d5c491f | |||
| 76eb167c5b | |||
| fa5b754333 | |||
| 0e378dc19c | |||
| 5cdfc28d2f | |||
| 6d745fe922 | |||
| c35b06e0bc | |||
| be770ec763 | |||
| 27f295f17e | |||
| 33b8d2799a | |||
| 188b758f7a | |||
| f4b0cf5683 | |||
| e7a71188a3 | |||
| 40a61387b9 | |||
| cc939451fd | |||
| 76e9b2562e | |||
| cc3091d401 | |||
| 3ddbf1a967 | |||
| be2a1cc114 | |||
| a093369dc5 | |||
| 76d83878eb | |||
| e735f78e58 | |||
| 98a2359c9d | |||
| 83e590d4e1 | |||
| 4e3ff9aa08 | |||
| 424d523c5e | |||
| 346275e12c | |||
| 6ab774cc43 | |||
| 2748b59221 | |||
| 34f01e9740 | |||
| dcdc6ff1d9 | |||
| 365b924e4b | |||
| e7e56d1859 | |||
| 443546027b | |||
| 1c4f3f756f | |||
| 3971d5ca5d | |||
| e95d1aa53b | |||
| 1ab2bd2153 | |||
| d35ad440fa | |||
| d58dc56024 | |||
| 34b7cdcf06 | |||
| af0d7807bc | |||
| e600fedcab | |||
| 147c7bc3a1 | |||
| 3fdf5c3ebf | |||
| cd177bd779 | |||
| 2c8dcbe93d | |||
| e84d262f38 | |||
| 29b192211d | |||
| 583ca2fbac | |||
| 82d25b0bee | |||
| 181b3249b8 | |||
| 8b38249df7 | |||
| 01af8237db | |||
| 2f4884c655 | |||
| c9ee2a1d24 | |||
| 8d5ba6a5e4 | |||
| d3cfe019e3 | |||
| 4c2ecd3f41 | |||
| d8ccdec501 | |||
| 938f9f1326 | |||
| 29bc21735d | |||
| ec7a7e4746 | |||
| 0b0952b28c | |||
| 9e52663261 | |||
| 8f2adb76e4 | |||
| 0d6c329477 | |||
| 1c751f7253 | |||
| 2264d22c69 | |||
| 9fe72ea96e | |||
| 48299810e0 | |||
| 2a0ab8e549 | |||
| 23a9d648d2 | |||
| a3029fa3f9 | |||
| 387785b40c | |||
| 03dea55131 | |||
| 7b8aa989f6 | |||
| 6ab6383144 | |||
| 690d60f9d6 | |||
| da0de80afd | |||
| cd7ae788b1 | |||
| 0d96cc53bf | |||
| 66fc3c5b35 | |||
| 5ab3eb8a38 | |||
| fec23b4acd | |||
| 901622fee0 | |||
| 527e622774 | |||
| 7223b79fe8 | |||
| 1ade41aeae | |||
| 58dc579255 | |||
| 370dac201b | |||
| 2a763006db | |||
| 522c66653b | |||
| b57f050b81 | |||
| 41ebcf150a | |||
| 1499def6ad | |||
| adbab0f5d7 | |||
| 88c88cac5b | |||
| 1ae38c98ad | |||
| 2d517cc594 | |||
| a9c82bd261 | |||
| 79aec86f5f | |||
| 9b3dfc7576 | |||
| 037ae74782 | |||
| b81c60a3ce | |||
| 363ac94c47 | |||
| 378f9e5095 | |||
| 659b494ee4 | |||
| 92965c6af2 | |||
| 70cb5aec9f | |||
| d59e77d5a2 | |||
| ff1b857ab0 |
@@ -0,0 +1,3 @@
|
||||
# Force Go as the main language
|
||||
*.go linguist-detectable=true
|
||||
* linguist-language=Go
|
||||
@@ -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 |
|
||||
+40
-9
@@ -9,14 +9,28 @@ import "sync"
|
||||
// ===================================================
|
||||
|
||||
type Config struct {
|
||||
NATSUrl string
|
||||
MongoUrl string
|
||||
MongoDatabase string
|
||||
Host string
|
||||
Port string
|
||||
LokiUrl string
|
||||
LogLevel string
|
||||
Whitelist bool
|
||||
IsApi bool
|
||||
IsNano 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
|
||||
InternalSchedulerAPI string
|
||||
}
|
||||
|
||||
func (c Config) GetUrl() string {
|
||||
@@ -37,12 +51,29 @@ func GetConfig() *Config {
|
||||
return instance
|
||||
}
|
||||
|
||||
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *Config {
|
||||
func SetConfig(isNano bool, isAPI bool, mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int,
|
||||
pkPath, ppPath,
|
||||
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI, internalWorkspaceAPI,
|
||||
internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *Config {
|
||||
GetConfig().IsNano = isNano
|
||||
GetConfig().IsApi = isAPI
|
||||
|
||||
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
|
||||
GetConfig().InternalSchedulerAPI = internalSchedulerAPI
|
||||
return GetConfig()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+198
-90
@@ -2,6 +2,8 @@ package dbs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
@@ -19,6 +21,8 @@ const (
|
||||
GT
|
||||
EQUAL
|
||||
NOT
|
||||
ELEMMATCH
|
||||
OR
|
||||
)
|
||||
|
||||
var str = [...]string{
|
||||
@@ -31,113 +35,44 @@ var str = [...]string{
|
||||
"gt",
|
||||
"equal",
|
||||
"not",
|
||||
"elemMatch",
|
||||
"or",
|
||||
}
|
||||
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
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, false)}}
|
||||
case ELEMMATCH:
|
||||
return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
|
||||
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, false)}
|
||||
case OR:
|
||||
return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value, true)}
|
||||
default:
|
||||
return defaultValue
|
||||
}
|
||||
@@ -152,13 +87,55 @@ 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{{Key: "$or", Value: orList}}
|
||||
} else {
|
||||
if len(orList) > 0 {
|
||||
andList = append(andList, bson.M{"$or": orList})
|
||||
}
|
||||
f = bson.D{{Key: "$and", Value: andList}}
|
||||
}
|
||||
}
|
||||
if operator == LIKE {
|
||||
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
|
||||
return f
|
||||
}
|
||||
|
||||
func (m Operator) ToValueOperator(operator Operator, value interface{}, or bool) interface{} {
|
||||
switch value := value.(type) {
|
||||
case *Filters:
|
||||
bson := GetBson(value)
|
||||
if or {
|
||||
for _, b := range bson {
|
||||
if b.Key == "$or" {
|
||||
return b.Value
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return bson
|
||||
}
|
||||
default:
|
||||
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
|
||||
value = ""
|
||||
}
|
||||
if operator == LIKE {
|
||||
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -172,6 +149,137 @@ type Filter struct {
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// FiltersFromFlatMap builds a *Filters from a map[string]interface{} whose structure
|
||||
// mirrors the JSON form of Filters:
|
||||
//
|
||||
// {
|
||||
// "and": { "name": [{"operator":"like","value":"foo"}] },
|
||||
// "or": { "source": [{"operator":"equal","value":"bar"}] }
|
||||
// }
|
||||
//
|
||||
// Keys inside "and"/"or" are json tag names; the function resolves each to its
|
||||
// full dotted BSON path using the target struct. Unknown keys are kept as-is.
|
||||
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Filters {
|
||||
filters := &Filters{
|
||||
And: make(map[string][]Filter),
|
||||
Or: make(map[string][]Filter),
|
||||
}
|
||||
paths := jsonToBsonPaths(reflect.TypeOf(target), "")
|
||||
resolve := func(jsonKey string) string {
|
||||
if p, ok := paths[jsonKey]; ok {
|
||||
return p
|
||||
}
|
||||
return jsonKey
|
||||
}
|
||||
parseFilters := func(raw interface{}) map[string][]Filter {
|
||||
out := make(map[string][]Filter)
|
||||
m, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
for jsonKey, val := range m {
|
||||
bsonKey := resolve(jsonKey)
|
||||
items, ok := val.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, item := range items {
|
||||
entry, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
f := Filter{}
|
||||
if op, ok := entry["operator"].(string); ok {
|
||||
f.Operator = op
|
||||
}
|
||||
if v, ok := entry["value"]; ok {
|
||||
f.Value = v
|
||||
}
|
||||
out[bsonKey] = append(out[bsonKey], f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
if and, ok := flatMap["and"]; ok {
|
||||
filters.And = parseFilters(and)
|
||||
}
|
||||
if or, ok := flatMap["or"]; ok {
|
||||
filters.Or = parseFilters(or)
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
// jsonToBsonPaths recursively walks a struct type and returns a map of
|
||||
// json_name → dotted_bson_path for every field reachable from that type.
|
||||
//
|
||||
// Anonymous embedded fields without any tag follow the BSON convention of this
|
||||
// codebase: they are stored as a nested sub-document whose key is the lowercased
|
||||
// struct type name (e.g. utils.AbstractObject → "abstractobject").
|
||||
func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string {
|
||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
result := make(map[string]string)
|
||||
if t.Kind() != reflect.Struct {
|
||||
return result
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
jsonTag := field.Tag.Get("json")
|
||||
bsonTag := field.Tag.Get("bson")
|
||||
jsonName := strings.Split(jsonTag, ",")[0]
|
||||
bsonName := strings.Split(bsonTag, ",")[0]
|
||||
|
||||
// Anonymous embedded struct with no tags: use lowercase type name as BSON prefix.
|
||||
if field.Anonymous && jsonName == "" && bsonName == "" {
|
||||
ft := field.Type
|
||||
for ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
embedPrefix := strings.ToLower(ft.Name())
|
||||
if prefix != "" {
|
||||
embedPrefix = prefix + "." + embedPrefix
|
||||
}
|
||||
for k, v := range jsonToBsonPaths(ft, embedPrefix) {
|
||||
if _, exists := result[k]; !exists {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if jsonName == "" || jsonName == "-" {
|
||||
continue
|
||||
}
|
||||
if bsonName == "" || bsonName == "-" {
|
||||
bsonName = jsonName
|
||||
}
|
||||
|
||||
fullPath := bsonName
|
||||
if prefix != "" {
|
||||
fullPath = prefix + "." + bsonName
|
||||
}
|
||||
|
||||
result[jsonName] = fullPath
|
||||
|
||||
ft := field.Type
|
||||
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
for k, v := range jsonToBsonPaths(ft, fullPath) {
|
||||
if _, exists := result[k]; !exists {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type Input = map[string]interface{}
|
||||
|
||||
func InputToBson(i Input, isUpdate bool) bson.D {
|
||||
|
||||
+19
-32
@@ -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) {
|
||||
@@ -270,6 +267,9 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
|
||||
}
|
||||
filter := bson.M{"_id": id}
|
||||
targetDBCollection := CollectionMap[collection_name]
|
||||
if targetDBCollection == nil {
|
||||
return nil, 503, errors.New("collection " + collection_name + " not initialized")
|
||||
}
|
||||
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
|
||||
@@ -282,37 +282,21 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
|
||||
return res, 200, nil
|
||||
}
|
||||
|
||||
func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.Cursor, int, error) {
|
||||
func (m *MongoDB) Search(filters *dbs.Filters, collection_name string, offset int64, limit int64) (*mongo.Cursor, int, error) {
|
||||
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
|
||||
return nil, 503, err
|
||||
}
|
||||
opts := options.Find()
|
||||
opts.SetLimit(100)
|
||||
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}}
|
||||
}
|
||||
if targetDBCollection == nil {
|
||||
return nil, 503, errors.New("collection " + collection_name + " not initialized")
|
||||
}
|
||||
|
||||
f := dbs.GetBson(filters)
|
||||
|
||||
opts.SetSkip(offset) // OFFSET
|
||||
opts.SetLimit(limit) // LIMIT
|
||||
|
||||
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// defer cancel()
|
||||
if cursor, err := targetDBCollection.Find(
|
||||
@@ -347,7 +331,8 @@ func (m *MongoDB) LoadFilter(filter map[string]interface{}, collection_name stri
|
||||
return res, 200, nil
|
||||
}
|
||||
|
||||
func (m *MongoDB) LoadAll(collection_name string) (*mongo.Cursor, int, error) {
|
||||
func (m *MongoDB) LoadAll(collection_name string, offset int64, limit int64) (*mongo.Cursor, int, error) {
|
||||
|
||||
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
|
||||
return nil, 503, err
|
||||
}
|
||||
@@ -355,8 +340,10 @@ func (m *MongoDB) LoadAll(collection_name string) (*mongo.Cursor, int, error) {
|
||||
|
||||
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
//defer cancel()
|
||||
|
||||
res, err := targetDBCollection.Find(MngoCtx, bson.D{})
|
||||
findOptions := options.Find()
|
||||
findOptions.SetSkip(offset) // OFFSET
|
||||
findOptions.SetLimit(limit) // LIMIT
|
||||
res, err := targetDBCollection.Find(MngoCtx, bson.D{}, findOptions)
|
||||
if err != nil {
|
||||
// m.Logger.Error().Msg("Couldn't find any resources. Error : " + err.Error())
|
||||
return nil, 404, err
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,214 @@
|
||||
# PlantUML — Format de commentaire human-readable
|
||||
|
||||
Ce document décrit la syntaxe des commentaires attachés aux ressources et aux liens
|
||||
dans les fichiers PlantUML importés par OpenCloud.
|
||||
|
||||
---
|
||||
|
||||
## Syntaxe générale
|
||||
|
||||
```plantuml
|
||||
TypeRessource(varName, "Nom affiché") ' clé: valeur, clé.sous_clé: valeur
|
||||
```
|
||||
|
||||
### Règles de parsing
|
||||
|
||||
| Règle | Détail |
|
||||
|---|---|
|
||||
| Séparateur de paires | `,` |
|
||||
| Séparateur clé/valeur | premier `:` de la paire (les URLs `http://...` sont gérées) |
|
||||
| Sous-objets | notation pointée `access.container.image: nginx` |
|
||||
| Types | auto-inférés : `bool` > `float64` > `string` |
|
||||
| Fallback | JSON brut si le commentaire commence par `{` (compatibilité ascendante) |
|
||||
|
||||
### Comportement à l'import
|
||||
|
||||
Chaque ressource reçoit automatiquement une **instance par défaut**, seedée avec les
|
||||
attributs de la ressource parente. Le commentaire vient ensuite **surcharger** uniquement
|
||||
les champs explicitement renseignés.
|
||||
|
||||
> **Exception :** `WorkflowEvent` n'a pas d'instance (voir section dédiée).
|
||||
|
||||
---
|
||||
|
||||
## Ressources disponibles
|
||||
|
||||
### `Data(var, "nom")` — Données
|
||||
|
||||
Ressource de données. Les attributs qualifient le modèle de données **et** son instance
|
||||
(source d'accès).
|
||||
|
||||
| Clé | Type | Description |
|
||||
|---|---|---|
|
||||
| `type` | string | Type de données (`raster`, `vector`, `tabular`…) |
|
||||
| `quality` | string | Niveau de qualité |
|
||||
| `open_data` | bool | Données en accès libre |
|
||||
| `static` | bool | Données statiques (pas de mise à jour) |
|
||||
| `personal_data` | bool | Contient des données personnelles |
|
||||
| `anonymized_personal_data` | bool | Données personnelles anonymisées |
|
||||
| `size` | float64 | Taille en GB |
|
||||
| `access_protocol` | string | Protocole d'accès (`http`, `s3`, `ftp`…) |
|
||||
| `country` | string | Code pays ISO (`FR`, `DE`…) |
|
||||
| `location.latitude` | float64 | Latitude géographique |
|
||||
| `location.longitude` | float64 | Longitude géographique |
|
||||
| `source` | string | URL / endpoint d'accès à la donnée |
|
||||
|
||||
```plantuml
|
||||
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Processing(var, "nom")` — Traitement
|
||||
|
||||
Ressource de traitement (algorithme, conteneur, service). Les attributs qualifient
|
||||
le modèle de traitement **et** sa configuration d'exécution.
|
||||
|
||||
| Clé | Type | Description |
|
||||
|---|---|---|
|
||||
| `infrastructure` | int | Infrastructure cible : `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
|
||||
| `is_service` | bool | Traitement persistant (service long-running) |
|
||||
| `open_source` | bool | Code source ouvert |
|
||||
| `license` | string | Licence (`MIT`, `Apache-2.0`, `GPL-3.0`…) |
|
||||
| `maturity` | string | Maturité (`prototype`, `beta`, `production`…) |
|
||||
| `access_protocol` | string | Protocole d'accès |
|
||||
| `country` | string | Code pays ISO |
|
||||
| `location.latitude` | float64 | Latitude |
|
||||
| `location.longitude` | float64 | Longitude |
|
||||
| `access.container.image` | string | Image du conteneur |
|
||||
| `access.container.command` | string | Commande de démarrage |
|
||||
| `access.container.args` | string | Arguments de la commande |
|
||||
|
||||
```plantuml
|
||||
Processing(p1, "NDVI Calc") ' infrastructure: 0, open_source: true, license: MIT, maturity: production, access.container.image: myrepo/ndvi:1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Storage(var, "nom")` — Stockage
|
||||
|
||||
Ressource de stockage. Produit une instance live (`LiveStorage`) à l'import.
|
||||
|
||||
| Clé | Type | Description |
|
||||
|---|---|---|
|
||||
| `storage_type` | int | Type de stockage (enum) |
|
||||
| `source` | string | URL / endpoint du stockage |
|
||||
| `path` | string | Chemin ou bucket dans le stockage |
|
||||
| `local` | bool | Stockage local |
|
||||
| `security_level` | string | Niveau de sécurité |
|
||||
| `size` | float64 | Taille allouée en GB |
|
||||
| `encryption` | bool | Chiffrement activé |
|
||||
| `redundancy` | string | Politique de redondance |
|
||||
| `throughput` | string | Débit cible |
|
||||
| `access_protocol` | string | Protocole (`s3`, `nfs`, `smb`…) |
|
||||
| `country` | string | Code pays ISO |
|
||||
| `location.latitude` | float64 | Latitude |
|
||||
| `location.longitude` | float64 | Longitude |
|
||||
|
||||
```plantuml
|
||||
Storage(s1, "Minio OVH") ' source: http://minio.example.com:9000, path: /bucket/data, access_protocol: s3, encryption: true, size: 500, country: FR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ComputeUnit(var, "nom")` — Unité de calcul
|
||||
|
||||
Ressource de calcul (datacenter, cluster). Produit une instance live (`LiveDatacenter`)
|
||||
à l'import.
|
||||
|
||||
| Clé | Type | Description |
|
||||
|---|---|---|
|
||||
| `architecture` | string | Architecture CPU (`x86_64`, `arm64`…) |
|
||||
| `infrastructure` | int | `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
|
||||
| `source` | string | URL de l'API du datacenter |
|
||||
| `security_level` | string | Niveau de sécurité |
|
||||
| `annual_co2_emissions` | float64 | Émissions CO₂ annuelles (kg) |
|
||||
| `access_protocol` | string | Protocole d'accès |
|
||||
| `country` | string | Code pays ISO |
|
||||
| `location.latitude` | float64 | Latitude |
|
||||
| `location.longitude` | float64 | Longitude |
|
||||
|
||||
```plantuml
|
||||
ComputeUnit(c1, "Datacenter Rennes") ' source: https://api.dc-rennes.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68, security_level: high
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `WorkflowEvent(var, "nom")` — Événement déclencheur de workflow
|
||||
|
||||
Crée directement un `NativeTool` de type `WORKFLOW_EVENT` (Kind = 0).
|
||||
Représente le point de départ d'un workflow.
|
||||
|
||||
> **Pas d'instance. Pas de commentaire.**
|
||||
> Le nom du `NativeTool` est forcé à `WORKFLOW_EVENT` à l'import.
|
||||
|
||||
```plantuml
|
||||
WorkflowEvent(e1, "Start")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Liens
|
||||
|
||||
Les commentaires sur les liens qualifient la connexion entre deux ressources
|
||||
(typiquement entre un traitement et un stockage).
|
||||
|
||||
### Syntaxe
|
||||
|
||||
```plantuml
|
||||
source --> destination ' clé: valeur
|
||||
source <-- destination ' clé: valeur
|
||||
source -- destination ' clé: valeur (non directionnel)
|
||||
```
|
||||
|
||||
### Attributs disponibles
|
||||
|
||||
| Clé | Type | Description |
|
||||
|---|---|---|
|
||||
| `storage_link_infos.write` | bool | `true` = écriture, `false` = lecture |
|
||||
| `storage_link_infos.source` | string | Chemin source dans le lien |
|
||||
| `storage_link_infos.destination` | string | Chemin destination dans le lien |
|
||||
| `storage_link_infos.filename` | string | Nom du fichier échangé |
|
||||
|
||||
```plantuml
|
||||
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: output.tif
|
||||
d1 --> p1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemple complet
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include opencloud.puml
|
||||
|
||||
WorkflowEvent(e1, "Start")
|
||||
|
||||
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
|
||||
|
||||
Processing(p1, "NDVI") ' infrastructure: 0, open_source: true, license: MIT, access.container.image: myrepo/ndvi:1.2
|
||||
|
||||
Storage(s1, "Minio résultats") ' source: http://minio.example.com:9000, path: /results, access_protocol: s3, encryption: true, size: 500, country: FR
|
||||
|
||||
ComputeUnit(c1, "DC Rennes") ' source: https://api.dc.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68
|
||||
|
||||
e1 --> p1
|
||||
d1 --> p1
|
||||
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: ndvi.tif
|
||||
s1 --> c1
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Récapitulatif des types de ressources
|
||||
|
||||
| Mot-clé PlantUML | Type Go | Instance | Live | Commentaire |
|
||||
|---|---|---|---|---|
|
||||
| `Data` | `DataResource` | `DataInstance` | non | oui |
|
||||
| `Processing` | `ProcessingResource` | `ProcessingInstance` | non | oui |
|
||||
| `Storage` | `StorageResource` | `StorageResourceInstance` | oui → `LiveStorage` | oui |
|
||||
| `ComputeUnit` | `ComputeResource` | `ComputeResourceInstance` | oui → `LiveDatacenter` | oui |
|
||||
| `WorkflowEvent` | `NativeTool` (Kind=WORKFLOW_EVENT) | aucune | non | non |
|
||||
+452
-134
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"runtime/debug"
|
||||
@@ -15,11 +17,19 @@ 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/billing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"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 +37,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"
|
||||
)
|
||||
|
||||
@@ -37,25 +50,73 @@ type LibDataEnum int
|
||||
|
||||
// init accessible constant to retrieve data from the database
|
||||
const (
|
||||
INVALID LibDataEnum = iota
|
||||
DATA_RESOURCE = tools.DATA_RESOURCE
|
||||
PROCESSING_RESOURCE = tools.PROCESSING_RESOURCE
|
||||
STORAGE_RESOURCE = tools.STORAGE_RESOURCE
|
||||
COMPUTE_RESOURCE = tools.COMPUTE_RESOURCE
|
||||
WORKFLOW_RESOURCE = tools.WORKFLOW_RESOURCE
|
||||
WORKFLOW = tools.WORKFLOW
|
||||
WORKSPACE = tools.WORKSPACE
|
||||
WORKFLOW_EXECUTION = tools.WORKFLOW_EXECUTION
|
||||
PEER = tools.PEER
|
||||
COLLABORATIVE_AREA = tools.COLLABORATIVE_AREA
|
||||
RULE = tools.RULE
|
||||
BOOKING = tools.BOOKING
|
||||
ORDER = tools.ORDER
|
||||
INVALID LibDataEnum = iota
|
||||
DATA_RESOURCE = tools.DATA_RESOURCE
|
||||
PROCESSING_RESOURCE = tools.PROCESSING_RESOURCE
|
||||
STORAGE_RESOURCE = tools.STORAGE_RESOURCE
|
||||
COMPUTE_RESOURCE = tools.COMPUTE_RESOURCE
|
||||
WORKFLOW_RESOURCE = tools.WORKFLOW_RESOURCE
|
||||
WORKFLOW = tools.WORKFLOW
|
||||
WORKSPACE = tools.WORKSPACE
|
||||
WORKFLOW_EXECUTION = tools.WORKFLOW_EXECUTION
|
||||
PEER = tools.PEER
|
||||
COLLABORATIVE_AREA = tools.COLLABORATIVE_AREA
|
||||
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
|
||||
EXECUTION_VERIFICATION = tools.EXECUTION_VERIFICATION
|
||||
ALLOWED_IMAGE = tools.ALLOWED_IMAGE
|
||||
SERVICE_RESOURCE = tools.SERVICE_RESOURCE
|
||||
LIVE_SERVICE = tools.LIVE_SERVICE
|
||||
BILL = tools.BILL
|
||||
PAYMENT = tools.PAYMENT
|
||||
REFUND = tools.REFUND
|
||||
DISCOUNT = tools.DISCOUNT
|
||||
SUBSCRIPTION = tools.SUBSCRIPTION
|
||||
)
|
||||
|
||||
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
|
||||
return dbs.FiltersFromFlatMap(flatMap, target)
|
||||
}
|
||||
|
||||
func GetMySelf() (*peer.Peer, error) {
|
||||
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
|
||||
if pp == nil {
|
||||
return nil, errors.New("peer not found")
|
||||
}
|
||||
return pp.(*peer.Peer), err
|
||||
}
|
||||
|
||||
func IsMySelf(peerID string) (bool, string) {
|
||||
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,25 +158,42 @@ 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.GetBoolDefault("IS_NANO", false),
|
||||
o.GetBoolDefault("IS_API", true),
|
||||
|
||||
o.GetStringDefault("MONGO_URL", "mongodb://127.0.0.1:27017"),
|
||||
o.GetStringDefault("MONGO_DATABASE", "DC_myDC"),
|
||||
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"),
|
||||
o.GetStringDefault("INTERNAL_SCHEDULER_API", "oc-scheduler"),
|
||||
)
|
||||
// Beego init
|
||||
beego.BConfig.AppName = appName
|
||||
beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080)
|
||||
beego.BConfig.WebConfig.DirectoryIndex = true
|
||||
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
|
||||
if config.GetConfig().IsApi {
|
||||
// Beego init
|
||||
beego.BConfig.AppName = appName
|
||||
beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080)
|
||||
beego.BConfig.WebConfig.DirectoryIndex = true
|
||||
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
|
||||
}
|
||||
}
|
||||
|
||||
type IDTokenClaims struct {
|
||||
@@ -135,6 +213,53 @@ type Claims struct {
|
||||
Session SessionClaims `json:"session"`
|
||||
}
|
||||
|
||||
func GetExtends(objs []utils.ShallowDBObject, typ ...string) []map[string]interface{} {
|
||||
cache := map[tools.DataType]map[string]interface{}{}
|
||||
m := []map[string]interface{}{}
|
||||
for _, obj := range objs {
|
||||
m = append(m, GetExtend(obj, obj.Extend(typ...), cache))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func GetExtend(obj utils.DBObject, extends map[string][]tools.DataType, cache map[tools.DataType]map[string]interface{}) map[string]interface{} {
|
||||
base := obj.Serialize(obj)
|
||||
for k, v := range extends {
|
||||
if base[k+"_id"] == nil || base[k+"_id"] == "" {
|
||||
fmt.Println(k+"_id", "GET EXTEND")
|
||||
continue
|
||||
}
|
||||
for _, vv := range v {
|
||||
if cache[vv] != nil && cache[vv][fmt.Sprintf("%v", base[k+"_id"])] != nil {
|
||||
base[k] = cache[vv][fmt.Sprintf("%v", base[k+"_id"])]
|
||||
continue
|
||||
}
|
||||
if d, _, err := models.Model(vv.EnumIndex()).GetAccessor(&tools.APIRequest{
|
||||
Admin: true,
|
||||
}).LoadOne(fmt.Sprintf("%v", base[k+"_id"])); d != nil && err == nil {
|
||||
base[k] = d.Serialize(d)
|
||||
if cache[vv] == nil {
|
||||
cache[vv] = map[string]interface{}{}
|
||||
}
|
||||
if cache[vv][fmt.Sprintf("%v", base[k+"_id"])] == nil {
|
||||
fmt.Println("TTT", vv, k, base[k])
|
||||
cache[vv][fmt.Sprintf("%v", base[k+"_id"])] = base[k]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func ExtractTokenInfoWs(request http.Request) (string, string, []string) {
|
||||
reqToken := request.Header.Get("Sec-WebSocket-Protocol")
|
||||
if reqToken != "" {
|
||||
return extractFromToken(reqToken, "user_id"), extractFromToken(reqToken, "peer_id"), strings.Split(extractFromToken(reqToken, "groups"), ",")
|
||||
}
|
||||
return "", "", []string{}
|
||||
}
|
||||
|
||||
func ExtractTokenInfo(request http.Request) (string, string, []string) {
|
||||
reqToken := request.Header.Get("Authorization")
|
||||
splitToken := strings.Split(reqToken, "Bearer ")
|
||||
@@ -144,27 +269,57 @@ func ExtractTokenInfo(request http.Request) (string, string, []string) {
|
||||
reqToken = splitToken[1]
|
||||
}
|
||||
if reqToken != "" {
|
||||
token := strings.Split(reqToken, ".")
|
||||
if len(token) > 2 {
|
||||
bytes, err := base64.StdEncoding.DecodeString(token[2])
|
||||
if err != nil {
|
||||
return "", "", []string{}
|
||||
}
|
||||
var c Claims
|
||||
err = json.Unmarshal(bytes, &c)
|
||||
if err != nil {
|
||||
return "", "", []string{}
|
||||
}
|
||||
return c.Session.IDToken.UserID, c.Session.IDToken.PeerID, c.Session.IDToken.Groups
|
||||
}
|
||||
return extractFromToken(reqToken, "user_id"), extractFromToken(reqToken, "peer_id"), strings.Split(extractFromToken(reqToken, "groups"), ",")
|
||||
}
|
||||
return "", "", []string{}
|
||||
}
|
||||
|
||||
func Init(appName string) {
|
||||
func extractFromToken(token string, attr string) string {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
payload := parts[1]
|
||||
switch len(payload) % 4 {
|
||||
case 2:
|
||||
payload += "=="
|
||||
case 3:
|
||||
payload += "="
|
||||
}
|
||||
b, err := base64.URLEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(b, &claims); err != nil {
|
||||
return ""
|
||||
}
|
||||
ext, ok := claims["ext"].(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
peerID, _ := ext[attr].(string)
|
||||
return peerID
|
||||
}
|
||||
|
||||
func InitAPI(appName string, extraRoutes ...map[string][]string) {
|
||||
InitDaemon(appName)
|
||||
api := &tools.API{}
|
||||
api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo())
|
||||
if config.GetConfig().IsApi {
|
||||
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(), extraRoutes...)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -186,8 +341,12 @@ 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(isNano bool, isApi bool, mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string,
|
||||
port int, pppath string, pkpath string,
|
||||
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
|
||||
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *config.Config {
|
||||
cfg := config.SetConfig(isNano, isApi, mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath, internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
|
||||
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI, internalSchedulerAPI)
|
||||
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 +380,37 @@ 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
|
||||
func NewRequestInfoAdmin(collection LibDataEnum, user string, groups []string, caller *tools.HTTPCaller) *Request {
|
||||
p, err := GetMySelf()
|
||||
peerID := ""
|
||||
if p != nil && err == nil {
|
||||
peerID = p.GetID()
|
||||
}
|
||||
fmt.Println("BAM", ws)
|
||||
return ws, nil
|
||||
return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller, admin: true}
|
||||
}
|
||||
|
||||
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 +418,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
|
||||
@@ -304,19 +429,20 @@ func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.Wo
|
||||
* @param c ...*tools.HTTPCaller
|
||||
* @return data LibDataShallow
|
||||
*/
|
||||
func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data LibDataShallow) {
|
||||
func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool, offset int64, limit int64) (data LibDataShallow) {
|
||||
defer func() { // recover the panic
|
||||
if r := recover(); r != nil {
|
||||
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Search : "+fmt.Sprintf("%v", r)))
|
||||
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,
|
||||
}).Search(filters, word, isDraft)
|
||||
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, offset, limit)
|
||||
if err != nil {
|
||||
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
|
||||
return
|
||||
@@ -331,19 +457,20 @@ func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data
|
||||
* @param c ...*tools.HTTPCaller
|
||||
* @return data LibDataShallow
|
||||
*/
|
||||
func (r *Request) LoadAll(isDraft bool) (data LibDataShallow) {
|
||||
func (r *Request) LoadAll(isDraft bool, offset int64, limit int64) (data LibDataShallow) {
|
||||
defer func() { // recover the panic
|
||||
if r := recover(); r != nil {
|
||||
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in LoadAll : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
|
||||
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,
|
||||
}).LoadAll(isDraft)
|
||||
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, offset, limit)
|
||||
if err != nil {
|
||||
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
|
||||
return
|
||||
@@ -366,11 +493,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,13 +523,14 @@ 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,
|
||||
}).UpdateOne(model.Deserialize(set, model), id)
|
||||
Caller: r.Caller,
|
||||
Username: r.User,
|
||||
PeerID: r.PeerID,
|
||||
Groups: r.Groups,
|
||||
Admin: r.admin,
|
||||
}).UpdateOne(set, id)
|
||||
if err != nil {
|
||||
data = LibData{Data: d, Code: code, Err: err.Error()}
|
||||
return
|
||||
@@ -424,11 +553,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 +582,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 +612,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 +708,189 @@ 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
|
||||
}
|
||||
|
||||
func (l *LibData) ToBill() *billing.Bill {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.BILL {
|
||||
return l.Data.(*billing.Bill)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToPayment() *payment.Payment {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.PAYMENT {
|
||||
return l.Data.(*payment.Payment)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToRefund() *refund.Refund {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.REFUND {
|
||||
return l.Data.(*refund.Refund)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToDiscount() *discount.Discount {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.DISCOUNT {
|
||||
return l.Data.(*discount.Discount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToSubscription() *subscription.Subscription {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.SUBSCRIPTION {
|
||||
return l.Data.(*subscription.Subscription)
|
||||
}
|
||||
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, 0, 1); 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, 0, 1); len(data.Data) > 0 {
|
||||
access.DeleteOne(data.Data[0].GetID())
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,75 @@
|
||||
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/libp2p/go-libp2p/core v0.43.0-rc2
|
||||
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/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // 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/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr v0.16.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.1 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.6.1 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // 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/spaolacci/murmur3 v1.1.0 // 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/exp v0.0.0-20250606033433-dcc06ee1d476 // 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
|
||||
lukechampine.com/blake3 v1.4.1 // 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,7 +83,6 @@ 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/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
@@ -45,16 +91,19 @@ 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
|
||||
)
|
||||
|
||||
replace github.com/libp2p/go-libp2p/core => github.com/libp2p/go-libp2p v0.47.0
|
||||
@@ -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,76 @@ 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-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ=
|
||||
github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw=
|
||||
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 +146,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 +162,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 +174,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 +202,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 +239,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=
|
||||
|
||||
+5
-6
@@ -58,14 +58,13 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
|
||||
// Add label that have been added to the event
|
||||
// A bit unsafe since we don't know what could be stored in the event
|
||||
// but we can't access this object once passed to the multilevel writter
|
||||
|
||||
for k,v := range(event){
|
||||
if k != "level" && k != "time" && k != "message"{
|
||||
labels[k] = v.(string)
|
||||
|
||||
for k, v := range event {
|
||||
if k != "level" && k != "time" && k != "message" {
|
||||
labels[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Format the timestamp in nanoseconds
|
||||
timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond))
|
||||
|
||||
@@ -87,7 +86,7 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
|
||||
|
||||
//fmt.Printf("Sending payload to Loki: %s\n", string(payloadBytes))
|
||||
|
||||
req, err := http.NewRequest("POST", w.url + "/loki/api/v1/push", bytes.NewReader(payloadBytes))
|
||||
req, err := http.NewRequest("POST", w.url+"/loki/api/v1/push", bytes.NewReader(payloadBytes))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package allowed_image
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
// AllowedImage représente une image de conteneur autorisée à persister
|
||||
// sur un peer après l'exécution d'un workflow.
|
||||
//
|
||||
// La décision de rétention est entièrement locale au datacenter —
|
||||
// le fournisseur de processing n'a aucun levier sur cette liste.
|
||||
//
|
||||
// Règle de matching (côté oc-datacenter) :
|
||||
// - Registry vide = toutes les registries
|
||||
// - TagConstraint vide = toutes les versions
|
||||
// - TagConstraint non vide = exact ou glob (ex: "3.*", "1.2.3")
|
||||
//
|
||||
// Les entrées IsDefault sont créées au bootstrap et ne peuvent pas
|
||||
// être supprimées via l'API.
|
||||
type AllowedImage struct {
|
||||
utils.AbstractObject
|
||||
|
||||
// Registry source (ex: "docker.io", "registry.example.com").
|
||||
// Vide = wildcard, accepte n'importe quelle registry.
|
||||
Registry string `json:"registry,omitempty" bson:"registry,omitempty"`
|
||||
|
||||
// Image est le nom de l'image sans registry ni tag
|
||||
// (ex: "natsio/nats-box", "library/alpine").
|
||||
Image string `json:"image" bson:"image" validate:"required"`
|
||||
|
||||
// TagConstraint est la contrainte sur le tag.
|
||||
// Vide = toutes les versions autorisées.
|
||||
// Supporte exact ("1.2.3") ou glob ("3.*", "*-alpine").
|
||||
TagConstraint string `json:"tag_constraint,omitempty" bson:"tag_constraint,omitempty"`
|
||||
|
||||
// IsDefault marque les entrées bootstrap insérées au démarrage.
|
||||
// Ces entrées ne peuvent pas être supprimées via l'API.
|
||||
IsDefault bool `json:"is_default,omitempty" bson:"is_default,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AllowedImage) StoreDraftDefault() {
|
||||
a.IsDraft = false // les allowed images sont actives immédiatement
|
||||
}
|
||||
|
||||
func (a *AllowedImage) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
return true, set
|
||||
}
|
||||
|
||||
func (a *AllowedImage) CanDelete() bool {
|
||||
return !a.IsDefault // les entrées bootstrap sont non supprimables
|
||||
}
|
||||
|
||||
func (a *AllowedImage) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package allowed_image
|
||||
|
||||
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 allowedImageMongoAccessor struct {
|
||||
utils.AbstractAccessor[*AllowedImage]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *allowedImageMongoAccessor {
|
||||
return &allowedImageMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*AllowedImage]{
|
||||
Logger: logs.CreateLogger(tools.ALLOWED_IMAGE.String()),
|
||||
Request: request,
|
||||
Type: tools.ALLOWED_IMAGE,
|
||||
New: func() *AllowedImage { return &AllowedImage{} },
|
||||
NotImplemented: []string{"CopyOne"},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package billing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/order"
|
||||
"cloud.o-forge.io/core/oc-lib/models/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"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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 (ri *Bill) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "order":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.ORDER)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
// IsFullySettled retourne vrai quand chaque ligne de chaque peer-order est réglée.
|
||||
func (b *Bill) IsFullySettled() bool {
|
||||
for _, po := range b.SubOrders {
|
||||
for _, item := range po.Items {
|
||||
if !item.Settled {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SettledTotal retourne le montant total des lignes déjà réglées.
|
||||
func (b *Bill) SettledTotal() float64 {
|
||||
total := 0.0
|
||||
for _, po := range b.SubOrders {
|
||||
for _, item := range po.Items {
|
||||
if item.Settled {
|
||||
total += item.UnitPriceHT * float64(item.Quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// MarkItemSettled marque une ligne comme réglée d'après son itemID
|
||||
// et propage le statut PAID sur le PeerOrder si toutes ses lignes sont réglées.
|
||||
func (b *Bill) MarkItemSettled(itemID string) bool {
|
||||
now := time.Now().UTC()
|
||||
for _, po := range b.SubOrders {
|
||||
for _, item := range po.Items {
|
||||
if item.ItemID == itemID {
|
||||
item.Settled = true
|
||||
item.SettledAt = &now
|
||||
// propage le statut PAID si toutes les lignes du peer sont réglées
|
||||
if po.IsFullySettled() {
|
||||
po.Status = enum.PAID
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GenerateBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
|
||||
return &Bill{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
|
||||
IsDraft: false,
|
||||
},
|
||||
OrderID: ord.UUID,
|
||||
Status: enum.PENDING,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DraftFirstBill crée le premier brouillon de facture pour un order.
|
||||
// Règle :
|
||||
// - Calcul total indépendant du mode de paiement (photo du coût réel).
|
||||
// - Les purchases sont toujours BILL_ONCE / PAY_ONCE → réglées immédiatement.
|
||||
// - Les bookings avec BillingStrategy != BILL_ONCE génèrent des Subscription.
|
||||
// - Chaque ligne reçoit un ItemID unique pour le suivi de règlement.
|
||||
func DraftFirstBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
|
||||
peers := map[string][]*PeerItemOrder{}
|
||||
|
||||
// Purchases : facturation immédiate, pas de subscription
|
||||
for _, p := range ord.Purchases {
|
||||
if _, ok := peers[p.DestPeerID]; !ok {
|
||||
peers[p.DestPeerID] = []*PeerItemOrder{}
|
||||
}
|
||||
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
|
||||
ItemID: uuid.New().String(),
|
||||
ResourceType: p.ResourceType,
|
||||
Purchase: p,
|
||||
Item: p.PricedItem,
|
||||
Quantity: 1,
|
||||
BillingStrategy: pricing.BILL_ONCE,
|
||||
PaymentType: pricing.PAY_ONCE,
|
||||
})
|
||||
}
|
||||
|
||||
// Bookings : exclure les ressources déjà achetées (purchase_resource existant)
|
||||
purchasedIDs := map[string]bool{}
|
||||
for _, p := range ord.Purchases {
|
||||
purchasedIDs[p.ResourceID] = true
|
||||
}
|
||||
for _, b := range ord.Bookings {
|
||||
if purchasedIDs[b.ResourceID] {
|
||||
continue
|
||||
}
|
||||
if _, ok := peers[b.DestPeerID]; !ok {
|
||||
peers[b.DestPeerID] = []*PeerItemOrder{}
|
||||
}
|
||||
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
|
||||
ItemID: uuid.New().String(),
|
||||
ResourceType: b.ResourceType,
|
||||
Quantity: 1,
|
||||
Item: b.PricedItem,
|
||||
BillingStrategy: b.BillingStrategy,
|
||||
PaymentType: b.PaymentType,
|
||||
})
|
||||
}
|
||||
|
||||
// Résolution des adresses de facturation peer
|
||||
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: ord.UUID,
|
||||
Status: enum.PENDING,
|
||||
SubOrders: peerOrders,
|
||||
}
|
||||
|
||||
// 1. Calcul des totaux (indépendant du mode de paiement)
|
||||
if _, err := bill.SumUpBill(request); err != nil {
|
||||
return bill, err
|
||||
}
|
||||
|
||||
// 2. Création des subscriptions pour les lignes récurrentes
|
||||
subIDs, err := createRecurringSubscriptions(bill, request)
|
||||
if err != nil {
|
||||
return bill, err
|
||||
}
|
||||
|
||||
// 3. Liaison des subscription IDs à l'order pour traçabilité
|
||||
if len(subIDs) > 0 {
|
||||
ord.SubscriptionIDs = append(ord.SubscriptionIDs, subIDs...)
|
||||
}
|
||||
|
||||
// 4. Persistance du brouillon de facture (les UnitPriceHT et SubscriptionID sont déjà set)
|
||||
stored, _, err := NewAccessor(request).StoreOne(bill)
|
||||
if err != nil {
|
||||
return bill, err
|
||||
}
|
||||
return stored.(*Bill), nil
|
||||
}
|
||||
|
||||
// createRecurringSubscriptions crée les Subscription pour chaque groupe
|
||||
// (peer × BillingStrategy) dont la stratégie est récurrente.
|
||||
// Modifie les PeerItemOrder en place (SubscriptionID).
|
||||
// Retourne les IDs de subscriptions créées.
|
||||
func createRecurringSubscriptions(b *Bill, request *tools.APIRequest) ([]string, error) {
|
||||
subIDs := []string{}
|
||||
|
||||
for peerID, po := range b.SubOrders {
|
||||
// Groupe les items récurrents par BillingStrategy
|
||||
byStrategy := map[pricing.BillingStrategy][]*PeerItemOrder{}
|
||||
for _, item := range po.Items {
|
||||
if item.BillingStrategy == pricing.BILL_ONCE {
|
||||
continue
|
||||
}
|
||||
byStrategy[item.BillingStrategy] = append(byStrategy[item.BillingStrategy], item)
|
||||
}
|
||||
|
||||
for strategy, items := range byStrategy {
|
||||
subItems := make([]*subscription.SubscriptionItem, 0, len(items))
|
||||
totalAmount := 0.0
|
||||
for _, item := range items {
|
||||
subItems = append(subItems, &subscription.SubscriptionItem{
|
||||
ResourceType: item.ResourceType,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPriceHT,
|
||||
})
|
||||
totalAmount += item.UnitPriceHT * float64(item.Quantity)
|
||||
}
|
||||
|
||||
var sub *subscription.Subscription
|
||||
switch strategy {
|
||||
case pricing.BILL_PER_YEAR:
|
||||
sub = subscription.NewYearlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
|
||||
case pricing.BILL_PER_WEEK:
|
||||
sub = subscription.NewWeeklySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
|
||||
default: // BILL_PER_MONTH et tout autre cas récurrent
|
||||
sub = subscription.NewMonthlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
|
||||
}
|
||||
sub.IsDraft = true
|
||||
|
||||
res, _, err := subscription.NewAccessor(request).StoreOne(sub)
|
||||
if err != nil {
|
||||
return subIDs, err
|
||||
}
|
||||
storedSub := res.(*subscription.Subscription)
|
||||
subIDs = append(subIDs, storedSub.GetID())
|
||||
|
||||
// Lie le SubscriptionID à chaque ligne concernée
|
||||
for _, item := range items {
|
||||
item.SubscriptionID = storedSub.GetID()
|
||||
}
|
||||
}
|
||||
}
|
||||
return subIDs, nil
|
||||
}
|
||||
|
||||
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
func (r *Bill) CanDelete() bool {
|
||||
return r.IsDraft
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PeerOrder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// IsFullySettled retourne vrai si toutes les lignes de ce peer sont réglées.
|
||||
func (po *PeerOrder) IsFullySettled() bool {
|
||||
for _, item := range po.Items {
|
||||
if !item.Settled {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
|
||||
switch dt {
|
||||
case tools.PROCESSING_RESOURCE:
|
||||
return &resources.PricedProcessingResource{}
|
||||
case tools.STORAGE_RESOURCE:
|
||||
return &resources.PricedStorageResource{}
|
||||
case tools.DATA_RESOURCE:
|
||||
return &resources.PricedDataResource{}
|
||||
case tools.COMPUTE_RESOURCE:
|
||||
return &resources.PricedComputeResource{}
|
||||
case tools.WORKFLOW_RESOURCE:
|
||||
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
|
||||
d.Status = enum.PENDING
|
||||
go func() {
|
||||
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER — UPDATE STATUS ON RETURN
|
||||
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
|
||||
if d.Status == enum.PAID {
|
||||
now := time.Now().UTC()
|
||||
for _, b := range d.Items {
|
||||
priced := PricedByType(b.ResourceType)
|
||||
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(),
|
||||
})
|
||||
// Marque la ligne comme réglée
|
||||
b.Settled = true
|
||||
b.SettledAt = &now
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Total += tot
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PeerItemOrder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PeerItemOrder est une ligne de facture pour un peer donné.
|
||||
type PeerItemOrder struct {
|
||||
// ItemID identifie de manière unique cette ligne pour le suivi de règlement.
|
||||
ItemID string `json:"item_id,omitempty" bson:"item_id,omitempty"`
|
||||
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
|
||||
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"`
|
||||
|
||||
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
|
||||
|
||||
// UnitPriceHT est le prix unitaire HT calculé par SumUpBill/GetPriceHT.
|
||||
// Utilisé pour la création des subscriptions sans recalcul.
|
||||
UnitPriceHT float64 `json:"unit_price_ht,omitempty" bson:"unit_price_ht,omitempty"`
|
||||
|
||||
// SubscriptionID référence la Subscription créée pour les lignes récurrentes.
|
||||
// Vide pour les lignes BILL_ONCE.
|
||||
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
|
||||
|
||||
// Settled indique si cette ligne a été réglée (paiement confirmé).
|
||||
Settled bool `json:"settled" bson:"settled"`
|
||||
SettledAt *time.Time `json:"settled_at,omitempty" bson:"settled_at,omitempty"`
|
||||
}
|
||||
|
||||
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
|
||||
if d.Purchase == nil {
|
||||
return 0, nil
|
||||
}
|
||||
priced := PricedByType(d.ResourceType)
|
||||
b, _ := json.Marshal(d.Item)
|
||||
err := json.Unmarshal(b, priced)
|
||||
if err != nil {
|
||||
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, 0, 10000)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
unitPrice, err := priced.GetPriceHT()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.UnitPriceHT = unitPrice // cache pour createRecurringSubscriptions
|
||||
return unitPrice * float64(d.Quantity), nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package billing
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
|
||||
return &billMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Bill]{
|
||||
Logger: logs.CreateLogger(tools.BILL.String()),
|
||||
Request: request,
|
||||
Type: tools.BILL,
|
||||
New: func() *Bill { return &Bill{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package discount
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type DiscountType int
|
||||
|
||||
const (
|
||||
PERCENTAGE DiscountType = iota // réduction en pourcentage
|
||||
FIXED_AMOUNT // réduction montant fixe
|
||||
)
|
||||
|
||||
func (d DiscountType) String() string {
|
||||
return [...]string{"percentage", "fixed_amount"}[d]
|
||||
}
|
||||
|
||||
func DiscountTypeList() []DiscountType {
|
||||
return []DiscountType{PERCENTAGE, FIXED_AMOUNT}
|
||||
}
|
||||
|
||||
type DiscountScope int
|
||||
|
||||
const (
|
||||
SCOPE_ALL DiscountScope = iota // applicable à tout
|
||||
SCOPE_RESOURCE_TYPE // applicable à un type de ressource
|
||||
SCOPE_RESOURCE // applicable à une ressource spécifique
|
||||
SCOPE_SUBSCRIPTION // applicable aux souscriptions
|
||||
)
|
||||
|
||||
func (d DiscountScope) String() string {
|
||||
return [...]string{"all", "resource_type", "resource", "subscription"}[d]
|
||||
}
|
||||
|
||||
// Discount représente une réduction applicable sur les ressources ou abonnements.
|
||||
type Discount struct {
|
||||
utils.AbstractObject
|
||||
Code string `json:"code,omitempty" bson:"code,omitempty"`
|
||||
DiscountType DiscountType `json:"discount_type" bson:"discount_type"`
|
||||
Scope DiscountScope `json:"scope" bson:"scope"`
|
||||
Value float64 `json:"value" bson:"value"` // pourcentage (0-100) ou montant fixe
|
||||
Currency string `json:"currency,omitempty" bson:"currency,omitempty"` // pour FIXED_AMOUNT
|
||||
ResourceTypes []tools.DataType `json:"resource_types,omitempty" bson:"resource_types,omitempty"` // si SCOPE_RESOURCE_TYPE
|
||||
ResourceIDs []string `json:"resource_ids,omitempty" bson:"resource_ids,omitempty"` // si SCOPE_RESOURCE
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
|
||||
MaxUsage int `json:"max_usage,omitempty" bson:"max_usage,omitempty"` // 0 = illimité
|
||||
CurrentUsage int `json:"current_usage" bson:"current_usage"`
|
||||
MinAmount float64 `json:"min_amount,omitempty" bson:"min_amount,omitempty"` // montant minimum du bill pour appliquer
|
||||
Active bool `json:"active" bson:"active" default:"true"`
|
||||
}
|
||||
|
||||
// IsValid vérifie si la réduction est applicable au moment présent.
|
||||
func (d *Discount) IsValid(billAmount float64) bool {
|
||||
now := time.Now().UTC()
|
||||
if !d.Active {
|
||||
return false
|
||||
}
|
||||
if d.MaxUsage > 0 && d.CurrentUsage >= d.MaxUsage {
|
||||
return false
|
||||
}
|
||||
if d.ValidFrom != nil && now.Before(*d.ValidFrom) {
|
||||
return false
|
||||
}
|
||||
if d.ValidUntil != nil && now.After(*d.ValidUntil) {
|
||||
return false
|
||||
}
|
||||
if d.MinAmount > 0 && billAmount < d.MinAmount {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Apply applique la réduction sur un prix HT et retourne le prix réduit.
|
||||
func (d *Discount) Apply(priceHT float64) float64 {
|
||||
switch d.DiscountType {
|
||||
case PERCENTAGE:
|
||||
return priceHT - (priceHT * d.Value / 100)
|
||||
case FIXED_AMOUNT:
|
||||
result := priceHT - d.Value
|
||||
if result < 0 {
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
return priceHT
|
||||
}
|
||||
|
||||
// DiscountAmount retourne le montant de la réduction sans l'appliquer.
|
||||
func (d *Discount) DiscountAmount(priceHT float64) float64 {
|
||||
switch d.DiscountType {
|
||||
case PERCENTAGE:
|
||||
return priceHT * d.Value / 100
|
||||
case FIXED_AMOUNT:
|
||||
if d.Value > priceHT {
|
||||
return priceHT
|
||||
}
|
||||
return d.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AppliesToResource vérifie si cette réduction s'applique à une ressource donnée.
|
||||
func (d *Discount) AppliesToResource(resourceID string, resourceType tools.DataType) bool {
|
||||
switch d.Scope {
|
||||
case SCOPE_ALL:
|
||||
return true
|
||||
case SCOPE_RESOURCE:
|
||||
for _, id := range d.ResourceIDs {
|
||||
if id == resourceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case SCOPE_RESOURCE_TYPE:
|
||||
for _, t := range d.ResourceTypes {
|
||||
if t == resourceType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IncrementUsage incrémente le compteur d'utilisation.
|
||||
func (d *Discount) IncrementUsage() {
|
||||
d.CurrentUsage++
|
||||
}
|
||||
|
||||
func (d *Discount) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (d *Discount) StoreDraftDefault() {
|
||||
d.IsDraft = true
|
||||
}
|
||||
|
||||
func (d *Discount) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
return d.IsDraft, set
|
||||
}
|
||||
|
||||
func (d *Discount) CanDelete() bool {
|
||||
return d.IsDraft || d.CurrentUsage == 0
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package discount
|
||||
|
||||
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 discountMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Discount]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *discountMongoAccessor {
|
||||
return &discountMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Discount]{
|
||||
Logger: logs.CreateLogger(tools.DISCOUNT.String()),
|
||||
Request: request,
|
||||
Type: tools.DISCOUNT,
|
||||
New: func() *Discount { return &Discount{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type PaymentStatus int
|
||||
|
||||
const (
|
||||
PAYMENT_PENDING PaymentStatus = iota
|
||||
PAYMENT_PROCESSING // en cours de traitement blockchain/réseau
|
||||
PAYMENT_COMPLETED // confirmé
|
||||
PAYMENT_FAILED // échoué
|
||||
PAYMENT_CANCELLED // annulé avant exécution
|
||||
PAYMENT_REFUNDED // remboursé
|
||||
)
|
||||
|
||||
func (s PaymentStatus) String() string {
|
||||
return [...]string{"pending", "processing", "completed", "failed", "cancelled", "refunded"}[s]
|
||||
}
|
||||
|
||||
func PaymentStatusList() []PaymentStatus {
|
||||
return []PaymentStatus{PAYMENT_PENDING, PAYMENT_PROCESSING, PAYMENT_COMPLETED, PAYMENT_FAILED, PAYMENT_CANCELLED, PAYMENT_REFUNDED}
|
||||
}
|
||||
|
||||
type PaymentMethod int
|
||||
|
||||
const (
|
||||
METHOD_BLOCKCHAIN PaymentMethod = iota
|
||||
METHOD_CREDIT_CARD
|
||||
METHOD_BANK_TRANSFER
|
||||
METHOD_CRYPTO
|
||||
METHOD_INTERNAL_CREDIT // crédit interne à la plateforme
|
||||
)
|
||||
|
||||
func (m PaymentMethod) String() string {
|
||||
return [...]string{"blockchain", "credit_card", "bank_transfer", "crypto", "internal_credit"}[m]
|
||||
}
|
||||
|
||||
func PaymentMethodList() []PaymentMethod {
|
||||
return []PaymentMethod{METHOD_BLOCKCHAIN, METHOD_CREDIT_CARD, METHOD_BANK_TRANSFER, METHOD_CRYPTO, METHOD_INTERNAL_CREDIT}
|
||||
}
|
||||
|
||||
// Payment représente une transaction de paiement — instantanée, mensuelle ou annuelle.
|
||||
type Payment struct {
|
||||
utils.AbstractObject
|
||||
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
|
||||
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
|
||||
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
|
||||
PayerPeerID string `json:"payer_peer_id,omitempty" bson:"payer_peer_id,omitempty"`
|
||||
RecipientPeerID string `json:"recipient_peer_id,omitempty" bson:"recipient_peer_id,omitempty"`
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
Status PaymentStatus `json:"status" bson:"status"`
|
||||
Method PaymentMethod `json:"method" bson:"method"`
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"` // PAY_ONCE, PAY_EVERY_MONTH, PAY_EVERY_YEAR
|
||||
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
|
||||
WalletFrom string `json:"wallet_from,omitempty" bson:"wallet_from,omitempty"`
|
||||
WalletTo string `json:"wallet_to,omitempty" bson:"wallet_to,omitempty"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at,omitempty" bson:"scheduled_at,omitempty"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
FailureReason string `json:"failure_reason,omitempty" bson:"failure_reason,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty" bson:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// NewInstantPayment crée un paiement immédiat (PAY_ONCE).
|
||||
func NewInstantPayment(billID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod) *Payment {
|
||||
return &Payment{
|
||||
BillID: billID,
|
||||
PayerPeerID: payerPeerID,
|
||||
RecipientPeerID: recipientPeerID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
Status: PAYMENT_PENDING,
|
||||
Method: method,
|
||||
PaymentType: pricing.PAY_ONCE,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScheduledPayment crée un paiement programmé (mensuel ou annuel).
|
||||
func NewScheduledPayment(subscriptionID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod, paymentType pricing.PaymentType, scheduledAt time.Time) *Payment {
|
||||
return &Payment{
|
||||
SubscriptionID: subscriptionID,
|
||||
PayerPeerID: payerPeerID,
|
||||
RecipientPeerID: recipientPeerID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
Status: PAYMENT_PENDING,
|
||||
Method: method,
|
||||
PaymentType: paymentType,
|
||||
ScheduledAt: &scheduledAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Complete marque le paiement comme confirmé.
|
||||
func (p *Payment) Complete(transactionID string) {
|
||||
now := time.Now().UTC()
|
||||
p.Status = PAYMENT_COMPLETED
|
||||
p.TransactionID = transactionID
|
||||
p.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Fail marque le paiement comme échoué.
|
||||
func (p *Payment) Fail(reason string) {
|
||||
now := time.Now().UTC()
|
||||
p.Status = PAYMENT_FAILED
|
||||
p.FailureReason = reason
|
||||
p.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Cancel annule le paiement s'il est encore en attente.
|
||||
func (p *Payment) Cancel() bool {
|
||||
if p.Status != PAYMENT_PENDING {
|
||||
return false
|
||||
}
|
||||
p.Status = PAYMENT_CANCELLED
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRefundable indique si le paiement peut faire l'objet d'un remboursement.
|
||||
func (p *Payment) IsRefundable() bool {
|
||||
return p.Status == PAYMENT_COMPLETED
|
||||
}
|
||||
|
||||
func (p *Payment) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (p *Payment) StoreDraftDefault() {
|
||||
p.IsDraft = true
|
||||
}
|
||||
|
||||
func (p *Payment) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Payment)
|
||||
if !p.IsDraft && p.Status != incoming.Status {
|
||||
return true, &Payment{
|
||||
Status: incoming.Status,
|
||||
TransactionID: incoming.TransactionID,
|
||||
FailureReason: incoming.FailureReason,
|
||||
}
|
||||
}
|
||||
return p.IsDraft, set
|
||||
}
|
||||
|
||||
func (p *Payment) CanDelete() bool {
|
||||
return p.IsDraft || p.Status == PAYMENT_PENDING
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package payment
|
||||
|
||||
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 paymentMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Payment]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *paymentMongoAccessor {
|
||||
return &paymentMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Payment]{
|
||||
Logger: logs.CreateLogger(tools.PAYMENT.String()),
|
||||
Request: request,
|
||||
Type: tools.PAYMENT,
|
||||
New: func() *Payment { return &Payment{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
)
|
||||
|
||||
type ScheduleStatus int
|
||||
|
||||
const (
|
||||
SCHEDULE_ACTIVE ScheduleStatus = iota
|
||||
SCHEDULE_PAUSED // mis en pause manuellement
|
||||
SCHEDULE_CANCELLED // résilié
|
||||
SCHEDULE_COMPLETED // terminé normalement (abonnement expiré)
|
||||
SCHEDULE_FAILED // trop d'échecs consécutifs
|
||||
)
|
||||
|
||||
func (s ScheduleStatus) String() string {
|
||||
return [...]string{"active", "paused", "cancelled", "completed", "failed"}[s]
|
||||
}
|
||||
|
||||
// PaymentSchedule pilote la récurrence des paiements d'un abonnement.
|
||||
type PaymentSchedule struct {
|
||||
SubscriptionID string `json:"subscription_id" bson:"subscription_id"`
|
||||
Frequency pricing.PaymentType `json:"frequency" bson:"frequency"` // PAY_EVERY_WEEK / PAY_EVERY_MONTH / PAY_EVERY_YEAR
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency"`
|
||||
Status ScheduleStatus `json:"status" bson:"status"`
|
||||
NextPaymentDate time.Time `json:"next_payment_date" bson:"next_payment_date"`
|
||||
LastExecutedAt *time.Time `json:"last_executed_at,omitempty" bson:"last_executed_at,omitempty"`
|
||||
FailureCount int `json:"failure_count" bson:"failure_count"`
|
||||
MaxRetries int `json:"max_retries" bson:"max_retries" default:"3"`
|
||||
}
|
||||
|
||||
// nextDate calcule la prochaine date selon la fréquence.
|
||||
func (ps *PaymentSchedule) nextDate() time.Time {
|
||||
switch ps.Frequency {
|
||||
case pricing.PAY_EVERY_WEEK:
|
||||
return ps.NextPaymentDate.AddDate(0, 0, 7)
|
||||
case pricing.PAY_EVERY_MONTH:
|
||||
return ps.NextPaymentDate.AddDate(0, 1, 0)
|
||||
case pricing.PAY_EVERY_YEAR:
|
||||
return ps.NextPaymentDate.AddDate(1, 0, 0)
|
||||
}
|
||||
return ps.NextPaymentDate
|
||||
}
|
||||
|
||||
// Advance enregistre l'exécution réussie et avance à la prochaine échéance.
|
||||
func (ps *PaymentSchedule) Advance() {
|
||||
now := time.Now().UTC()
|
||||
ps.LastExecutedAt = &now
|
||||
ps.FailureCount = 0
|
||||
ps.NextPaymentDate = ps.nextDate()
|
||||
}
|
||||
|
||||
// RecordFailure incrémente le compteur d'échecs et désactive après MaxRetries.
|
||||
func (ps *PaymentSchedule) RecordFailure() {
|
||||
ps.FailureCount++
|
||||
if ps.MaxRetries > 0 && ps.FailureCount >= ps.MaxRetries {
|
||||
ps.Status = SCHEDULE_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
// IsDue retourne vrai si le paiement est dû maintenant.
|
||||
func (ps *PaymentSchedule) IsDue() bool {
|
||||
return ps.Status == SCHEDULE_ACTIVE && !time.Now().UTC().Before(ps.NextPaymentDate)
|
||||
}
|
||||
|
||||
// Pause suspend temporairement le calendrier.
|
||||
func (ps *PaymentSchedule) Pause() {
|
||||
if ps.Status == SCHEDULE_ACTIVE {
|
||||
ps.Status = SCHEDULE_PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
// Resume réactive un calendrier mis en pause.
|
||||
func (ps *PaymentSchedule) Resume() {
|
||||
if ps.Status == SCHEDULE_PAUSED {
|
||||
ps.Status = SCHEDULE_ACTIVE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package refund
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type RefundStatus int
|
||||
|
||||
const (
|
||||
REFUND_PENDING RefundStatus = iota
|
||||
REFUND_APPROVED // approuvé, en attente de traitement
|
||||
REFUND_REJECTED // rejeté
|
||||
REFUND_PROCESSING // en cours de virement/blockchain
|
||||
REFUND_COMPLETED // remboursé
|
||||
REFUND_CANCELLED // annulé avant approbation
|
||||
)
|
||||
|
||||
func (s RefundStatus) String() string {
|
||||
return [...]string{"pending", "approved", "rejected", "processing", "completed", "cancelled"}[s]
|
||||
}
|
||||
|
||||
func RefundStatusList() []RefundStatus {
|
||||
return []RefundStatus{REFUND_PENDING, REFUND_APPROVED, REFUND_REJECTED, REFUND_PROCESSING, REFUND_COMPLETED, REFUND_CANCELLED}
|
||||
}
|
||||
|
||||
// Refund représente une demande de remboursement sur un paiement validé.
|
||||
type Refund struct {
|
||||
utils.AbstractObject
|
||||
PaymentID string `json:"payment_id" bson:"payment_id" validate:"required"`
|
||||
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
|
||||
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
|
||||
RefundType pricing.RefundType `json:"refund_type" bson:"refund_type"`
|
||||
Amount float64 `json:"amount" bson:"amount" validate:"required"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
Reason string `json:"reason,omitempty" bson:"reason,omitempty"`
|
||||
Status RefundStatus `json:"status" bson:"status"`
|
||||
RequestedAt time.Time `json:"requested_at" bson:"requested_at"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
ProcessedByID string `json:"processed_by_id,omitempty" bson:"processed_by_id,omitempty"`
|
||||
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
|
||||
Notes string `json:"notes,omitempty" bson:"notes,omitempty"`
|
||||
// ratio appliqué sur le montant original (0-100). 0 = non renseigné (remboursement total).
|
||||
RefundRatio float64 `json:"refund_ratio,omitempty" bson:"refund_ratio,omitempty"`
|
||||
}
|
||||
|
||||
// NewRefund crée une demande de remboursement total.
|
||||
func NewRefund(paymentID, billID string, amount float64, currency string, refundType pricing.RefundType, reason string) *Refund {
|
||||
return &Refund{
|
||||
PaymentID: paymentID,
|
||||
BillID: billID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
RefundType: refundType,
|
||||
Reason: reason,
|
||||
Status: REFUND_PENDING,
|
||||
RequestedAt: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewPartialRefund crée une demande de remboursement partiel selon un ratio pourcentage.
|
||||
func NewPartialRefund(paymentID, billID string, originalAmount, ratioPercent float64, currency string, refundType pricing.RefundType, reason string) *Refund {
|
||||
amount := originalAmount * ratioPercent / 100
|
||||
r := NewRefund(paymentID, billID, amount, currency, refundType, reason)
|
||||
r.RefundRatio = ratioPercent
|
||||
return r
|
||||
}
|
||||
|
||||
// Approve approuve la demande de remboursement.
|
||||
func (r *Refund) Approve(processedByID string) {
|
||||
r.Status = REFUND_APPROVED
|
||||
r.ProcessedByID = processedByID
|
||||
}
|
||||
|
||||
// Reject rejette la demande de remboursement.
|
||||
func (r *Refund) Reject(processedByID, notes string) {
|
||||
now := time.Now().UTC()
|
||||
r.Status = REFUND_REJECTED
|
||||
r.ProcessedByID = processedByID
|
||||
r.ProcessedAt = &now
|
||||
r.Notes = notes
|
||||
}
|
||||
|
||||
// Process passe le remboursement en cours de traitement.
|
||||
func (r *Refund) Process() bool {
|
||||
if r.Status != REFUND_APPROVED {
|
||||
return false
|
||||
}
|
||||
r.Status = REFUND_PROCESSING
|
||||
return true
|
||||
}
|
||||
|
||||
// Complete finalise le remboursement avec l'identifiant de transaction.
|
||||
func (r *Refund) Complete(transactionID string) {
|
||||
now := time.Now().UTC()
|
||||
r.Status = REFUND_COMPLETED
|
||||
r.TransactionID = transactionID
|
||||
r.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Cancel annule la demande si elle est encore en attente.
|
||||
func (r *Refund) Cancel() bool {
|
||||
if r.Status != REFUND_PENDING {
|
||||
return false
|
||||
}
|
||||
r.Status = REFUND_CANCELLED
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Refund) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (r *Refund) StoreDraftDefault() {
|
||||
r.IsDraft = true
|
||||
}
|
||||
|
||||
func (r *Refund) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Refund)
|
||||
if !r.IsDraft && r.Status != incoming.Status {
|
||||
return true, &Refund{
|
||||
Status: incoming.Status,
|
||||
TransactionID: incoming.TransactionID,
|
||||
Notes: incoming.Notes,
|
||||
ProcessedByID: incoming.ProcessedByID,
|
||||
}
|
||||
}
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
func (r *Refund) CanDelete() bool {
|
||||
return r.IsDraft || r.Status == REFUND_PENDING
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package refund
|
||||
|
||||
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 refundMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Refund]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *refundMongoAccessor {
|
||||
return &refundMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Refund]{
|
||||
Logger: logs.CreateLogger(tools.REFUND.String()),
|
||||
Request: request,
|
||||
Type: tools.REFUND,
|
||||
New: func() *Refund { return &Refund{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type SubscriptionStatus int
|
||||
|
||||
const (
|
||||
SUBSCRIPTION_PENDING SubscriptionStatus = iota // en attente de premier paiement
|
||||
SUBSCRIPTION_TRIAL // période d'essai
|
||||
SUBSCRIPTION_ACTIVE // actif
|
||||
SUBSCRIPTION_PAUSED // suspendu temporairement
|
||||
SUBSCRIPTION_CANCELLED // résilié par l'utilisateur
|
||||
SUBSCRIPTION_EXPIRED // date de fin dépassée
|
||||
)
|
||||
|
||||
func (s SubscriptionStatus) String() string {
|
||||
return [...]string{"pending", "trial", "active", "paused", "cancelled", "expired"}[s]
|
||||
}
|
||||
|
||||
func SubscriptionStatusList() []SubscriptionStatus {
|
||||
return []SubscriptionStatus{SUBSCRIPTION_PENDING, SUBSCRIPTION_TRIAL, SUBSCRIPTION_ACTIVE, SUBSCRIPTION_PAUSED, SUBSCRIPTION_CANCELLED, SUBSCRIPTION_EXPIRED}
|
||||
}
|
||||
|
||||
// SubscriptionItem représente un élément d'un abonnement (ressource louée).
|
||||
type SubscriptionItem struct {
|
||||
ResourceID string `json:"resource_id" bson:"resource_id"`
|
||||
ResourceType tools.DataType `json:"resource_type" bson:"resource_type"`
|
||||
Quantity int `json:"quantity" bson:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price" bson:"unit_price"`
|
||||
}
|
||||
|
||||
// Subscription représente un abonnement mensuel ou annuel à des ressources.
|
||||
type Subscription struct {
|
||||
utils.AbstractObject
|
||||
SubscriberPeerID string `json:"subscriber_peer_id" bson:"subscriber_peer_id" validate:"required"`
|
||||
ProviderPeerID string `json:"provider_peer_id,omitempty" bson:"provider_peer_id,omitempty"`
|
||||
Status SubscriptionStatus `json:"status" bson:"status"`
|
||||
PlanType pricing.PaymentType `json:"plan_type" bson:"plan_type"` // PAY_EVERY_MONTH ou PAY_EVERY_YEAR
|
||||
Items []*SubscriptionItem `json:"items,omitempty" bson:"items,omitempty"`
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
StartDate time.Time `json:"start_date" bson:"start_date"`
|
||||
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"`
|
||||
NextBillingDate time.Time `json:"next_billing_date" bson:"next_billing_date"`
|
||||
AutoRenew bool `json:"auto_renew" bson:"auto_renew" default:"true"`
|
||||
TrialEndDate *time.Time `json:"trial_end_date,omitempty" bson:"trial_end_date,omitempty"`
|
||||
DiscountIDs []string `json:"discount_ids,omitempty" bson:"discount_ids,omitempty"`
|
||||
CancelledAt *time.Time `json:"cancelled_at,omitempty" bson:"cancelled_at,omitempty"`
|
||||
CancelReason string `json:"cancel_reason,omitempty" bson:"cancel_reason,omitempty"`
|
||||
}
|
||||
|
||||
// newSubscription est le constructeur interne commun.
|
||||
func newSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, plan pricing.PaymentType, nextBilling time.Time) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return &Subscription{
|
||||
SubscriberPeerID: subscriberPeerID,
|
||||
ProviderPeerID: providerPeerID,
|
||||
Status: SUBSCRIPTION_PENDING,
|
||||
PlanType: plan,
|
||||
Items: items,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
StartDate: now,
|
||||
NextBillingDate: nextBilling,
|
||||
AutoRenew: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWeeklySubscription crée un abonnement hebdomadaire.
|
||||
func NewWeeklySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_WEEK, now.AddDate(0, 0, 7))
|
||||
}
|
||||
|
||||
// NewMonthlySubscription crée un abonnement mensuel.
|
||||
func NewMonthlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_MONTH, now.AddDate(0, 1, 0))
|
||||
}
|
||||
|
||||
// NewYearlySubscription crée un abonnement annuel.
|
||||
func NewYearlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_YEAR, now.AddDate(1, 0, 0))
|
||||
}
|
||||
|
||||
// NewTrialSubscription crée un abonnement mensuel avec période d'essai.
|
||||
func NewTrialSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, trialDays int) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
trialEnd := now.AddDate(0, 0, trialDays)
|
||||
s := NewMonthlySubscription(subscriberPeerID, providerPeerID, items, amount, currency)
|
||||
s.Status = SUBSCRIPTION_TRIAL
|
||||
s.TrialEndDate = &trialEnd
|
||||
s.NextBillingDate = trialEnd
|
||||
return s
|
||||
}
|
||||
|
||||
// Activate passe l'abonnement au statut actif (après premier paiement).
|
||||
func (s *Subscription) Activate() {
|
||||
s.Status = SUBSCRIPTION_ACTIVE
|
||||
}
|
||||
|
||||
// Pause suspend l'abonnement.
|
||||
func (s *Subscription) Pause() {
|
||||
if s.Status == SUBSCRIPTION_ACTIVE {
|
||||
s.Status = SUBSCRIPTION_PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
// Resume réactive un abonnement suspendu.
|
||||
func (s *Subscription) Resume() {
|
||||
if s.Status == SUBSCRIPTION_PAUSED {
|
||||
s.Status = SUBSCRIPTION_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel résilie l'abonnement.
|
||||
func (s *Subscription) Cancel(reason string) {
|
||||
now := time.Now().UTC()
|
||||
s.Status = SUBSCRIPTION_CANCELLED
|
||||
s.CancelledAt = &now
|
||||
s.CancelReason = reason
|
||||
s.AutoRenew = false
|
||||
}
|
||||
|
||||
// Renew avance la prochaine date de facturation d'une période.
|
||||
func (s *Subscription) Renew() {
|
||||
switch s.PlanType {
|
||||
case pricing.PAY_EVERY_WEEK:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(0, 0, 7)
|
||||
case pricing.PAY_EVERY_MONTH:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(0, 1, 0)
|
||||
case pricing.PAY_EVERY_YEAR:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(1, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// IsExpired vérifie si l'abonnement a dépassé sa date de fin.
|
||||
func (s *Subscription) IsExpired() bool {
|
||||
if s.EndDate == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().UTC().After(*s.EndDate)
|
||||
}
|
||||
|
||||
// IsBillingDue vérifie si la prochaine échéance est atteinte.
|
||||
func (s *Subscription) IsBillingDue() bool {
|
||||
return s.Status == SUBSCRIPTION_ACTIVE && !time.Now().UTC().Before(s.NextBillingDate)
|
||||
}
|
||||
|
||||
// IsInTrial vérifie si l'abonnement est en période d'essai.
|
||||
func (s *Subscription) IsInTrial() bool {
|
||||
return s.Status == SUBSCRIPTION_TRIAL && s.TrialEndDate != nil && time.Now().UTC().Before(*s.TrialEndDate)
|
||||
}
|
||||
|
||||
// ComputeAmount recalcule le montant total depuis les items.
|
||||
func (s *Subscription) ComputeAmount() float64 {
|
||||
total := 0.0
|
||||
for _, item := range s.Items {
|
||||
total += item.UnitPrice * float64(item.Quantity)
|
||||
}
|
||||
s.Amount = total
|
||||
return total
|
||||
}
|
||||
|
||||
func (s *Subscription) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (s *Subscription) StoreDraftDefault() {
|
||||
s.IsDraft = true
|
||||
}
|
||||
|
||||
func (s *Subscription) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Subscription)
|
||||
if !s.IsDraft && s.Status != incoming.Status {
|
||||
return true, &Subscription{
|
||||
Status: incoming.Status,
|
||||
AutoRenew: incoming.AutoRenew,
|
||||
CancelReason: incoming.CancelReason,
|
||||
}
|
||||
}
|
||||
return s.IsDraft, set
|
||||
}
|
||||
|
||||
func (s *Subscription) CanDelete() bool {
|
||||
return s.IsDraft || s.Status == SUBSCRIPTION_CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package subscription
|
||||
|
||||
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 subscriptionMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Subscription]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *subscriptionMongoAccessor {
|
||||
return &subscriptionMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Subscription]{
|
||||
Logger: logs.CreateLogger(tools.SUBSCRIPTION.String()),
|
||||
Request: request,
|
||||
Type: tools.SUBSCRIPTION,
|
||||
New: func() *Subscription { return &Subscription{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package billing_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestBill_StoreDraftDefault(t *testing.T) {
|
||||
b := &billing.Bill{}
|
||||
b.StoreDraftDefault()
|
||||
assert.True(t, b.IsDraft)
|
||||
}
|
||||
|
||||
func TestBill_CanDelete_Draft(t *testing.T) {
|
||||
b := &billing.Bill{}
|
||||
b.IsDraft = true
|
||||
assert.True(t, b.CanDelete())
|
||||
}
|
||||
|
||||
func TestBill_CanDelete_NonDraft(t *testing.T) {
|
||||
b := &billing.Bill{}
|
||||
b.IsDraft = false
|
||||
assert.False(t, b.CanDelete())
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = false
|
||||
set := &billing.Bill{Status: enum.PAID}
|
||||
ok, returned := b.CanUpdate(set)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, enum.PAID, returned.(*billing.Bill).Status)
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) {
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = false
|
||||
set := &billing.Bill{Status: enum.PENDING}
|
||||
ok, _ := b.CanUpdate(set)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_Draft(t *testing.T) {
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = true
|
||||
set := &billing.Bill{Status: enum.PAID}
|
||||
ok, _ := b.CanUpdate(set)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestBill_GetAccessor(t *testing.T) {
|
||||
b := &billing.Bill{}
|
||||
acc := b.GetAccessor(&tools.APIRequest{})
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
|
||||
func TestBill_GetAccessor_NilRequest(t *testing.T) {
|
||||
b := &billing.Bill{}
|
||||
acc := b.GetAccessor(nil)
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
|
||||
func TestGenerateBill_Basic(t *testing.T) {
|
||||
o := &order.Order{
|
||||
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
|
||||
}
|
||||
req := &tools.APIRequest{PeerID: "peer-abc"}
|
||||
b, err := billing.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")
|
||||
}
|
||||
|
||||
func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
|
||||
b := &billing.Bill{Total: 0}
|
||||
result, err := b.SumUpBill(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0.0, result.Total)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Billing process
|
||||
Scheduler process a drafted order + a first bill corresponding to every once buying.
|
||||
+84
-43
@@ -3,56 +3,90 @@ package booking
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/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"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
|
||||
/*
|
||||
* 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)
|
||||
|
||||
FromNano string `json:"from_nano,omitempty" bson:"priced_item,omitempty"`
|
||||
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" bson:"state"` // State is the state of the booking
|
||||
ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
|
||||
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
|
||||
|
||||
// Authorization: identifies who created this draft and the Check session it belongs to.
|
||||
// Used to verify UPDATE and DELETE orders from remote schedulers.
|
||||
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
|
||||
|
||||
// Peerless is true when the booked resource has no destination peer
|
||||
// (e.g. a public Docker Hub image). No peer confirmation or pricing
|
||||
// negotiation is needed; the booking is stored locally only.
|
||||
Peerless bool `json:"peerless,omitempty" bson:"peerless,omitempty"`
|
||||
|
||||
// OriginRef carries the registry reference of a peerless resource
|
||||
// (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it.
|
||||
OriginRef string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"`
|
||||
|
||||
// BillingStrategy est la fréquence de facturation appliquée à ce booking
|
||||
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
|
||||
// Transmis depuis WorkflowExecution.SelectedBillingStrategy lors du Book().
|
||||
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
|
||||
|
||||
// PaymentType est le mode de paiement choisi pour cette ressource spécifique
|
||||
// (PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR).
|
||||
// Résolu depuis WorkflowExecution.SelectedPaymentMode[itemID] lors du Book().
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
|
||||
}
|
||||
|
||||
// CheckBooking checks if a booking is possible on a specific compute resource
|
||||
func (wfa *Booking) Check(id string, start time.Time, end *time.Time, parrallelAllowed int) (bool, error) {
|
||||
// check if
|
||||
if end == nil {
|
||||
// if no end... then Book like a savage
|
||||
e := start.Add(time.Hour)
|
||||
end = &e
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
accessor := NewAccessor(nil)
|
||||
res, code, err := accessor.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{ // check if there is a booking on the same compute resource by filtering on the compute_resource_id, the state and the execution date
|
||||
"resource_id": {{Operator: dbs.EQUAL.String(), Value: id}},
|
||||
"state": {{Operator: dbs.EQUAL.String(), Value: enum.DRAFT.EnumIndex()}},
|
||||
"expected_start_date": {
|
||||
{Operator: dbs.LTE.String(), Value: primitive.NewDateTimeFromTime(*end)},
|
||||
{Operator: dbs.GTE.String(), Value: primitive.NewDateTimeFromTime(start)},
|
||||
},
|
||||
},
|
||||
}, "", wfa.IsDraft)
|
||||
if code != 200 {
|
||||
return false, err
|
||||
}
|
||||
return len(res) <= parrallelAllowed, nil
|
||||
return m
|
||||
}
|
||||
|
||||
func (d *Booking) GetDelayForLaunch() time.Duration {
|
||||
@@ -60,10 +94,10 @@ func (d *Booking) GetDelayForLaunch() time.Duration {
|
||||
}
|
||||
|
||||
func (d *Booking) GetDelayForFinishing() time.Duration {
|
||||
if d.ExpectedEndDate == nil {
|
||||
if d.ExpectedEndDate == nil || d.RealEndDate == nil {
|
||||
return time.Duration(0)
|
||||
}
|
||||
return d.RealEndDate.Sub(d.ExpectedStartDate)
|
||||
return d.RealEndDate.Sub(*d.ExpectedEndDate)
|
||||
}
|
||||
|
||||
func (d *Booking) GetUsualDuration() time.Duration {
|
||||
@@ -85,23 +119,30 @@ 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
|
||||
}
|
||||
|
||||
func (r *Booking) StoreDraftDefault() {
|
||||
r.IsDraft = false
|
||||
r.IsDraft = true
|
||||
r.State = enum.DRAFT
|
||||
}
|
||||
|
||||
func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
if !r.IsDraft && r.State != set.(*Booking).State || r.RealStartDate != set.(*Booking).RealStartDate || r.RealEndDate != set.(*Booking).RealEndDate {
|
||||
return true, &Booking{
|
||||
State: set.(*Booking).State,
|
||||
RealStartDate: set.(*Booking).RealStartDate,
|
||||
RealEndDate: set.(*Booking).RealEndDate,
|
||||
} // only state can be updated
|
||||
incoming := set.(*Booking)
|
||||
if !r.IsDraft && r.State != incoming.State || r.RealStartDate != incoming.RealStartDate || r.RealEndDate != incoming.RealEndDate {
|
||||
patch := &Booking{
|
||||
State: incoming.State,
|
||||
RealStartDate: incoming.RealStartDate,
|
||||
RealEndDate: incoming.RealEndDate,
|
||||
}
|
||||
// Auto-set RealStartDate when transitioning to STARTED and not already set
|
||||
if r.State != enum.STARTED && incoming.State == enum.STARTED && patch.RealStartDate == nil {
|
||||
now := time.Now()
|
||||
patch.RealStartDate = &now
|
||||
}
|
||||
return true, patch
|
||||
}
|
||||
// TODO : HERE WE CAN HANDLE THE CASE WHERE THE BOOKING IS DELAYED OR EXCEEDING OR ending sooner
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,25 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/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,32 +30,25 @@ 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) {
|
||||
if set.(*Booking).State == 0 {
|
||||
func (a *BookingMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
|
||||
if set["state"] == nil {
|
||||
return nil, 400, errors.New("state is required")
|
||||
}
|
||||
realSet := &Booking{State: set.(*Booking).State}
|
||||
return utils.GenericUpdateOne(realSet, id, a, &Booking{})
|
||||
set = map[string]interface{}{
|
||||
"state": set["state"],
|
||||
}
|
||||
return utils.GenericUpdateOne(set, id, a)
|
||||
}
|
||||
|
||||
func (a *bookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
return utils.GenericStoreOne(data, a)
|
||||
}
|
||||
|
||||
func (a *bookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
return utils.GenericStoreOne(data, a)
|
||||
}
|
||||
|
||||
func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
|
||||
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) {
|
||||
return utils.GenericDeleteOne(d.GetID(), a)
|
||||
// Direct raw delete to avoid infinite recursion:
|
||||
// GenericDeleteOne calls a.LoadOne which would re-enter this callback.
|
||||
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
|
||||
return nil, 410, errors.New("draft booking expired and deleted")
|
||||
}
|
||||
if (d.(*Booking).ExpectedEndDate) == nil {
|
||||
d.(*Booking).State = enum.FORGOTTEN
|
||||
@@ -67,20 +61,13 @@ 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)
|
||||
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
|
||||
utils.GenericDeleteOne(d.GetID(), a)
|
||||
// Direct raw delete to avoid infinite recursion (same as LoadOne callback).
|
||||
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
|
||||
return nil
|
||||
}
|
||||
if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package booking
|
||||
|
||||
import "cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
|
||||
// BookingMode is kept here as an alias for backward compatibility.
|
||||
// The canonical definition lives in models/common/enum.
|
||||
type BookingMode = enum.BookingMode
|
||||
|
||||
const (
|
||||
PLANNED = enum.PLANNED
|
||||
PREEMPTED = enum.PREEMPTED
|
||||
WHEN_POSSIBLE = enum.WHEN_POSSIBLE
|
||||
)
|
||||
@@ -0,0 +1,567 @@
|
||||
package planner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/booking"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/live"
|
||||
"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
|
||||
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
|
||||
}
|
||||
|
||||
// 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 (0–100).
|
||||
type PlannerSlot struct {
|
||||
Start time.Time `json:"start"`
|
||||
End time.Time `json:"end"`
|
||||
InstanceID string `json:"instance_id,omitempty"` // instance targeted by this booking
|
||||
BookingID string `json:"booking_id,omitempty"` // empty in shallow mode
|
||||
Usage map[string]float64 `json:"usage,omitempty"` // dimension -> % of max (0-100)
|
||||
}
|
||||
|
||||
// PlannerITF is the interface used by Planify to check resource availability.
|
||||
// *Planner satisfies this interface.
|
||||
type PlannerITF interface {
|
||||
NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time
|
||||
}
|
||||
|
||||
// Planner is a volatile (non-persisted) object that organises bookings by resource.
|
||||
// ComputeResource, StorageResource and HOSTED ServiceResource bookings appear in the schedule.
|
||||
// BlockedResources marks resources for which no matching Live was found at generation time:
|
||||
// any availability check against a blocked resource returns false immediately.
|
||||
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
|
||||
BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
|
||||
}
|
||||
|
||||
// Generate builds a full Planner from all active bookings.
|
||||
// Each slot includes the booking ID, the instance ID, and the usage percentage of every resource dimension.
|
||||
func Generate(request *tools.APIRequest) (*Planner, error) {
|
||||
return generate(request, false)
|
||||
}
|
||||
|
||||
// GenerateShallow builds a Planner from all active bookings without booking IDs.
|
||||
func GenerateShallow(request *tools.APIRequest) (*Planner, error) {
|
||||
return generate(request, true)
|
||||
}
|
||||
|
||||
func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
|
||||
accessor := booking.NewAccessor(request)
|
||||
// Include both confirmed (IsDraft=false) and draft (IsDraft=true) bookings
|
||||
// so the planner reflects the full picture: first-come first-served on all
|
||||
// pending reservations regardless of confirmation state.
|
||||
confirmed, code, err := accessor.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
|
||||
},
|
||||
}, "*", false, 0, 10000)
|
||||
if code != 200 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drafts, _, _ := accessor.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
|
||||
},
|
||||
}, "*", true, 0, 10000)
|
||||
bookings := append(confirmed, drafts...)
|
||||
|
||||
p := &Planner{
|
||||
GeneratedAt: time.Now(),
|
||||
Schedule: map[string][]*PlannerSlot{},
|
||||
Capacities: map[string]map[string]*InstanceCapacity{},
|
||||
BlockedResources: map[string]bool{},
|
||||
}
|
||||
|
||||
for _, b := range bookings {
|
||||
bk := b.(*booking.Booking)
|
||||
|
||||
// Skip terminal bookings — they no longer occupy capacity.
|
||||
switch bk.State {
|
||||
case enum.SUCCESS, enum.FAILURE, enum.FORGOTTEN, enum.CANCELLED:
|
||||
continue
|
||||
}
|
||||
|
||||
// Eligible resource types: compute, storage, and HOSTED services.
|
||||
if bk.ResourceType != tools.COMPUTE_RESOURCE &&
|
||||
bk.ResourceType != tools.STORAGE_RESOURCE &&
|
||||
bk.ResourceType != tools.SERVICE_RESOURCE {
|
||||
continue
|
||||
}
|
||||
|
||||
end := bk.ExpectedEndDate
|
||||
if end == nil {
|
||||
e := bk.ExpectedStartDate.UTC().Add(5 * time.Minute)
|
||||
end = &e
|
||||
}
|
||||
|
||||
instanceID, usage, cap, blocked := extractSlotData(bk, request)
|
||||
if blocked {
|
||||
p.BlockedResources[bk.ResourceID] = true
|
||||
continue
|
||||
}
|
||||
if instanceID == "" {
|
||||
instanceID = bk.InstanceID
|
||||
}
|
||||
|
||||
if cap != nil && instanceID != "" {
|
||||
if p.Capacities[bk.ResourceID] == nil {
|
||||
p.Capacities[bk.ResourceID] = map[string]*InstanceCapacity{}
|
||||
}
|
||||
p.Capacities[bk.ResourceID][instanceID] = cap
|
||||
}
|
||||
|
||||
slot := &PlannerSlot{
|
||||
Start: bk.ExpectedStartDate,
|
||||
End: *end,
|
||||
InstanceID: instanceID,
|
||||
Usage: usage,
|
||||
}
|
||||
if !shallow {
|
||||
slot.BookingID = bk.GetID()
|
||||
}
|
||||
|
||||
p.Schedule[bk.ResourceID] = append(p.Schedule[bk.ResourceID], slot)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Check reports whether the requested time window has enough remaining capacity
|
||||
// on the specified instance of the given resource.
|
||||
//
|
||||
// req describes the amounts needed; nil fields default to the full instance capacity.
|
||||
// If req itself is nil, the full capacity of every dimension is assumed.
|
||||
// If end is nil, a 1-hour window from start is assumed.
|
||||
//
|
||||
// A slot that overlaps the requested window is acceptable if, for every requested
|
||||
// dimension, existing usage + requested usage ≤ 100 %.
|
||||
// Slots targeting other instances are ignored.
|
||||
// If no capacity is known for this instance (never booked), it is fully available.
|
||||
func (p *Planner) Check(resourceID string, instanceID string, req *ResourceRequest, start time.Time, end *time.Time) bool {
|
||||
if p.BlockedResources[resourceID] {
|
||||
return false // no Live found at generation time — cannot book
|
||||
}
|
||||
if end == nil {
|
||||
e := start.Add(5 * time.Minute)
|
||||
end = &e
|
||||
}
|
||||
|
||||
cap := p.instanceCapacity(resourceID, instanceID)
|
||||
reqPct := toPercentages(req, cap)
|
||||
|
||||
slots, ok := p.Schedule[resourceID]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
for _, slot := range slots {
|
||||
// Only consider slots on the same instance
|
||||
if slot.InstanceID != instanceID {
|
||||
continue
|
||||
}
|
||||
// Only consider overlapping slots
|
||||
if !slot.Start.Before(*end) || !slot.End.After(start) {
|
||||
continue
|
||||
}
|
||||
// If capacity is unknown (reqPct empty), any overlap blocks the slot.
|
||||
if len(reqPct) == 0 {
|
||||
return false
|
||||
}
|
||||
// Combined usage must not exceed 100 % for any requested dimension
|
||||
for dim, needed := range reqPct {
|
||||
if slot.Usage[dim]+needed >= 100.0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// instanceCapacity returns the stored max capacity for a resource/instance pair.
|
||||
// Returns an empty (but non-nil) capacity when the instance has never been booked.
|
||||
func (p *Planner) instanceCapacity(resourceID, instanceID string) *InstanceCapacity {
|
||||
if instances, ok := p.Capacities[resourceID]; ok {
|
||||
if c, ok := instances[instanceID]; ok {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return &InstanceCapacity{
|
||||
CPUCores: map[string]float64{},
|
||||
GPUMemGB: map[string]float64{},
|
||||
}
|
||||
}
|
||||
|
||||
// toPercentages converts a ResourceRequest into a map of dimension -> percentage-of-max.
|
||||
// nil fields in req (or nil req) are treated as requesting the full capacity (100 %).
|
||||
func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float64 {
|
||||
pct := map[string]float64{}
|
||||
|
||||
if req == nil {
|
||||
for model := range cap.CPUCores {
|
||||
pct["cpu_"+model] = 100.0
|
||||
}
|
||||
for model := range cap.GPUMemGB {
|
||||
pct["gpu_"+model] = 100.0
|
||||
}
|
||||
if cap.RAMGB > 0 {
|
||||
pct["ram"] = 100.0
|
||||
}
|
||||
if cap.StorageGB > 0 {
|
||||
pct["storage"] = 100.0
|
||||
}
|
||||
return pct
|
||||
}
|
||||
|
||||
if req.CPUCores == nil {
|
||||
for model, maxCores := range cap.CPUCores {
|
||||
if maxCores > 0 {
|
||||
pct["cpu_"+model] = 100.0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for model, needed := range req.CPUCores {
|
||||
if maxCores, ok := cap.CPUCores[model]; ok && maxCores > 0 {
|
||||
pct["cpu_"+model] = (needed / maxCores) * 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.GPUMemGB == nil {
|
||||
for model, maxMem := range cap.GPUMemGB {
|
||||
if maxMem > 0 {
|
||||
pct["gpu_"+model] = 100.0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for model, needed := range req.GPUMemGB {
|
||||
if maxMem, ok := cap.GPUMemGB[model]; ok && maxMem > 0 {
|
||||
pct["gpu_"+model] = (needed / maxMem) * 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.RAMGB == nil {
|
||||
if cap.RAMGB > 0 {
|
||||
pct["ram"] = 100.0
|
||||
}
|
||||
} else if cap.RAMGB > 0 {
|
||||
pct["ram"] = (*req.RAMGB / cap.RAMGB) * 100.0
|
||||
}
|
||||
|
||||
if req.StorageGB == nil {
|
||||
if cap.StorageGB > 0 {
|
||||
pct["storage"] = 100.0
|
||||
}
|
||||
} else if cap.StorageGB > 0 {
|
||||
pct["storage"] = (*req.StorageGB / cap.StorageGB) * 100.0
|
||||
}
|
||||
|
||||
// HOSTED service: each booking consumes one call slot.
|
||||
if cap.MaxConcurrent > 0 {
|
||||
pct["calls"] = (1.0 / cap.MaxConcurrent) * 100.0
|
||||
}
|
||||
|
||||
return pct
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// extractSlotData parses the booking's PricedItem, loads the corresponding Live resource
|
||||
// as the authoritative capacity source, and returns the instance ID, usage percentages,
|
||||
// capacity, and whether a matching Live was found.
|
||||
// blocked=true means no Live exists for this resource; the resource must not be scheduled.
|
||||
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
|
||||
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)
|
||||
case tools.SERVICE_RESOURCE:
|
||||
instanceID, usage, cap, blocked = extractServiceSlot(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
|
||||
}
|
||||
|
||||
// extractServiceSlot extracts the instance ID, usage, and capacity for a HOSTED service booking.
|
||||
// The LiveService is the authoritative source for MaxConcurrent — the ServiceResource is not trusted.
|
||||
// If no LiveService references this resourceID, blocked=true signals the resource cannot be scheduled.
|
||||
func extractServiceSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
|
||||
usage = map[string]float64{}
|
||||
|
||||
var priced resources.PricedServiceResource
|
||||
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
|
||||
blocked = true
|
||||
return
|
||||
}
|
||||
|
||||
// LiveService is the authoritative capacity source — look it up by resources_id.
|
||||
liveResults, _, err := (&live.LiveService{}).GetAccessor(request).Search(
|
||||
&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"resources_id": {{Operator: dbs.EQUAL.String(), Value: resourceID}},
|
||||
},
|
||||
}, "*", false, 0, 1)
|
||||
if err != nil || len(liveResults) == 0 {
|
||||
blocked = true // no Live → cannot schedule
|
||||
return
|
||||
}
|
||||
ls := liveResults[0].(*live.LiveService)
|
||||
if ls.MaxConcurrent <= 0 {
|
||||
blocked = true
|
||||
return
|
||||
}
|
||||
|
||||
// Instance ID: use the first instance referenced by the priced item.
|
||||
instanceID = priced.GetID()
|
||||
if instanceID == "" {
|
||||
instanceID = resourceID // fallback: treat the resource itself as the instance key
|
||||
}
|
||||
|
||||
maxC := float64(ls.MaxConcurrent)
|
||||
cap = &InstanceCapacity{
|
||||
CPUCores: map[string]float64{},
|
||||
GPUMemGB: map[string]float64{},
|
||||
MaxConcurrent: maxC,
|
||||
}
|
||||
usage["calls"] = (1.0 / maxC) * 100.0
|
||||
return
|
||||
}
|
||||
|
||||
// findComputeInstance returns the instance referenced by the priced item's InstancesRefs,
|
||||
// falling back to the first available instance.
|
||||
func findComputeInstance(compute *resources.ComputeResource, refs map[string]string) *resources.ComputeResourceInstance {
|
||||
for _, inst := range compute.Instances {
|
||||
if _, ok := refs[inst.GetID()]; ok {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
if len(compute.Instances) > 0 {
|
||||
return compute.Instances[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findStorageInstance returns the instance referenced by the priced item's InstancesRefs,
|
||||
// falling back to the first available instance.
|
||||
func findStorageInstance(storage *resources.StorageResource, refs map[string]string) *resources.StorageResourceInstance {
|
||||
for _, inst := range storage.Instances {
|
||||
if _, ok := refs[inst.GetID()]; ok {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
if len(storage.Instances) > 0 {
|
||||
return storage.Instances[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// totalCPUCores returns the total number of cores for a given CPU model across all nodes.
|
||||
// It multiplies the per-chip core count (from the instance's CPU spec) by the total
|
||||
// number of chips of that model across all nodes (chip_count × node.Quantity).
|
||||
// Falls back to the spec's core count if no nodes are defined.
|
||||
func totalCPUCores(instance *resources.ComputeResourceInstance, model string) float64 {
|
||||
spec, ok := instance.CPUs[model]
|
||||
if !ok || spec == nil || spec.Cores == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(instance.Nodes) == 0 {
|
||||
return float64(spec.Cores)
|
||||
}
|
||||
totalChips := int64(0)
|
||||
for _, node := range instance.Nodes {
|
||||
if chipCount, ok := node.CPUs[model]; ok {
|
||||
totalChips += chipCount * max(node.Quantity, 1)
|
||||
}
|
||||
}
|
||||
if totalChips == 0 {
|
||||
return float64(spec.Cores)
|
||||
}
|
||||
return float64(totalChips * int64(spec.Cores))
|
||||
}
|
||||
|
||||
// totalGPUMemory returns the total GPU memory (GB) for a given model across all nodes.
|
||||
// Falls back to the spec's memory if no nodes are defined.
|
||||
func totalGPUMemory(instance *resources.ComputeResourceInstance, model string) float64 {
|
||||
spec, ok := instance.GPUs[model]
|
||||
if !ok || spec == nil || spec.MemoryGb == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(instance.Nodes) == 0 {
|
||||
return spec.MemoryGb
|
||||
}
|
||||
totalUnits := int64(0)
|
||||
for _, node := range instance.Nodes {
|
||||
if unitCount, ok := node.GPUs[model]; ok {
|
||||
totalUnits += unitCount * max(node.Quantity, 1)
|
||||
}
|
||||
}
|
||||
if totalUnits == 0 {
|
||||
return spec.MemoryGb
|
||||
}
|
||||
return float64(totalUnits) * spec.MemoryGb
|
||||
}
|
||||
|
||||
// totalRAM returns the total RAM (GB) across all nodes of a compute instance.
|
||||
func totalRAM(instance *resources.ComputeResourceInstance) float64 {
|
||||
total := float64(0)
|
||||
for _, node := range instance.Nodes {
|
||||
if node.RAM != nil && node.RAM.SizeGb > 0 {
|
||||
total += node.RAM.SizeGb * float64(max(node.Quantity, 1))
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// NextAvailableStart returns the earliest time >= start when resourceID/instanceID has a
|
||||
// free window of duration d. Slots are scanned in order so a single linear pass suffices.
|
||||
// If the planner has no slots for this resource/instance, start is returned unchanged.
|
||||
func (p *Planner) NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time {
|
||||
slots := p.Schedule[resourceID]
|
||||
if len(slots) == 0 {
|
||||
return start
|
||||
}
|
||||
// Collect and sort slots for this instance by start time.
|
||||
relevant := make([]*PlannerSlot, 0, len(slots))
|
||||
for _, s := range slots {
|
||||
if s.InstanceID == instanceID {
|
||||
relevant = append(relevant, s)
|
||||
}
|
||||
}
|
||||
sort.Slice(relevant, func(i, j int) bool { return relevant[i].Start.Before(relevant[j].Start) })
|
||||
|
||||
end := start.Add(d)
|
||||
for _, slot := range relevant {
|
||||
if !slot.Start.Before(end) {
|
||||
break // all remaining slots start after our window — done
|
||||
}
|
||||
if slot.End.After(start) {
|
||||
// conflict: push start to after this slot
|
||||
start = slot.End
|
||||
end = start.Add(d)
|
||||
}
|
||||
}
|
||||
return start
|
||||
}
|
||||
@@ -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(-10 * time.Minute)
|
||||
end := start.Add(5 * time.Minute)
|
||||
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)
|
||||
}
|
||||
@@ -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,20 +84,16 @@ 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 {
|
||||
return NewAccessor(request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
func (d *CollaborativeArea) Trim() *CollaborativeArea {
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *CollaborativeArea) StoreDraftDefault() {
|
||||
d.AllowedPeersGroup = map[string][]string{
|
||||
d.CreatorID: []string{"*"},
|
||||
d.CreatorID: {"*"},
|
||||
}
|
||||
d.IsDraft = false
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
@@ -52,8 +53,8 @@ func (a *collaborativeAreaMongoAccessor) DeleteOne(id string) (utils.DBObject, i
|
||||
}
|
||||
|
||||
// UpdateOne updates a collaborative area in the database, given its ID and the new data, it automatically share to peers if the workspace is shared
|
||||
func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
|
||||
res, code, err := utils.GenericUpdateOne(set.(*CollaborativeArea).Trim(), id, a, &CollaborativeArea{})
|
||||
func (a *collaborativeAreaMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
|
||||
res, code, err := utils.GenericUpdateOne(set, id, a)
|
||||
// a.deleteToPeer(res.(*CollaborativeArea)) // delete the collaborative area on the peer
|
||||
a.sharedWorkflow(res.(*CollaborativeArea), id) // replace all shared workflows
|
||||
a.sharedWorkspace(res.(*CollaborativeArea), id) // replace all collaborative areas (not shared worspace obj but workspace one)
|
||||
@@ -63,14 +64,19 @@ func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string
|
||||
|
||||
// StoreOne stores a collaborative area in the database, it automatically share to peers if the workspace is shared
|
||||
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
|
||||
d, code, err := utils.GenericStoreOne(data.(*CollaborativeArea).Trim(), a)
|
||||
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = pp.GetID()
|
||||
d, code, err := utils.GenericStoreOne(data, a)
|
||||
if code == 200 {
|
||||
a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows
|
||||
a.sharedWorkspace(d.(*CollaborativeArea), d.GetID()) // create all collaborative areas
|
||||
@@ -79,19 +85,13 @@ 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{
|
||||
Or: map[string][]dbs.Filter{
|
||||
"abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}},
|
||||
},
|
||||
}, "", isDrafted)
|
||||
fmt.Println(res, arr, isDrafted, a)
|
||||
}, "", isDrafted, 0, int64(len(arr)))
|
||||
if code == 200 {
|
||||
for _, r := range res {
|
||||
new = append(new, r.(T))
|
||||
@@ -125,23 +125,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)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -153,7 +140,9 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
|
||||
eld := eldest.(*CollaborativeArea)
|
||||
if eld.Workspaces != nil { // update all your workspaces in the eldest by replacing shared ref by an empty string
|
||||
for _, v := range eld.Workspaces {
|
||||
a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: ""}, v)
|
||||
a.workspaceAccessor.UpdateOne(map[string]interface{}{
|
||||
"shared": "",
|
||||
}, v)
|
||||
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
|
||||
continue
|
||||
}
|
||||
@@ -169,7 +158,10 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
|
||||
}
|
||||
if shared.Workspaces != nil {
|
||||
for _, v := range shared.Workspaces { // update all the collaborative areas
|
||||
workspace, code, _ := a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: shared.UUID}, v) // add the shared ref to workspace
|
||||
workspace, code, _ := a.workspaceAccessor.UpdateOne(
|
||||
map[string]interface{}{
|
||||
"shared": shared.UUID,
|
||||
}, v) // add the shared ref to workspace
|
||||
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
|
||||
continue
|
||||
}
|
||||
@@ -209,7 +201,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
|
||||
} // kick the shared reference in your old shared workflow
|
||||
n := &w.Workflow{}
|
||||
n.Shared = new
|
||||
a.workflowAccessor.UpdateOne(n, v)
|
||||
a.workflowAccessor.UpdateOne(n.Serialize(n), v)
|
||||
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
|
||||
continue
|
||||
}
|
||||
@@ -231,7 +223,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
|
||||
s := data.(*w.Workflow)
|
||||
if !slices.Contains(s.Shared, id) {
|
||||
s.Shared = append(s.Shared, id)
|
||||
workflow, code, _ := a.workflowAccessor.UpdateOne(s, v)
|
||||
workflow, code, _ := a.workflowAccessor.UpdateOne(s.Serialize(s), v)
|
||||
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
|
||||
continue
|
||||
}
|
||||
@@ -254,6 +246,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 +265,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-37
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package enum
|
||||
|
||||
type BookingMode int
|
||||
|
||||
const (
|
||||
PLANNED BookingMode = iota // timing prédictible
|
||||
PREEMPTED // peut être interrompu, premium payé
|
||||
WHEN_POSSIBLE // timing imprévisible
|
||||
)
|
||||
|
||||
/*
|
||||
3 notions distinctes :
|
||||
- BookingMode : QUAND est exécuté (PLANNED / PREEMPTED / WHEN_POSSIBLE)
|
||||
- BillingStrategy : fréquence de facturation (ONCE / WEEKLY / MONTHLY / YEARLY)
|
||||
- PaymentType : mode de paiement par ressource (PAY_ONCE / PAY_EVERY_MONTH / ...)
|
||||
*/
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
FORGOTTEN
|
||||
DELAYED
|
||||
CANCELLED
|
||||
IN_PREPARATION
|
||||
)
|
||||
|
||||
var str = [...]string{
|
||||
@@ -43,6 +44,7 @@ var str = [...]string{
|
||||
"forgotten",
|
||||
"delayed",
|
||||
"cancelled",
|
||||
"in_preparation",
|
||||
}
|
||||
|
||||
func FromInt(i int) string {
|
||||
@@ -60,5 +62,5 @@ func (d BookingStatus) EnumIndex() int {
|
||||
|
||||
// List
|
||||
func StatusList() []BookingStatus {
|
||||
return []BookingStatus{DRAFT, SCHEDULED, STARTED, FAILURE, SUCCESS, FORGOTTEN, DELAYED, CANCELLED}
|
||||
return []BookingStatus{DRAFT, SCHEDULED, STARTED, FAILURE, SUCCESS, FORGOTTEN, DELAYED, CANCELLED, IN_PREPARATION}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package models
|
||||
|
||||
type PathSource struct {
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"` // Image is the container image TEMPO
|
||||
IsReachable bool `json:"is_reachable,omitempty" bson:"is_reachable,omitempty"`
|
||||
Args string `json:"args,omitempty" bson:"args,omitempty"` // Args is the container arguments
|
||||
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
|
||||
Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
|
||||
Args string `json:"args,omitempty" bson:"args,omitempty"` // Args is the container arguments
|
||||
Env map[string]string `json:"env,omitempty" bson:"env,omitempty"` // Env is the container environment variables
|
||||
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
|
||||
|
||||
Exposes []Expose `bson:"exposes,omitempty" json:"exposes,omitempty"` // Expose is the execution
|
||||
}
|
||||
|
||||
type Expose struct {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
Regular → Executable
+19
-17
@@ -7,36 +7,38 @@ import (
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 {
|
||||
near := float64(10000000000) // set a high value
|
||||
func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
|
||||
near := float64(-1) // unset sentinel
|
||||
for _, items := range planned { // loop through the planned items
|
||||
for _, priced := range items { // loop through the priced items
|
||||
if priced.GetLocationStart() == nil { // if the start is nil,
|
||||
continue // skip the iteration
|
||||
}
|
||||
newS := priced.GetLocationStart() // get the start
|
||||
if newS.Sub(start).Seconds() < near { // if the difference between the start and the new start is less than the nearest start
|
||||
near = newS.Sub(start).Seconds()
|
||||
newS := priced.GetLocationStart() // get the start
|
||||
diff := newS.Sub(start).Seconds() // get the difference
|
||||
if near < 0 || diff < near { // if the difference is less than the nearest start
|
||||
near = diff
|
||||
}
|
||||
}
|
||||
}
|
||||
if near < 0 {
|
||||
return 0 // no items found, start at the given start time
|
||||
}
|
||||
return near
|
||||
}
|
||||
|
||||
func GetPlannerLongestTime(end *time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 {
|
||||
if end == nil {
|
||||
return -1
|
||||
}
|
||||
// GetPlannerLongestTime returns the sum of all processing+service durations.
|
||||
// Returns -1 if any item is open-ended (no deadline).
|
||||
func GetPlannerLongestTime(planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
|
||||
longestTime := float64(0)
|
||||
for _, priced := range planned[tools.PROCESSING_RESOURCE] {
|
||||
if priced.GetLocationEnd() == nil {
|
||||
continue
|
||||
for _, dt := range []tools.DataType{tools.PROCESSING_RESOURCE, tools.SERVICE_RESOURCE} {
|
||||
for _, priced := range planned[dt] {
|
||||
d := priced.GetExplicitDurationInS()
|
||||
if d < 0 {
|
||||
return -1
|
||||
}
|
||||
longestTime += d
|
||||
}
|
||||
newS := priced.GetLocationEnd()
|
||||
if end == nil && longestTime < newS.Sub(*end).Seconds() {
|
||||
longestTime = newS.Sub(*end).Seconds()
|
||||
}
|
||||
// get the nearest start from start var
|
||||
}
|
||||
return longestTime
|
||||
}
|
||||
|
||||
Regular → Executable
+10
-2
@@ -3,18 +3,26 @@ package pricing
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type PricedItemITF interface {
|
||||
GetID() string
|
||||
GetName() string
|
||||
GetInstanceID() string
|
||||
GetType() tools.DataType
|
||||
IsPurchased() bool
|
||||
IsPurchasable() bool
|
||||
IsBooked() bool
|
||||
GetQuantity() int
|
||||
AddQuantity(amount int)
|
||||
GetBookingMode() enum.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)
|
||||
}
|
||||
|
||||
Regular → Executable
+52
-5
@@ -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
|
||||
@@ -26,16 +28,61 @@ func RefundTypeList() []RefundType {
|
||||
return []RefundType{REFUND_DEAD_END, REFUND_ON_ERROR, REFUND_ON_EARLY_END}
|
||||
}
|
||||
|
||||
type PaymentType int
|
||||
|
||||
const (
|
||||
PAY_ONCE PaymentType = iota
|
||||
PAY_EVERY_WEEK
|
||||
PAY_EVERY_MONTH
|
||||
PAY_EVERY_YEAR
|
||||
)
|
||||
|
||||
func (t PaymentType) String() string {
|
||||
return [...]string{"PAY ONCE", "PAY_EVERY_WEEK", "PAY_EVERY_MONTH", "PAY_EVERY_YEAR"}[t]
|
||||
}
|
||||
|
||||
func PaymentTypeList() []PaymentType {
|
||||
return []PaymentType{PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR}
|
||||
}
|
||||
|
||||
type AccessPricingProfile[T Strategy] struct { // only use for acces such as : DATA && PROCESSING
|
||||
Pricing PricingStrategy[T] `json:"pricing,omitempty" bson:"pricing,omitempty"` // Price is the price of the resource
|
||||
DefaultRefund RefundType `json:"default_refund" bson:"default_refund"` // DefaultRefund is the default refund type of the pricing
|
||||
RefundRatio int32 `json:"refund_ratio" bson:"refund_ratio" default:"0"` // RefundRatio is the refund ratio if missing
|
||||
AllowedPaymentType []PaymentType `json:"allowed_payment_type,omitempty" bson:"allowed_payment_type,omitempty"` // Price is the price of the resource
|
||||
Pricing PricingStrategy[T] `json:"pricing,omitempty" bson:"pricing,omitempty"` // Price is the price of the resource
|
||||
DefaultRefund RefundType `json:"default_refund" bson:"default_refund"` // DefaultRefund is the default refund type of the pricing
|
||||
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: SUBSCRIPTION,
|
||||
TimePricingStrategy: PER_SECOND,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ExploitPrivilegeStrategy int
|
||||
|
||||
const (
|
||||
|
||||
Regular → Executable
+110
-16
@@ -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
|
||||
SUBSCRIPTION BuyingStrategy = iota // is a permanent buying ( predictible )
|
||||
UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible )
|
||||
PERMANENT // 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{"SUBSCRIPTION", "UNDEFINED_SUBSCRIPTION", "PERMANENT"}[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{SUBSCRIPTION, UNDEFINED_SUBSCRIPTION, PERMANENT}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
@@ -62,13 +111,11 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
|
||||
|
||||
fromAverageDuration := after.Sub(now).Seconds()
|
||||
var tEnd time.Time
|
||||
if end == nil {
|
||||
tEnd = start.Add(1 * time.Hour)
|
||||
} else {
|
||||
fromDateDuration := float64(0)
|
||||
if end != nil {
|
||||
tEnd = *end
|
||||
fromDateDuration = tEnd.Sub(start).Seconds()
|
||||
}
|
||||
fromDateDuration := tEnd.Sub(start).Seconds()
|
||||
|
||||
if fromAverageDuration > fromDateDuration {
|
||||
return fromAverageDuration
|
||||
}
|
||||
@@ -77,6 +124,9 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
|
||||
|
||||
func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInSecond float64, start time.Time, end *time.Time) (float64, error) {
|
||||
locationDurationInSecond = getAverageTimeInSecond(locationDurationInSecond, start, end)
|
||||
if locationDurationInSecond <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
priceStr := fmt.Sprintf("%v", price)
|
||||
p, err := strconv.ParseFloat(priceStr, 64)
|
||||
if err != nil {
|
||||
@@ -101,19 +151,48 @@ 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
|
||||
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
|
||||
|
||||
// NO NEED ?
|
||||
BuyingStrategy BuyingStrategy `json:"buying_strategy" bson:"buying_strategy" default:"0"` // BuyingStrategy is the buying strategy of the pricing
|
||||
TimePricingStrategy TimePricingStrategy `json:"time_pricing_strategy" bson:"time_pricing_strategy" default:"0"` // TimePricingStrategy is the time pricing strategy of the pricing
|
||||
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) {
|
||||
if p.BuyingStrategy == SUBSCRIPTION {
|
||||
return BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
|
||||
} else if p.BuyingStrategy == UNLIMITED {
|
||||
func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time, variations []*PricingVariation) (float64, error) {
|
||||
switch p.BuyingStrategy {
|
||||
case SUBSCRIPTION:
|
||||
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
|
||||
case 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 +208,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(10 * time.Minute)
|
||||
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(5 * time.Minute)
|
||||
|
||||
// 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)
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package execution_verification
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
/*
|
||||
* ExecutionVerification is a struct that represents a list of workflow executions
|
||||
* Warning: No user can write (del, post, put) a workflow execution, it is only used by the system
|
||||
* workflows generate their own executions
|
||||
*/
|
||||
type ExecutionVerification struct {
|
||||
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
|
||||
WorkflowID string `json:"workflow_id" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
|
||||
Payload string `json:"payload" bson:"payload,omitempty"`
|
||||
IsVerified bool `json:"is_verified" bson:"is_verified,omitempty"`
|
||||
Validate bool `json:"validate" bson:"validate,omitempty"`
|
||||
}
|
||||
|
||||
func (ri *ExecutionVerification) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "wokflow":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.WORKFLOW)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func (r *ExecutionVerification) StoreDraftDefault() {
|
||||
r.IsDraft = false // TODO: TEMPORARY
|
||||
}
|
||||
|
||||
func (d *ExecutionVerification) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
func (d *ExecutionVerification) VerifyAuth(callName string, request *tools.APIRequest) bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package execution_verification
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type ExecutionVerificationMongoAccessor struct {
|
||||
utils.AbstractAccessor[*ExecutionVerification]
|
||||
shallow bool
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *ExecutionVerificationMongoAccessor {
|
||||
return &ExecutionVerificationMongoAccessor{
|
||||
shallow: false,
|
||||
AbstractAccessor: utils.AbstractAccessor[*ExecutionVerification]{
|
||||
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
|
||||
Request: request,
|
||||
Type: tools.WORKFLOW_EXECUTION,
|
||||
New: func() *ExecutionVerification { return &ExecutionVerification{} },
|
||||
NotImplemented: []string{"DeleteOne", "StoreOne", "CopyOne"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wfa *ExecutionVerificationMongoAccessor) StoreOne(set utils.DBObject) (utils.DBObject, int, error) {
|
||||
set.(*ExecutionVerification).IsVerified = false
|
||||
return utils.GenericStoreOne(set, wfa)
|
||||
}
|
||||
|
||||
func (wfa *ExecutionVerificationMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
|
||||
set = map[string]interface{}{
|
||||
"is_verified": true,
|
||||
"validate": set["validate"],
|
||||
}
|
||||
return utils.GenericUpdateOne(set, id, wfa)
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
)
|
||||
|
||||
type LiveInterface interface {
|
||||
utils.DBObject
|
||||
IsCompatible(service map[string]interface{}) bool
|
||||
GetMonitorPath() string
|
||||
GetResourcesID() []string
|
||||
SetResourcesID(string)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"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 GeoPoint struct {
|
||||
Latitude float64 `json:"latitude,omitempty" bson:"latitude,omitempty"`
|
||||
Longitude float64 `json:"longitude,omitempty" bson:"longitude,omitempty"`
|
||||
}
|
||||
|
||||
type AbstractLive struct {
|
||||
utils.AbstractObject
|
||||
Certs LiveCerts `json:"certs,omitempty" bson:"certs,omitempty"`
|
||||
|
||||
MonitorPath string `json:"monitor_path,omitempty" bson:"monitor_path,omitempty"`
|
||||
Location 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 (ri *AbstractLive) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "resource":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.WORKFLOW_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.DATA_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.COMPUTE_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.STORAGE_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.PROCESSING_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.SERVICE_RESOURCE)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func (d *AbstractLive) GetMonitorPath() string {
|
||||
return d.MonitorPath
|
||||
}
|
||||
|
||||
func (d *AbstractLive) GetResourcesID() []string {
|
||||
return d.ResourcesID
|
||||
}
|
||||
|
||||
func (d *AbstractLive) SetResourcesID(resourcesid string) {
|
||||
if slices.Contains(d.ResourcesID, resourcesid) {
|
||||
d.ResourcesID = append(d.ResourcesID, resourcesid)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AbstractLive) GetResourceType() tools.DataType {
|
||||
return tools.INVALID
|
||||
}
|
||||
|
||||
func (r *AbstractLive) StoreDraftDefault() {
|
||||
r.IsDraft = false
|
||||
}
|
||||
|
||||
func (r *AbstractLive) CanDelete() bool {
|
||||
return r.IsDraft // only draft ComputeUnits can be deleted
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
/*
|
||||
* LiveDatacenter is a struct that represents a compute units in your datacenters
|
||||
*/
|
||||
|
||||
type ComputeNode struct {
|
||||
Name string `json:"name,omitempty" bson:"name,omitempty"`
|
||||
Quantity int64 `json:"quantity" bson:"quantity" default:"1"`
|
||||
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"` // RAM is the RAM
|
||||
CPUs map[string]int64 `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
|
||||
GPUs map[string]int64 `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
|
||||
}
|
||||
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 []*ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
|
||||
}
|
||||
|
||||
func (r *LiveDatacenter) IsCompatible(service map[string]interface{}) bool {
|
||||
return service["infrastructure"] == r.Infrastructure && service["architecture"] == r.Architecture
|
||||
}
|
||||
|
||||
func (d *LiveDatacenter) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*LiveDatacenter](tools.LIVE_DATACENTER, request) // Create a new instance of the accessor
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package live
|
||||
|
||||
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 liveMongoAccessor[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) *liveMongoAccessor[T] {
|
||||
return &liveMongoAccessor[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{}
|
||||
case tools.LIVE_SERVICE:
|
||||
return &LiveService{}
|
||||
}
|
||||
return &LiveDatacenter{}
|
||||
},
|
||||
NotImplemented: []string{"CopyOne"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wfa *liveMongoAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
|
||||
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit)
|
||||
}
|
||||
|
||||
func (wfa *liveMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
|
||||
if filters == nil && search == "*" {
|
||||
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
|
||||
return d
|
||||
}, isDraft, wfa, offset, limit)
|
||||
}
|
||||
return utils.GenericSearch[T](filters, search, wfa.New().GetObjectFilters(search),
|
||||
func(d utils.DBObject) utils.ShallowDBObject {
|
||||
return d
|
||||
}, isDraft, wfa, offset, limit)
|
||||
}
|
||||
|
||||
func (a *liveMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
|
||||
return func(d utils.DBObject) utils.ShallowDBObject {
|
||||
return d
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"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 ServiceProtocol int
|
||||
|
||||
const (
|
||||
HTTP ServiceProtocol = iota
|
||||
GRPC
|
||||
WEBSOCKET
|
||||
TCP
|
||||
)
|
||||
|
||||
func (p ServiceProtocol) String() string {
|
||||
return [...]string{"HTTP", "GRPC", "WEBSOCKET", "TCP"}[p]
|
||||
}
|
||||
|
||||
// LiveService is the authoritative description of a hosted service run by the peer.
|
||||
// MaxConcurrent is the only capacity dimension that matters for scheduling:
|
||||
// it caps the number of simultaneous callers the service can accept.
|
||||
// All other service metadata (endpoint, protocol) is live-verified here
|
||||
// rather than trusted from the ServiceResource, which may be stale.
|
||||
type LiveService struct {
|
||||
AbstractLive
|
||||
MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"`
|
||||
Protocol ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
|
||||
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
|
||||
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,omitempty"`
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
|
||||
}
|
||||
|
||||
func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*LiveService](tools.LIVE_SERVICE, request)
|
||||
}
|
||||
|
||||
func (r *LiveService) IsCompatible(service map[string]interface{}) bool {
|
||||
return service["infrastructure"] == r.Infrastructure
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
/*
|
||||
* LiveStorage is a struct that represents a compute units in your datacenters
|
||||
*/
|
||||
|
||||
type LiveStorage struct {
|
||||
AbstractLive
|
||||
|
||||
StorageType enum.StorageType `bson:"storage_type" json:"storage_type" default:"-1"`
|
||||
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 (r *LiveStorage) IsCompatible(service map[string]interface{}) bool {
|
||||
return service["storage_type"] == r.StorageType
|
||||
}
|
||||
|
||||
func (d *LiveStorage) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*LiveStorage](tools.LIVE_STORAGE, request) // Create a new instance of the accessor
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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_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_SetResourcesID_NoDuplication(t *testing.T) {
|
||||
s := &live.LiveStorage{}
|
||||
s.SetResourcesID("storage-1")
|
||||
s.SetResourcesID("storage-1")
|
||||
assert.Len(t, s.GetResourcesID(), 1)
|
||||
}
|
||||
+47
-22
@@ -2,8 +2,17 @@ package models
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/allowed_image"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"cloud.o-forge.io/core/oc-lib/models/execution_verification"
|
||||
"cloud.o-forge.io/core/oc-lib/models/live"
|
||||
"cloud.o-forge.io/core/oc-lib/models/order"
|
||||
"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"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/booking"
|
||||
@@ -11,7 +20,6 @@ import (
|
||||
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
|
||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||
resource "cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"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"
|
||||
w3 "cloud.o-forge.io/core/oc-lib/models/workspace"
|
||||
@@ -21,39 +29,56 @@ 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{
|
||||
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.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{} },
|
||||
tools.PEER.String(): func() utils.DBObject { return &peer.Peer{} },
|
||||
tools.COLLABORATIVE_AREA.String(): func() utils.DBObject { return &collaborative_area.CollaborativeArea{} },
|
||||
tools.RULE.String(): func() utils.DBObject { return &rule.Rule{} },
|
||||
tools.BOOKING.String(): func() utils.DBObject { return &booking.Booking{} },
|
||||
tools.WORKFLOW_HISTORY.String(): func() utils.DBObject { return &w2.WorkflowHistory{} },
|
||||
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{} },
|
||||
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.SERVICE_RESOURCE.String(): func() utils.DBObject { return &resource.ServiceResource{} },
|
||||
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{} },
|
||||
tools.PEER.String(): func() utils.DBObject { return &peer.Peer{} },
|
||||
tools.COLLABORATIVE_AREA.String(): func() utils.DBObject { return &collaborative_area.CollaborativeArea{} },
|
||||
tools.RULE.String(): func() utils.DBObject { return &rule.Rule{} },
|
||||
tools.BOOKING.String(): func() utils.DBObject { return &booking.Booking{} },
|
||||
tools.WORKFLOW_HISTORY.String(): func() utils.DBObject { return &w2.WorkflowHistory{} },
|
||||
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.LIVE_SERVICE.String(): func() utils.DBObject { return &live.LiveService{} },
|
||||
tools.BILL.String(): func() utils.DBObject { return &billing.Bill{} },
|
||||
tools.PAYMENT.String(): func() utils.DBObject { return &payment.Payment{} },
|
||||
tools.REFUND.String(): func() utils.DBObject { return &refund.Refund{} },
|
||||
tools.DISCOUNT.String(): func() utils.DBObject { return &discount.Discount{} },
|
||||
tools.SUBSCRIPTION.String(): func() utils.DBObject { return &subscription.Subscription{} },
|
||||
tools.EXECUTION_VERIFICATION.String(): func() utils.DBObject { return &execution_verification.ExecutionVerification{} },
|
||||
tools.ALLOWED_IMAGE.String(): func() utils.DBObject { return &allowed_image.AllowedImage{} },
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
+13
-287
@@ -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,17 @@ 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 groupe les bookings par fréquence de facturation, peuplé par GenerateOrder.
|
||||
Billing map[pricing.BillingStrategy][]*booking.Booking `json:"billing" bson:"billing"`
|
||||
|
||||
// SubscriptionIDs liste les abonnements récurrents créés pour cet order
|
||||
// (un par peer × stratégie de facturation). Peuplé par DraftFirstBill.
|
||||
SubscriptionIDs []string `json:"subscription_ids,omitempty" bson:"subscription_ids,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Order) StoreDraftDefault() {
|
||||
@@ -46,287 +45,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+152
-25
@@ -2,45 +2,186 @@ package peer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
"github.com/biter777/countries"
|
||||
)
|
||||
|
||||
// now write a go enum for the state partner with self, blacklist, partner
|
||||
|
||||
type PeerState int
|
||||
type PeerPerm int
|
||||
|
||||
const (
|
||||
NONE PeerState = iota
|
||||
READ PeerRelation = iota
|
||||
WRITE
|
||||
MONITOR
|
||||
)
|
||||
|
||||
type PeerRelation int
|
||||
|
||||
const (
|
||||
NONE PeerRelation = iota
|
||||
SELF
|
||||
PARTNER
|
||||
BLACKLIST
|
||||
PENDING_PARTNER
|
||||
MASTER
|
||||
NANO
|
||||
PENDING_NANO
|
||||
PENDING_MASTER
|
||||
)
|
||||
|
||||
func (m PeerState) String() string {
|
||||
return [...]string{"NONE", "SELF", "PARTNER", "BLACKLIST"}[m]
|
||||
var path = []string{"known", "self", "partner", "blacklist", "pending_partner", "master", "nano", "pending_nano", "pending_master"}
|
||||
|
||||
func GetRelationPath(str string) int {
|
||||
for i, p := range path {
|
||||
fmt.Println("GetRelationPath", i, p)
|
||||
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)
|
||||
}
|
||||
|
||||
// BehaviorWarning records a single misbehavior observed by a trusted service.
|
||||
type BehaviorWarning struct {
|
||||
At time.Time `json:"at" bson:"at"`
|
||||
ReporterApp string `json:"reporter_app" bson:"reporter_app"`
|
||||
Severity tools.BehaviorSeverity `json:"severity" bson:"severity"`
|
||||
Reason string `json:"reason" bson:"reason"`
|
||||
Evidence string `json:"evidence,omitempty" bson:"evidence,omitempty"`
|
||||
}
|
||||
|
||||
// PeerLocation holds the voluntarily disclosed geographic position of a node.
|
||||
// Granularity controls how precise the location is:
|
||||
//
|
||||
// 0 = not disclosed
|
||||
// 1 = continent (±15°)
|
||||
// 2 = country (±3°) — default
|
||||
// 3 = region (±0.5°)
|
||||
// 4 = city (±0.05°)
|
||||
//
|
||||
// The coordinates are always fuzzed by oc-discovery before publication,
|
||||
// so a granularity-2 location identifies only the rough country area.
|
||||
type PeerLocation struct {
|
||||
Latitude float64 `json:"latitude" bson:"latitude"`
|
||||
Longitude float64 `json:"longitude" bson:"longitude"`
|
||||
Granularity int `json:"granularity" bson:"granularity"`
|
||||
|
||||
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
IsNano bool `json:"is_nano" bson:"is_nano"`
|
||||
|
||||
PeerPerms []PeerPerm `json:"peer_perms" bson:"peer_perms"`
|
||||
|
||||
RelationLastChangeDate time.Time `json:"relation_last_change_date" bson:"relation_last_change_date"`
|
||||
RelationLastChangeUser string `json:"relation_last_change_user" bson:"relation_last_change_user"`
|
||||
|
||||
Verify bool `json:"verify" bson:"verify"`
|
||||
OrganizationID string `json:"organization_id" bson:"organization_id"`
|
||||
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
|
||||
|
||||
// Location is the voluntarily disclosed (and fuzzed) geographic position.
|
||||
Location *PeerLocation `json:"location,omitempty" bson:"location,omitempty"`
|
||||
|
||||
// Trust scoring — maintained by oc-discovery from PEER_BEHAVIOR_EVENT reports.
|
||||
TrustScore float64 `json:"trust_score" bson:"trust_score" default:"100"`
|
||||
BlacklistReason string `json:"blacklist_reason,omitempty" bson:"blacklist_reason,omitempty"`
|
||||
BehaviorWarnings []BehaviorWarning `json:"behavior_warnings,omitempty" bson:"behavior_warnings,omitempty"`
|
||||
|
||||
// MasterID is the libp2p PeerID of this peer's MASTER node.
|
||||
// Set by a NANO in its own signed PeerRecord so intermediaries cannot forge it.
|
||||
// When oc-discovery fails to reach a NANO, it routes the booking to MasterID instead.
|
||||
MasterID string `json:"master_id,omitempty" bson:"master_id,omitempty"`
|
||||
|
||||
// Volatile connectivity state — never persisted to DB (bson:"-").
|
||||
// Set in-memory by oc-peer when it receives a PEER_OBSERVE_RESPONSE_EVENT.
|
||||
// Considered offline when LastHeartbeat is older than 60 s (30 s interval + 30 s grace).
|
||||
Online bool `json:"online" bson:"-"`
|
||||
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty" bson:"-"`
|
||||
}
|
||||
|
||||
func (ao *Peer) VerifyAuth(request *tools.APIRequest) bool {
|
||||
func (ri *Peer) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "peer":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.PEER)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func (ao *Peer) VerifyAuth(callName string, request *tools.APIRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BlacklistThreshold is the trust score below which a peer is auto-blacklisted.
|
||||
const BlacklistThreshold = 20.0
|
||||
|
||||
// ApplyBehaviorReport records a misbehavior, deducts the trust penalty, and
|
||||
// returns true when the trust score has fallen below BlacklistThreshold so the
|
||||
// caller can trigger the relation change.
|
||||
func (p *Peer) ApplyBehaviorReport(r tools.PeerBehaviorReport) (shouldBlacklist bool) {
|
||||
p.BehaviorWarnings = append(p.BehaviorWarnings, BehaviorWarning{
|
||||
At: r.At,
|
||||
ReporterApp: r.ReporterApp,
|
||||
Severity: r.Severity,
|
||||
Reason: r.Reason,
|
||||
Evidence: r.Evidence,
|
||||
})
|
||||
if p.TrustScore == 0 {
|
||||
p.TrustScore = 100 // initialise if never set
|
||||
}
|
||||
p.TrustScore -= r.Severity.Penalty()
|
||||
if p.TrustScore < 0 {
|
||||
p.TrustScore = 0
|
||||
}
|
||||
if p.TrustScore <= BlacklistThreshold {
|
||||
p.BlacklistReason = r.Reason
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResetTrust clears all behavior history and resets the trust score to 100.
|
||||
// Must be called when a peer relation is manually set to NONE or PARTNER.
|
||||
func (p *Peer) ResetTrust() {
|
||||
p.TrustScore = 100
|
||||
p.BlacklistReason = ""
|
||||
p.BehaviorWarnings = nil
|
||||
}
|
||||
|
||||
// AddExecution adds an execution to the list of failed executions
|
||||
func (ao *Peer) AddExecution(exec PeerExecution) {
|
||||
found := false
|
||||
@@ -66,18 +207,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
|
||||
}
|
||||
@@ -85,7 +216,3 @@ func (d *Peer) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
data := NewAccessor(request) // Create a new instance of the accessor
|
||||
return data
|
||||
}
|
||||
|
||||
func (r *Peer) CanDelete() bool {
|
||||
return false // only draft order can be deleted
|
||||
}
|
||||
|
||||
+31
-54
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
@@ -29,91 +28,69 @@ type PeerCache struct {
|
||||
}
|
||||
|
||||
// urlFormat formats the URL of the peer with the data type API function
|
||||
func (p *PeerCache) urlFormat(url 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(url, "localhost") {
|
||||
localhost = "localhost"
|
||||
}
|
||||
if strings.Contains(url, "127.0.0.1") {
|
||||
localhost = "127.0.0.1"
|
||||
}
|
||||
if localhost != "" {
|
||||
r := regexp.MustCompile("(" + localhost + ":[0-9]+)")
|
||||
t := r.FindString(url)
|
||||
if t != "" {
|
||||
url = strings.Replace(url, t, dt.API()+":8080/oc", -1)
|
||||
} else {
|
||||
url = strings.ReplaceAll(url, localhost, dt.API()+":8080/oc")
|
||||
}
|
||||
} else {
|
||||
url = dt.API() + "/" + url
|
||||
}
|
||||
return url
|
||||
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
|
||||
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
|
||||
fmt.Println("URL peer : ", url)
|
||||
fmt.Println("State : ", state)
|
||||
fmt.Println("Services : ", services)
|
||||
access.UpdateOne(res.Serialize(res), peerID) // Update the peer in the db
|
||||
return res.(*Peer), state != tools.DEAD && services[appName] == 0 // Return the peer and its status
|
||||
}
|
||||
|
||||
// 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("no path found")
|
||||
return map[string]interface{}{}, errors.New("Requested method " + method.String() + " not declared in HTTPCaller")
|
||||
}
|
||||
meth := methods[method] // Get the method url to execute
|
||||
meth = strings.ReplaceAll(meth, ":id", dataID) // Replace the id in the url in case of a DELETE / UPDATE method (it's a standard naming in OC)
|
||||
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)+meth, dt),
|
||||
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 not reachable")
|
||||
NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db
|
||||
return map[string]interface{}{}, errors.New("peer is " + peerID + " not reachable")
|
||||
} else {
|
||||
if mypeer == nil {
|
||||
return nil, errors.New("peer 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)+meth, dt) // 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)
|
||||
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.Serialize(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)
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -125,16 +102,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
|
||||
}
|
||||
|
||||
@@ -10,80 +10,68 @@ 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
|
||||
}
|
||||
|
||||
/*
|
||||
TODO : organization_ID est un peer_ID duquel on se revendique faire parti.
|
||||
Ca implique une clé d'organisation + une demande d'intégration.
|
||||
|
||||
Slave-Master IOT
|
||||
*/
|
||||
func (dca *peerMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
pp, _ := utils.GetMySelf(NewAccessor(&tools.APIRequest{Admin: true}))
|
||||
if data != nil {
|
||||
d := data.(*Peer)
|
||||
if pp != nil && d.OrganizationID != "" && d.OrganizationID == pp.(*Peer).OrganizationID {
|
||||
d.Relation = PARTNER // defaulting on partner if same organization.
|
||||
}
|
||||
}
|
||||
return utils.GenericStoreOne(data, dca)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 +81,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}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Regular → Executable
+94
-43
@@ -2,14 +2,17 @@ package resources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"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/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -23,47 +26,63 @@ type ComputeResource struct {
|
||||
}
|
||||
|
||||
func (d *ComputeResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*ComputeResource](tools.COMPUTE_RESOURCE, request, func() utils.DBObject { return &ComputeResource{} })
|
||||
return NewAccessor[*ComputeResource](tools.COMPUTE_RESOURCE, request)
|
||||
}
|
||||
|
||||
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 := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
|
||||
priced := p.(*PricedResource)
|
||||
p, err := ConvertToPricedResource[*ComputeResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priced := p.(*PricedResource[*ComputeResourcePricingProfile])
|
||||
return &PricedComputeResource{
|
||||
PricedResource: *priced,
|
||||
}
|
||||
}
|
||||
|
||||
type ComputeNode struct {
|
||||
Name string `json:"name,omitempty" bson:"name,omitempty"`
|
||||
Quantity int64 `json:"quantity" bson:"quantity" default:"1"`
|
||||
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"` // RAM is the RAM
|
||||
CPUs map[string]int64 `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
|
||||
GPUs map[string]int64 `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ComputeResourceInstance struct {
|
||||
ResourceInstance[*ComputeResourcePartnership]
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"`
|
||||
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 []*ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
|
||||
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"`
|
||||
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"`
|
||||
Nodes []*live.ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
|
||||
// AvailableStorages lists storage capabilities activatable on this compute unit (e.g. Minio, local volumes).
|
||||
// These are shallow StorageResource entries — not independent catalog items — but carry full pricing structure.
|
||||
AvailableStorages []*StorageResource `json:"available_storages,omitempty" bson:"available_storages,omitempty"`
|
||||
}
|
||||
|
||||
// IsPeerless is always false for compute instances: a compute resource is
|
||||
// infrastructure owned by a peer and can never be declared peerless.
|
||||
func (ri *ComputeResourceInstance) IsPeerless() bool { return false }
|
||||
|
||||
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 +95,20 @@ 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 {
|
||||
fmt.Println("Buying", p.Pricing.BuyingStrategy)
|
||||
return p.Pricing.BuyingStrategy == pricing.PERMANENT
|
||||
}
|
||||
|
||||
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,17 +117,20 @@ 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")
|
||||
}
|
||||
pp := float64(0)
|
||||
model := params[1]
|
||||
model := ""
|
||||
if len(params) > 1 {
|
||||
model = params[1]
|
||||
}
|
||||
if strings.Contains(params[0], "cpus") && len(params) > 1 {
|
||||
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 +141,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 +151,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
|
||||
}
|
||||
@@ -127,44 +161,61 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
|
||||
}
|
||||
|
||||
type PricedComputeResource struct {
|
||||
PricedResource
|
||||
PricedResource[*ComputeResourcePricingProfile]
|
||||
|
||||
CPUsLocated map[string]float64 `json:"cpus_in_use" bson:"cpus_in_use"` // CPUsInUse is the list of CPUs in use
|
||||
GPUsLocated map[string]float64 `json:"gpus_in_use" bson:"gpus_in_use"` // GPUsInUse is the list of GPUs in use
|
||||
RAMLocated float64 `json:"ram_in_use" bson:"ram_in_use"` // RAMInUse is the RAM in use
|
||||
}
|
||||
|
||||
func (r *PricedComputeResource) ensurePricing() {
|
||||
if r.SelectedPricing == nil {
|
||||
r.SelectedPricing = &ComputeResourcePricingProfile{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PricedComputeResource) IsPurchasable() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (r *PricedComputeResource) IsBooked() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (r *PricedComputeResource) GetType() tools.DataType {
|
||||
return tools.COMPUTE_RESOURCE
|
||||
}
|
||||
|
||||
func (r *PricedComputeResource) GetPrice() (float64, error) {
|
||||
func (r *PricedComputeResource) GetPriceHT() (float64, error) {
|
||||
r.ensurePricing()
|
||||
if r.BookingConfiguration == nil {
|
||||
r.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
now := time.Now()
|
||||
if r.UsageStart == nil {
|
||||
r.UsageStart = &now
|
||||
if r.BookingConfiguration.UsageStart == nil {
|
||||
r.BookingConfiguration.UsageStart = &now
|
||||
}
|
||||
if r.UsageEnd == nil {
|
||||
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
|
||||
r.UsageEnd = &add
|
||||
if r.BookingConfiguration.UsageEnd == nil {
|
||||
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
|
||||
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]
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Regular → Executable
+71
-47
@@ -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"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -30,46 +30,50 @@ type DataResource struct {
|
||||
}
|
||||
|
||||
func (d *DataResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*DataResource](tools.DATA_RESOURCE, request, func() utils.DBObject { return &DataResource{} }) // Create a new instance of the accessor
|
||||
return NewAccessor[*DataResource](tools.DATA_RESOURCE, request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
func (r *DataResource) GetType() string {
|
||||
return tools.DATA_RESOURCE.String()
|
||||
}
|
||||
|
||||
func (abs *DataResource) ConvertToPricedResource(
|
||||
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
|
||||
func (ri *DataResource) StoreDraftDefault() {
|
||||
ri.AbstractObject.StoreDraftDefault()
|
||||
ri.Env = append(ri.Env, models.Param{
|
||||
Attr: "source",
|
||||
Value: "[resource]instance.source",
|
||||
Readonly: true,
|
||||
})
|
||||
}
|
||||
|
||||
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 := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
|
||||
priced := p.(*PricedResource)
|
||||
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priced := p.(*PricedResource[*DataResourcePricingProfile])
|
||||
return &PricedDataResource{
|
||||
PricedResource: *priced,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DataInstance struct {
|
||||
ResourceInstance[*DataResourcePartnership]
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
|
||||
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
|
||||
}
|
||||
|
||||
func (ri *DataInstance) StoreDraftDefault() {
|
||||
found := false
|
||||
for _, p := range ri.ResourceInstance.Env {
|
||||
if p.Attr == "source" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
|
||||
return &DataInstance{
|
||||
ResourceInstance: ResourceInstance[*DataResourcePartnership]{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
UUID: uuid.New().String(),
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if !found {
|
||||
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
|
||||
Attr: "source",
|
||||
Value: ri.Source,
|
||||
Readonly: true,
|
||||
})
|
||||
}
|
||||
ri.ResourceInstance.StoreDraftDefault()
|
||||
}
|
||||
|
||||
type DataResourcePartnership struct {
|
||||
@@ -82,7 +86,7 @@ type DataResourcePartnership struct {
|
||||
type DataResourcePricingStrategy int
|
||||
|
||||
const (
|
||||
PER_DOWNLOAD DataResourcePricingStrategy = iota
|
||||
PER_DOWNLOAD DataResourcePricingStrategy = iota + 7
|
||||
PER_TB_DOWNLOADED
|
||||
PER_GB_DOWNLOADED
|
||||
PER_MB_DOWNLOADED
|
||||
@@ -90,7 +94,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 +108,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,40 +141,54 @@ 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.PERMANENT
|
||||
}
|
||||
|
||||
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 {
|
||||
PricedResource
|
||||
PricedResource[*DataResourcePricingProfile]
|
||||
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
|
||||
}
|
||||
|
||||
func (r *PricedDataResource) ensurePricing() {
|
||||
if r.SelectedPricing == nil {
|
||||
r.SelectedPricing = &DataResourcePricingProfile{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PricedDataResource) IsPurchasable() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (r *PricedDataResource) IsBooked() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (r *PricedDataResource) GetType() tools.DataType {
|
||||
return tools.DATA_RESOURCE
|
||||
}
|
||||
|
||||
func (r *PricedDataResource) GetPrice() (float64, error) {
|
||||
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
|
||||
func (r *PricedDataResource) GetPriceHT() (float64, error) {
|
||||
r.ensurePricing()
|
||||
if r.BookingConfiguration == nil {
|
||||
r.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
now := time.Now()
|
||||
if r.UsageStart == nil {
|
||||
r.UsageStart = &now
|
||||
if r.BookingConfiguration.UsageStart == nil {
|
||||
r.BookingConfiguration.UsageStart = &now
|
||||
}
|
||||
if r.UsageEnd == nil {
|
||||
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
|
||||
r.UsageEnd = &add
|
||||
if r.BookingConfiguration.UsageEnd == nil {
|
||||
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
|
||||
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]
|
||||
}
|
||||
pricing := *r.SelectedPricing
|
||||
pricing := r.SelectedPricing
|
||||
var err error
|
||||
amountOfData := float64(1)
|
||||
if pricing.GetOverrideStrategyValue() >= 0 {
|
||||
@@ -175,5 +197,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)
|
||||
}
|
||||
|
||||
Executable
+298
@@ -0,0 +1,298 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"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"
|
||||
)
|
||||
|
||||
/*
|
||||
* DynamicResource is a struct that represents a data resource
|
||||
* it defines the resource data
|
||||
*/
|
||||
type DynamicResource struct {
|
||||
AbstractResource
|
||||
Type tools.DataType `bson:"type,omitempty" json:"type,omitempty"`
|
||||
Filters map[string]interface{} `bson:"filters,omitempty" json:"filters,omitempty"`
|
||||
SortRules map[string]string `bson:"rules,omitempty" json:"rules,omitempty"`
|
||||
|
||||
PeerIds map[int]string `bson:"peer_ids,omitempty" json:"peer_ids,omitempty"`
|
||||
ResourceIds map[int]string `bson:"resource_ids,omitempty" json:"resource_ids,omitempty"`
|
||||
|
||||
SelectedIndex int `bson:"selected_index,omitempty" json:"selected_index,omitempty"`
|
||||
SelectedPartnershipIndex *int `bson:"selected_partnership_index,omitempty" json:"selected_partnership_index,omitempty"`
|
||||
|
||||
SelectedBuyingStrategy int `bson:"selected_buying_strategy" json:"selected_buying_strategy,omitempty"`
|
||||
SelectedPricingStrategy int `bson:"selected_pricing_strategy" json:"selected_pricing_strategy,omitempty"`
|
||||
|
||||
Instances []ResourceInstanceITF `bson:"instances,omitempty" json:"instances,omitempty"`
|
||||
WatchedDynamicResource []string `bson:"watched_dynamic_resource,omitempty" json:"watched_dynamic_resource,omitempty"`
|
||||
}
|
||||
|
||||
func (d *DynamicResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DynamicResource) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
|
||||
d.Instances = []ResourceInstanceITF{}
|
||||
for k, v := range map[tools.DataType]ResourceInterface{
|
||||
tools.COMPUTE_RESOURCE: &ComputeResource{},
|
||||
tools.DATA_RESOURCE: &DataResource{},
|
||||
tools.STORAGE_RESOURCE: &StorageResource{},
|
||||
tools.PROCESSING_RESOURCE: &ProcessingResource{},
|
||||
tools.WORKFLOW_RESOURCE: &WorkflowResource{}} {
|
||||
if d.Type != k {
|
||||
continue
|
||||
}
|
||||
access := NewAccessor[*DynamicResource](k, request)
|
||||
a, _, _ := access.Search(dbs.FiltersFromFlatMap(d.Filters, v), "", false, 0, 100000)
|
||||
d.PeerIds = map[int]string{}
|
||||
d.ResourceIds = map[int]string{}
|
||||
for _, res := range a {
|
||||
for _, i := range res.(ResourceInterface).SetAllowedInstances(request, instance_id...) {
|
||||
d.PeerIds[len(d.Instances)] = res.GetCreatorID()
|
||||
d.ResourceIds[len(d.Instances)] = res.GetID()
|
||||
d.Instances = append(d.Instances, i)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
sorted := make([]ResourceInstanceITF, len(d.Instances))
|
||||
copy(sorted, d.Instances)
|
||||
slices.SortStableFunc(sorted, func(a, b ResourceInstanceITF) int {
|
||||
d.SortRules["partnerships"] = "%v not contains 2"
|
||||
return d.compareByRules(a, b, d.SortRules)
|
||||
})
|
||||
d.WatchedDynamicResource = []string{}
|
||||
return d.Instances
|
||||
}
|
||||
|
||||
func (d *DynamicResource) AddInstances(instance ResourceInstanceITF) {
|
||||
d.Instances = append(d.Instances, instance)
|
||||
}
|
||||
|
||||
func (d *DynamicResource) GetSelectedInstance(index *int) ResourceInstanceITF {
|
||||
if len(d.Instances) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i, inst := range d.Instances {
|
||||
if slices.Contains(d.WatchedDynamicResource, inst.GetID()) {
|
||||
continue
|
||||
}
|
||||
d.WatchedDynamicResource = append(d.WatchedDynamicResource, inst.GetID())
|
||||
d.SelectedIndex = i
|
||||
|
||||
for i := range inst.GetPartnerships() {
|
||||
if inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy) != nil {
|
||||
d.SelectedPartnershipIndex = &i
|
||||
break
|
||||
}
|
||||
}
|
||||
if d.SelectedPartnershipIndex == nil {
|
||||
continue
|
||||
}
|
||||
return inst
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// compareByRules orders instances so those satisfying more sort rules come first.
|
||||
// When both satisfy a rule, the one with the lower first-attribute value wins (ASC strict).
|
||||
// Key format: "attrA" for single-%s rules, "attrA,attrB" for two-%s rules.
|
||||
func (ri *DynamicResource) compareByRules(a, b ResourceInstanceITF, rules map[string]string) int {
|
||||
ma := a.Serialize(a)
|
||||
mb := b.Serialize(b)
|
||||
for attrs, rule := range rules {
|
||||
attrPaths := strings.Split(attrs, ",")
|
||||
|
||||
aOk, aFirst := ri.ruleMatchesAny(rule, attrPaths, ma)
|
||||
bOk, bFirst := ri.ruleMatchesAny(rule, attrPaths, mb)
|
||||
|
||||
if aOk && !bOk {
|
||||
return -1
|
||||
}
|
||||
if !aOk && bOk {
|
||||
return 1
|
||||
}
|
||||
if aOk && bOk {
|
||||
if aFirst < bFirst {
|
||||
return -1
|
||||
}
|
||||
if aFirst > bFirst {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// ruleMatchesAny checks if any value (or combination for 2-%s rules) satisfies rule.
|
||||
// Arrays at any path level are iterated. Returns (matched, firstMatchingValue).
|
||||
func (ri *DynamicResource) ruleMatchesAny(rule string, attrPaths []string, m map[string]interface{}) (bool, string) {
|
||||
placeholders := strings.Count(rule, "%s")
|
||||
if placeholders == 0 {
|
||||
return false, ""
|
||||
}
|
||||
valsA := ri.getVals(strings.Split(strings.TrimSpace(attrPaths[0]), "."), m)
|
||||
if placeholders == 1 {
|
||||
for _, v := range valsA {
|
||||
if ri.byRules(rule, v) {
|
||||
return true, fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
if len(attrPaths) < 2 {
|
||||
return false, ""
|
||||
}
|
||||
valsB := ri.getVals(strings.Split(strings.TrimSpace(attrPaths[1]), "."), m)
|
||||
for _, a := range valsA {
|
||||
for _, b := range valsB {
|
||||
if ri.byRules(rule, a, b) {
|
||||
return true, fmt.Sprintf("%v", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// getVals navigates attrs into m, collecting all leaf values.
|
||||
// At each level it detects whether the value is a dict (map) or an array and acts accordingly:
|
||||
// - array of maps → recurse into each element with the remaining path
|
||||
// - array of scalars (leaf) → collect all as strings
|
||||
// - map → recurse with the remaining path
|
||||
func (ri *DynamicResource) getVals(attrs []string, m map[string]interface{}) []interface{} {
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
attr := attrs[0]
|
||||
if attr == "" || m[attr] == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(m[attr])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Leaf level: detect array vs scalar.
|
||||
if len(attrs) == 1 {
|
||||
var arr []interface{}
|
||||
if err := json.Unmarshal(b, &arr); err == nil {
|
||||
results := []interface{}{}
|
||||
for _, v := range arr {
|
||||
results = append(results, fmt.Sprintf("%v", v))
|
||||
}
|
||||
return results
|
||||
}
|
||||
return []interface{}{m[attr]}
|
||||
}
|
||||
// Intermediate level: detect array of maps vs single map.
|
||||
var arrMaps []map[string]interface{}
|
||||
if err := json.Unmarshal(b, &arrMaps); err == nil {
|
||||
results := []interface{}{}
|
||||
for _, item := range arrMaps {
|
||||
results = append(results, ri.getVals(attrs[1:], item)...)
|
||||
}
|
||||
return results
|
||||
}
|
||||
nm := map[string]interface{}{}
|
||||
if err := json.Unmarshal(b, &nm); err != nil {
|
||||
return nil
|
||||
}
|
||||
return ri.getVals(attrs[1:], nm)
|
||||
}
|
||||
|
||||
func (ri *DynamicResource) byRules(rule string, vals ...interface{}) bool {
|
||||
if len(vals) == 0 {
|
||||
return false
|
||||
}
|
||||
formatted := fmt.Sprintf(rule, vals...)
|
||||
// hm hm
|
||||
switch {
|
||||
case strings.Contains(rule, "not contains"):
|
||||
a := strings.Split(formatted, " not contains ")
|
||||
if reflect.TypeOf(vals[0]).Kind() == reflect.Map {
|
||||
return vals[0].(map[string]interface{})[fmt.Sprintf("%v", a[1])] != nil
|
||||
}
|
||||
return strings.Contains(a[0], a[1])
|
||||
case strings.Contains(rule, "contains"):
|
||||
a := strings.Split(formatted, " contains ")
|
||||
if reflect.TypeOf(vals[0]).Kind() == reflect.Map {
|
||||
return vals[0].(map[string]interface{})[fmt.Sprintf("%v", a[1])] != nil
|
||||
}
|
||||
return strings.Contains(a[0], a[1])
|
||||
case strings.Contains(rule, "<="):
|
||||
a := strings.Split(formatted, " <= ")
|
||||
return len(a) > 1 && a[0] <= a[1]
|
||||
case strings.Contains(rule, ">="):
|
||||
a := strings.Split(formatted, " >= ")
|
||||
return len(a) > 1 && a[0] >= a[1]
|
||||
case strings.Contains(rule, "<>"), strings.Contains(rule, "not like"):
|
||||
if strings.Contains(rule, "<>") {
|
||||
a := strings.Split(formatted, " <> ")
|
||||
return len(a) > 1 && !strings.Contains(a[0], a[1]) && !strings.Contains(a[1], a[0])
|
||||
}
|
||||
a := strings.Split(formatted, " not like ")
|
||||
return len(a) > 1 && !strings.Contains(a[0], a[1]) && !strings.Contains(a[1], a[0])
|
||||
case strings.Contains(rule, "<"):
|
||||
a := strings.Split(formatted, " < ")
|
||||
return len(a) > 1 && a[0] < a[1]
|
||||
case strings.Contains(rule, ">"):
|
||||
a := strings.Split(formatted, " > ")
|
||||
return len(a) > 1 && a[0] > a[1]
|
||||
case strings.Contains(rule, "=="):
|
||||
a := strings.Split(formatted, " == ")
|
||||
return len(a) > 1 && a[0] == a[1]
|
||||
case strings.Contains(rule, "!="):
|
||||
a := strings.Split(formatted, " != ")
|
||||
return len(a) > 1 && a[0] != a[1]
|
||||
case strings.Contains(rule, "like"):
|
||||
a := strings.Split(formatted, " like ")
|
||||
return len(a) > 1 && (strings.Contains(a[0], a[1]) || strings.Contains(a[1], a[0]))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *DynamicResource) GetType() string {
|
||||
return tools.DYNAMIC_RESOURCE.String()
|
||||
}
|
||||
|
||||
func (abs *DynamicResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
|
||||
var p pricing.PricedItemITF
|
||||
var err error
|
||||
for _, v := range []tools.DataType{
|
||||
tools.COMPUTE_RESOURCE,
|
||||
tools.DATA_RESOURCE,
|
||||
tools.STORAGE_RESOURCE,
|
||||
tools.PROCESSING_RESOURCE,
|
||||
tools.WORKFLOW_RESOURCE,
|
||||
} {
|
||||
switch v {
|
||||
case tools.COMPUTE_RESOURCE:
|
||||
if p, err = ConvertToPricedResource[*ComputeResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
|
||||
return p.(*PricedResource[*ProcessingResourcePricingProfile]), nil
|
||||
}
|
||||
case tools.DATA_RESOURCE:
|
||||
if p, err = ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
|
||||
return p.(*PricedResource[*DataResourcePricingProfile]), nil
|
||||
}
|
||||
case tools.STORAGE_RESOURCE:
|
||||
if p, err = ConvertToPricedResource[*StorageResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
|
||||
return p.(*PricedResource[*StorageResourcePricingProfile]), nil
|
||||
}
|
||||
case tools.PROCESSING_RESOURCE:
|
||||
if p, err = ConvertToPricedResource[*ProcessingResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
|
||||
return p.(*PricedResource[*ProcessingResourcePricingProfile]), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("can't convert priced resource")
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package resources
|
||||
|
||||
// exploitation_authorization.go — Autorisation d'Exploitation (AE)
|
||||
//
|
||||
// AEs are embedded inside AbstractResource (field ExploitationAuthorizations).
|
||||
// They are NOT a separate MongoDB collection — each resource document carries
|
||||
// its own AEs, just like it carries its Instances.
|
||||
//
|
||||
// # Visibility filtering
|
||||
//
|
||||
// When a resource is returned to a consumer peer the AE list is filtered:
|
||||
// - AllowedPeerIDs empty → public AE, visible to all peers.
|
||||
// - AllowedPeerIDs non-empty, contains requester → visible to that peer.
|
||||
// - AllowedPeerIDs non-empty, doesn't contain requester → stripped from response.
|
||||
//
|
||||
// The resource owner always sees all of their own AEs unfiltered.
|
||||
//
|
||||
// # Enforcement
|
||||
//
|
||||
// oc-schedulerd's validateWorkflowIntegrity calls CheckWorkflowAE (defined in
|
||||
// its own package to avoid circular imports) before launching any execution.
|
||||
// Violations emit PEER_BEHAVIOR_EVENT(BehaviorFraud) against the consumer peer
|
||||
// and cause the execution to be rejected.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
// CouplingConstraint defines which resources must or must not co-exist in the
|
||||
// same workflow when the protected resource is included.
|
||||
type CouplingConstraint struct {
|
||||
// RequiredResourceIDs — ALL of these resource UUIDs must appear in the
|
||||
// workflow alongside the protected resource.
|
||||
RequiredResourceIDs []string `json:"required_resource_ids,omitempty" bson:"required_resource_ids,omitempty"`
|
||||
// ForbiddenResourceIDs — NONE of these resource UUIDs may appear in the
|
||||
// workflow alongside the protected resource.
|
||||
ForbiddenResourceIDs []string `json:"forbidden_resource_ids,omitempty" bson:"forbidden_resource_ids,omitempty"`
|
||||
}
|
||||
|
||||
// ExploitationAuthorization (AE) is embedded in a resource and restricts how
|
||||
// the resource may be used by other consumer peers.
|
||||
//
|
||||
// It is stored as part of the resource document (bson embedded), not as a
|
||||
// separate collection. Create/update it by PATCHing the parent resource.
|
||||
type ExploitationAuthorization struct {
|
||||
// ID is a client-assigned UUID so individual AEs can be referenced.
|
||||
ID string `json:"id" bson:"id"`
|
||||
// Name is a human-readable label shown in the catalog detail view.
|
||||
Name string `json:"name,omitempty" bson:"name,omitempty"`
|
||||
|
||||
// AllowedPeerIDs restricts which consumer peers may use the resource.
|
||||
// An empty list means any peer is allowed.
|
||||
AllowedPeerIDs []string `json:"allowed_peer_ids,omitempty" bson:"allowed_peer_ids,omitempty"`
|
||||
|
||||
// AllowedWorkflowIDs restricts which workflow IDs may include the resource.
|
||||
// An empty list means any workflow is allowed.
|
||||
AllowedWorkflowIDs []string `json:"allowed_workflow_ids,omitempty" bson:"allowed_workflow_ids,omitempty"`
|
||||
|
||||
// Coupling describes positive (required) and negative (forbidden) coupling.
|
||||
// Nil means no coupling constraint.
|
||||
Coupling *CouplingConstraint `json:"coupling,omitempty" bson:"coupling,omitempty"`
|
||||
|
||||
// ValidFrom / ValidUntil define the active window.
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
|
||||
|
||||
// IsRevoked allows instant revocation without deleting the AE from the resource.
|
||||
IsRevoked bool `json:"is_revoked" bson:"is_revoked"`
|
||||
}
|
||||
|
||||
// IsVisibleTo returns true when this AE should be included in the response to
|
||||
// peerID. The resource owner (creatorID) always sees all AEs.
|
||||
func (ae *ExploitationAuthorization) IsVisibleTo(peerID, creatorID string) bool {
|
||||
if peerID == creatorID {
|
||||
return true // owner sees everything
|
||||
}
|
||||
return len(ae.AllowedPeerIDs) == 0 || slices.Contains(ae.AllowedPeerIDs, peerID)
|
||||
}
|
||||
|
||||
// CheckAE evaluates this AE against the execution context and returns any
|
||||
// violations found. workflowResourceIDs is the set of all resource UUIDs in
|
||||
// the workflow; resourceID is the UUID of the resource this AE belongs to.
|
||||
func (ae *ExploitationAuthorization) CheckAE(
|
||||
resourceID, workflowID, consumerPeerID string,
|
||||
workflowResourceIDs map[string]struct{},
|
||||
now time.Time,
|
||||
) []AEViolation {
|
||||
var vs []AEViolation
|
||||
add := func(t AEViolationType, msg string) {
|
||||
vs = append(vs, AEViolation{AEID: ae.ID, ResourceID: resourceID, Type: t, Message: msg})
|
||||
}
|
||||
|
||||
if ae.IsRevoked {
|
||||
add(AEViolationRevoked, fmt.Sprintf("AE %s for resource %s is revoked", ae.ID, resourceID))
|
||||
return vs
|
||||
}
|
||||
if ae.ValidUntil != nil && now.After(*ae.ValidUntil) {
|
||||
add(AEViolationExpired, fmt.Sprintf("AE %s for resource %s expired at %s",
|
||||
ae.ID, resourceID, ae.ValidUntil.Format(time.RFC3339)))
|
||||
return vs
|
||||
}
|
||||
if ae.ValidFrom != nil && now.Before(*ae.ValidFrom) {
|
||||
add(AEViolationNotYetValid, fmt.Sprintf("AE %s for resource %s not valid until %s",
|
||||
ae.ID, resourceID, ae.ValidFrom.Format(time.RFC3339)))
|
||||
return vs
|
||||
}
|
||||
if consumerPeerID != "" && len(ae.AllowedPeerIDs) > 0 {
|
||||
if !slices.Contains(ae.AllowedPeerIDs, consumerPeerID) {
|
||||
add(AEViolationPeerNotAllowed, fmt.Sprintf(
|
||||
"peer %s not allowed to use resource %s (AE %s)", consumerPeerID, resourceID, ae.ID))
|
||||
}
|
||||
}
|
||||
if workflowID != "" && len(ae.AllowedWorkflowIDs) > 0 {
|
||||
if !slices.Contains(ae.AllowedWorkflowIDs, workflowID) {
|
||||
add(AEViolationWorkflowNotAllow, fmt.Sprintf(
|
||||
"workflow %s not in allowed-workflow list for resource %s (AE %s)", workflowID, resourceID, ae.ID))
|
||||
}
|
||||
}
|
||||
if ae.Coupling != nil {
|
||||
for _, req := range ae.Coupling.RequiredResourceIDs {
|
||||
if _, ok := workflowResourceIDs[req]; !ok {
|
||||
add(AEViolationCouplingRequired, fmt.Sprintf(
|
||||
"resource %s requires %s to be present (AE %s)", resourceID, req, ae.ID))
|
||||
}
|
||||
}
|
||||
for _, forb := range ae.Coupling.ForbiddenResourceIDs {
|
||||
if _, ok := workflowResourceIDs[forb]; ok {
|
||||
add(AEViolationCouplingForbid, fmt.Sprintf(
|
||||
"resource %s forbids co-use with %s (AE %s)", resourceID, forb, ae.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
// ── Violation types ───────────────────────────────────────────────────────────
|
||||
|
||||
type AEViolationType string
|
||||
|
||||
const (
|
||||
AEViolationRevoked AEViolationType = "ae_revoked"
|
||||
AEViolationExpired AEViolationType = "ae_expired"
|
||||
AEViolationNotYetValid AEViolationType = "ae_not_yet_valid"
|
||||
AEViolationPeerNotAllowed AEViolationType = "ae_peer_not_allowed"
|
||||
AEViolationWorkflowNotAllow AEViolationType = "ae_workflow_not_allowed"
|
||||
AEViolationCouplingRequired AEViolationType = "ae_coupling_required"
|
||||
AEViolationCouplingForbid AEViolationType = "ae_coupling_forbidden"
|
||||
)
|
||||
|
||||
// AEViolation describes a single constraint that was not satisfied.
|
||||
type AEViolation struct {
|
||||
AEID string
|
||||
ResourceID string
|
||||
Type AEViolationType
|
||||
Message string
|
||||
}
|
||||
|
||||
// ── NATS emit helper (uses tools only — no oclib circular import) ─────────────
|
||||
|
||||
// EmitAEBehaviorReport emits a PEER_BEHAVIOR_EVENT(BehaviorFraud) for each
|
||||
// unique AE violation. Call this before rejecting the execution.
|
||||
func EmitAEBehaviorReport(consumerPeerID string, violations []AEViolation) {
|
||||
if consumerPeerID == "" || len(violations) == 0 {
|
||||
return
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, v := range violations {
|
||||
key := v.AEID + ":" + v.ResourceID
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
report := tools.PeerBehaviorReport{
|
||||
ReporterApp: "oc-scheduler",
|
||||
TargetPeerID: consumerPeerID,
|
||||
Severity: tools.BehaviorFraud,
|
||||
Reason: fmt.Sprintf("AE violation (%s): %s", v.Type, v.Message),
|
||||
Evidence: v.AEID,
|
||||
At: time.Now().UTC(),
|
||||
}
|
||||
if b, err := json.Marshal(report); err == nil {
|
||||
tools.NewNATSCaller().SetNATSPub(tools.PEER_BEHAVIOR_EVENT, tools.NATSResponse{
|
||||
FromApp: "oc-scheduler",
|
||||
Method: int(tools.PEER_BEHAVIOR_EVENT),
|
||||
Payload: b,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OriginType qualifies where a resource instance comes from.
|
||||
type OriginType int
|
||||
|
||||
const (
|
||||
// OriginPeer: instance offered by a known network peer (default).
|
||||
OriginPeer OriginType = iota
|
||||
// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
|
||||
// No peer confirmation is needed; access is unrestricted.
|
||||
OriginPublic
|
||||
// OriginSelf: self-hosted instance with no third-party peer.
|
||||
OriginSelf
|
||||
)
|
||||
|
||||
// OriginMeta carries provenance information for a resource instance.
|
||||
type OriginMeta struct {
|
||||
Type OriginType `json:"origin_type" bson:"origin_type"`
|
||||
Ref string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
|
||||
Verified bool `json:"origin_verified" bson:"origin_verified"` // manually vetted by an OC admin
|
||||
}
|
||||
|
||||
// IsPeerless MUST NOT be used for authorization decisions.
|
||||
// Use ResourceInstance.IsPeerless() instead, which derives the property
|
||||
// from structural invariants rather than this self-declared field.
|
||||
//
|
||||
// This method is kept only for display/logging purposes.
|
||||
func (o OriginMeta) DeclaredPeerless() bool {
|
||||
return o.Type != OriginPeer
|
||||
}
|
||||
Regular → Executable
+28
-6
@@ -1,34 +1,56 @@
|
||||
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/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"
|
||||
)
|
||||
|
||||
type PricedResourceITF interface {
|
||||
pricing.PricedItemITF
|
||||
}
|
||||
|
||||
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)
|
||||
VerifyBuy()
|
||||
SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF
|
||||
AddInstances(instance ResourceInstanceITF)
|
||||
GetSelectedInstance(index *int) ResourceInstanceITF
|
||||
StoreDraftDefault()
|
||||
|
||||
GetEnv() []models.Param
|
||||
GetInputs() []models.Param
|
||||
GetOutputs() []models.Param
|
||||
}
|
||||
|
||||
type ResourceInstanceITF interface {
|
||||
utils.DBObject
|
||||
GetID() string
|
||||
GetName() string
|
||||
StoreDraftDefault()
|
||||
ClearEnv()
|
||||
GetOrigin() OriginMeta
|
||||
IsPeerless() bool
|
||||
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()
|
||||
GetPartnerships() []ResourcePartnerITF
|
||||
GetAverageDurationS() float64
|
||||
UpdateAverageDuration(actualS float64)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Regular → Executable
+21
@@ -11,12 +11,19 @@ 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"`
|
||||
Services []string `bson:"services,omitempty" json:"services,omitempty"`
|
||||
Dynamics []string `bson:"dynamics,omitempty" json:"dynamics,omitempty"`
|
||||
|
||||
// DynamicResources are stored inline — no DB collection, resolved at runtime via SetAllowedInstances.
|
||||
DynamicResources []*DynamicResource `bson:"-" json:"dynamic_resources,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"`
|
||||
ServiceResources []*ServiceResource `bson:"-" json:"service_resources,omitempty"`
|
||||
}
|
||||
|
||||
func (r *ResourceSet) Clear() {
|
||||
@@ -25,6 +32,8 @@ func (r *ResourceSet) Clear() {
|
||||
r.ProcessingResources = nil
|
||||
r.ComputeResources = nil
|
||||
r.WorkflowResources = nil
|
||||
r.ServiceResources = nil
|
||||
r.DynamicResources = nil
|
||||
}
|
||||
|
||||
func (r *ResourceSet) Fill(request *tools.APIRequest) {
|
||||
@@ -35,6 +44,8 @@ func (r *ResourceSet) Fill(request *tools.APIRequest) {
|
||||
(&StorageResource{}): r.Storages,
|
||||
(&ProcessingResource{}): r.Processings,
|
||||
(&WorkflowResource{}): r.Workflows,
|
||||
(&ServiceResource{}): r.Services,
|
||||
(&DynamicResource{}): r.Dynamics,
|
||||
} {
|
||||
for _, id := range v {
|
||||
d, _, e := k.GetAccessor(request).LoadOne(id)
|
||||
@@ -50,10 +61,17 @@ func (r *ResourceSet) Fill(request *tools.APIRequest) {
|
||||
r.ProcessingResources = append(r.ProcessingResources, d.(*ProcessingResource))
|
||||
case *WorkflowResource:
|
||||
r.WorkflowResources = append(r.WorkflowResources, d.(*WorkflowResource))
|
||||
case *ServiceResource:
|
||||
r.ServiceResources = append(r.ServiceResources, d.(*ServiceResource))
|
||||
case *DynamicResource:
|
||||
r.DynamicResources = append(r.DynamicResources, d.(*DynamicResource))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, d := range r.DynamicResources {
|
||||
d.SetAllowedInstances(request)
|
||||
}
|
||||
}
|
||||
|
||||
type ItemResource struct {
|
||||
@@ -62,4 +80,7 @@ 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"`
|
||||
Service *ServiceResource `bson:"service,omitempty" json:"service,omitempty"`
|
||||
Dynamic *DynamicResource `bson:"dynamic,omitempty" json:"dynamic,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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 (r *NativeTool) AddInstances(instance ResourceInstanceITF) {
|
||||
}
|
||||
|
||||
func (r *NativeTool) GetType() string {
|
||||
return tools.NATIVE_TOOL.String()
|
||||
}
|
||||
|
||||
func (d *NativeTool) ClearEnv() utils.DBObject {
|
||||
return d
|
||||
}
|
||||
|
||||
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
|
||||
// WorkflowResource has no instances, but still carries AEs that must be
|
||||
// filtered before the resource is returned to a non-owner, non-admin peer.
|
||||
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
if request != nil {
|
||||
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
return []ResourceInstanceITF{}
|
||||
}
|
||||
|
||||
func (w *NativeTool) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
|
||||
return &PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{
|
||||
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, 0, 10)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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"`
|
||||
ManualCheck bool `json:"manual_check" bson:"manual_check"`
|
||||
Input string `json:"input" bson:"input"`
|
||||
Payload string `json:"payload" bson:"payload"`
|
||||
BookingMode *booking.BookingMode `json:"booking_mode" bson:"booking_mode"`
|
||||
}
|
||||
|
||||
func (wep *WorkflowEventParams) GetBuyingStrategy() pricing.BillingStrategy {
|
||||
return pricing.BILL_ONCE
|
||||
}
|
||||
Regular → Executable
+122
-60
@@ -2,92 +2,154 @@ 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 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (abs *PricedResource) GetID() string {
|
||||
type PricedResource[T pricing.PricingProfileITF] 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"`
|
||||
SelectedPricing T `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[T]) GetQuantity() int {
|
||||
return abs.Quantity
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) AddQuantity(amount int) {
|
||||
abs.Quantity += amount
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) SelectPricing() pricing.PricingProfileITF {
|
||||
return abs.SelectedPricing
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) GetID() string {
|
||||
return abs.ResourceID
|
||||
}
|
||||
|
||||
func (abs *PricedResource) GetType() tools.DataType {
|
||||
func (abs *PricedResource[T]) GetName() string {
|
||||
return abs.Name
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) GetInstanceID() string {
|
||||
return abs.InstanceID
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) GetType() tools.DataType {
|
||||
return abs.ResourceType
|
||||
}
|
||||
|
||||
func (abs *PricedResource) GetCreatorID() string {
|
||||
func (abs *PricedResource[T]) GetCreatorID() string {
|
||||
return abs.CreatorID
|
||||
}
|
||||
|
||||
func (abs *PricedResource) IsPurchased() bool {
|
||||
if abs.SelectedPricing == nil {
|
||||
// IsPurchasable and IsBooked fall back to false when SelectedPricing is a nil interface.
|
||||
// Concrete types (PricedComputeResource, etc.) override these and guarantee non-nil pricing.
|
||||
func (abs *PricedResource[T]) IsPurchasable() bool {
|
||||
if any(abs.SelectedPricing) == nil {
|
||||
return false
|
||||
}
|
||||
return (*abs.SelectedPricing).IsPurchased()
|
||||
return abs.SelectedPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (abs *PricedResource) GetLocationEnd() *time.Time {
|
||||
return abs.UsageEnd
|
||||
}
|
||||
|
||||
func (abs *PricedResource) GetLocationStart() *time.Time {
|
||||
return abs.UsageStart
|
||||
}
|
||||
|
||||
func (abs *PricedResource) SetLocationStart(start time.Time) {
|
||||
abs.UsageStart = &start
|
||||
}
|
||||
|
||||
func (abs *PricedResource) SetLocationEnd(end time.Time) {
|
||||
abs.UsageEnd = &end
|
||||
}
|
||||
|
||||
func (abs *PricedResource) GetExplicitDurationInS() float64 {
|
||||
if abs.ExplicitBookingDurationS == 0 {
|
||||
if abs.UsageEnd == nil && abs.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
|
||||
}
|
||||
return abs.UsageEnd.Sub(*abs.UsageStart).Seconds()
|
||||
func (abs *PricedResource[T]) IsBooked() bool {
|
||||
if any(abs.SelectedPricing) == nil {
|
||||
return false
|
||||
}
|
||||
return abs.ExplicitBookingDurationS
|
||||
return abs.SelectedPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (r *PricedResource) GetPrice() (float64, error) {
|
||||
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
|
||||
func (abs *PricedResource[T]) GetLocationEnd() *time.Time {
|
||||
if abs.BookingConfiguration == nil {
|
||||
return nil
|
||||
}
|
||||
return abs.BookingConfiguration.UsageEnd
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) GetLocationStart() *time.Time {
|
||||
if abs.BookingConfiguration == nil {
|
||||
return nil
|
||||
}
|
||||
return abs.BookingConfiguration.UsageStart
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) SetLocationStart(start time.Time) {
|
||||
if abs.BookingConfiguration == nil {
|
||||
abs.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
abs.BookingConfiguration.UsageStart = &start
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) SetLocationEnd(end time.Time) {
|
||||
if abs.BookingConfiguration == nil {
|
||||
abs.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
abs.BookingConfiguration.UsageEnd = &end
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) GetBookingMode() booking.BookingMode {
|
||||
if abs.BookingConfiguration == nil {
|
||||
return booking.WHEN_POSSIBLE
|
||||
}
|
||||
return abs.BookingConfiguration.Mode
|
||||
}
|
||||
|
||||
func (abs *PricedResource[T]) GetExplicitDurationInS() float64 {
|
||||
if abs.BookingConfiguration == nil {
|
||||
abs.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
if abs.BookingConfiguration.ExplicitBookingDurationS == 0 {
|
||||
if abs.BookingConfiguration.UsageEnd == nil && abs.BookingConfiguration.UsageStart == nil {
|
||||
return (5 * time.Minute).Seconds()
|
||||
}
|
||||
if abs.BookingConfiguration.UsageEnd == nil {
|
||||
add := abs.BookingConfiguration.UsageStart.Add(5 * time.Minute)
|
||||
abs.BookingConfiguration.UsageEnd = &add
|
||||
}
|
||||
d := abs.BookingConfiguration.UsageEnd.Sub(*abs.BookingConfiguration.UsageStart).Seconds()
|
||||
if d <= 0 {
|
||||
return (5 * time.Minute).Seconds()
|
||||
}
|
||||
return d
|
||||
}
|
||||
return abs.BookingConfiguration.ExplicitBookingDurationS
|
||||
}
|
||||
|
||||
func (r *PricedResource[T]) 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.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]
|
||||
if r.BookingConfiguration.UsageEnd == nil {
|
||||
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
|
||||
r.BookingConfiguration.UsageEnd = &add
|
||||
}
|
||||
pricing := *r.SelectedPricing
|
||||
return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd)
|
||||
if any(r.SelectedPricing) == nil {
|
||||
return 0, errors.New("pricing profile must be set for resource " + r.ResourceID)
|
||||
}
|
||||
pricing := r.SelectedPricing
|
||||
return pricing.GetPriceHT(1, 0, *r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
|
||||
}
|
||||
|
||||
Regular → Executable
+78
-26
@@ -1,6 +1,7 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
@@ -8,6 +9,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,30 +28,66 @@ type ProcessingUsage struct {
|
||||
*/
|
||||
type ProcessingResource struct {
|
||||
AbstractInstanciatedResource[*ProcessingInstance]
|
||||
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
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
|
||||
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"`
|
||||
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
|
||||
License string `json:"license,omitempty" bson:"license,omitempty"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
// IsService marks a long-running processing that acts as a persistent service.
|
||||
// Such processings do not require a Compute booking (they manage their own lifecycle).
|
||||
IsService bool `json:"is_service" bson:"is_service" default:"false"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
// License is now in AbstractResource — kept here as alias for backward compat with existing DB docs.
|
||||
// New code should use AbstractResource.License.
|
||||
}
|
||||
|
||||
func (r *ProcessingResource) GetType() string {
|
||||
return tools.PROCESSING_RESOURCE.String()
|
||||
}
|
||||
|
||||
type ProcessingResourceAccess struct {
|
||||
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
|
||||
}
|
||||
|
||||
type ProcessingInstance struct {
|
||||
ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]
|
||||
Access *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
|
||||
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
|
||||
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
|
||||
}
|
||||
|
||||
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
|
||||
PricedResource[*ProcessingResourcePricingProfile]
|
||||
}
|
||||
|
||||
func (r *PricedProcessingResource) ensurePricing() {
|
||||
if r.SelectedPricing == nil {
|
||||
r.SelectedPricing = &ProcessingResourcePricingProfile{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PricedProcessingResource) IsPurchasable() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (r *PricedProcessingResource) IsBooked() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (r *PricedProcessingResource) GetPriceHT() (float64, error) {
|
||||
r.ensurePricing()
|
||||
return r.PricedResource.GetPriceHT()
|
||||
}
|
||||
|
||||
func (r *PricedProcessingResource) GetType() tools.DataType {
|
||||
@@ -57,30 +95,44 @@ func (r *PricedProcessingResource) GetType() tools.DataType {
|
||||
}
|
||||
|
||||
func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
|
||||
if a.ExplicitBookingDurationS == 0 {
|
||||
if a.IsService || a.UsageStart == nil {
|
||||
if a.IsService {
|
||||
return -1
|
||||
}
|
||||
return time.Duration(1 * time.Hour).Seconds()
|
||||
}
|
||||
return a.UsageEnd.Sub(*a.UsageStart).Seconds()
|
||||
if a.BookingConfiguration == nil {
|
||||
a.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
return a.ExplicitBookingDurationS
|
||||
if a.BookingConfiguration.ExplicitBookingDurationS == 0 {
|
||||
if a.BookingConfiguration.UsageStart == nil {
|
||||
return (5 * time.Minute).Seconds()
|
||||
}
|
||||
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
|
||||
}
|
||||
return a.BookingConfiguration.ExplicitBookingDurationS
|
||||
}
|
||||
|
||||
func (d *ProcessingResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request, func() utils.DBObject { return &ProcessingResource{} }) // Create a new instance of the accessor
|
||||
return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
func (abs *ProcessingResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
|
||||
if t != tools.PROCESSING_RESOURCE {
|
||||
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
|
||||
}
|
||||
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priced := p.(*PricedResource[*DataResourcePricingProfile])
|
||||
return &PricedDataResource{
|
||||
PricedResource: *priced,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ProcessingResourcePricingProfile struct {
|
||||
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.PERMANENT
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -9,11 +9,50 @@ 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"`
|
||||
FromNano string `json:"from_nano,omitempty" bson:"priced_item,omitempty"`
|
||||
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"`
|
||||
|
||||
// Authorization: identifies who created this draft and the Check session it belongs to.
|
||||
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
|
||||
}
|
||||
|
||||
func (ri *PurchaseResource) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "dest_peer", "scheduler_peer":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.PEER)
|
||||
case "execution":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.WORKFLOW_EXECUTION)
|
||||
case "resource":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.WORKFLOW_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.DATA_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.COMPUTE_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.STORAGE_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.PROCESSING_RESOURCE)
|
||||
ext[t] = append(ext[t], tools.SERVICE_RESOURCE)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
}
|
||||
func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
@@ -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,46 +27,26 @@ 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)
|
||||
utils.GenericDelete(d, a)
|
||||
return nil, 404, nil
|
||||
}
|
||||
return d, 200, nil
|
||||
}, 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)
|
||||
utils.GenericDelete(d, a)
|
||||
return nil
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
func (dca *PurchaseResourceMongoAccessor) ShouldVerifyAuth() bool {
|
||||
return false // TEMP : by pass
|
||||
}
|
||||
|
||||
@@ -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(-5 * time.Minute)
|
||||
future := now.Add(5 * time.Minute)
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
Regular → Executable
+450
-92
@@ -1,86 +1,228 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"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/live"
|
||||
"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/tools"
|
||||
"github.com/biter777/countries"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
|
||||
return dbs.FiltersFromFlatMap(flatMap, target)
|
||||
}
|
||||
|
||||
// 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)
|
||||
PurchaseID string `json:"purchase_id,omitempty"` // is_buy precise if a resource is buy or not
|
||||
Type string `json:"type,omitempty" bson:"type,omitempty"` // Type is the type of the resource
|
||||
Logo string `json:"logo,omitempty" bson:"logo,omitempty"` // Logo is the logo of the resource
|
||||
Description string `json:"description,omitempty" bson:"description,omitempty"` // Description is the description of the resource
|
||||
ShortDescription string `json:"short_description,omitempty" bson:"short_description,omitempty"` // ShortDescription is the short description of the resource
|
||||
Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource
|
||||
UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
|
||||
AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"`
|
||||
|
||||
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"`
|
||||
|
||||
// License is the usage licence of the resource (SPDX identifier or free-text).
|
||||
// Displayed prominently in the catalog detail view.
|
||||
License string `json:"license,omitempty" bson:"license,omitempty"`
|
||||
|
||||
// ExploitationAuthorizations (AEs) are coupling and peer-usage constraints
|
||||
// issued by the resource owner. Stored embedded in the resource document,
|
||||
// NOT in a separate collection.
|
||||
// Visibility-filtered per requesting peer before any response is sent.
|
||||
ExploitationAuthorizations []ExploitationAuthorization `json:"exploitation_authorizations,omitempty" bson:"exploitation_authorizations,omitempty"`
|
||||
}
|
||||
|
||||
func (r *AbstractResource) GetSelectedInstance() utils.DBObject {
|
||||
func (ri *AbstractResource) Extend(typ ...string) map[string][]tools.DataType {
|
||||
dt := ri.AbstractObject.Extend(typ...)
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "purchase":
|
||||
if _, ok := dt[t]; !ok {
|
||||
dt[t] = []tools.DataType{}
|
||||
}
|
||||
dt[t] = append(dt[t], tools.PURCHASE_RESOURCE)
|
||||
}
|
||||
}
|
||||
return dt
|
||||
}
|
||||
|
||||
func (abs *AbstractResource) VerifyBuy() {
|
||||
p := &purchase_resource.PurchaseResource{}
|
||||
access := p.GetAccessor(&tools.APIRequest{Admin: true})
|
||||
|
||||
purchase, _, _ := access.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"resource_id": {{Operator: dbs.EQUAL.String(), Value: abs.GetID()}},
|
||||
},
|
||||
}, "", false, 0, 1)
|
||||
if len(purchase) > 0 {
|
||||
abs.PurchaseID = purchase[0].GetID()
|
||||
}
|
||||
}
|
||||
|
||||
func (abs *AbstractResource) GetEnv() []models.Param {
|
||||
return abs.Env
|
||||
}
|
||||
func (abs *AbstractResource) GetInputs() []models.Param {
|
||||
return abs.Inputs
|
||||
}
|
||||
func (abs *AbstractResource) GetOutputs() []models.Param {
|
||||
return abs.Outputs
|
||||
}
|
||||
|
||||
func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExploitationAuthorizations returns all AEs attached to this resource.
|
||||
// Used by oc-schedulerd's CheckWorkflowAE via structural interface assertion.
|
||||
func (r *AbstractResource) GetExploitationAuthorizations() []ExploitationAuthorization {
|
||||
return r.ExploitationAuthorizations
|
||||
}
|
||||
|
||||
// FilterExploitationAuthorizations removes AEs that are not visible to peerID.
|
||||
// Must be called before serializing the resource for a consumer peer.
|
||||
// The resource owner (CreatorID) always sees all AEs unfiltered.
|
||||
func (r *AbstractResource) FilterExploitationAuthorizations(peerID string, isAdmin bool) {
|
||||
if isAdmin {
|
||||
return // admin or owner: no filtering
|
||||
}
|
||||
filtered := r.ExploitationAuthorizations[:0]
|
||||
for _, ae := range r.ExploitationAuthorizations {
|
||||
if ae.IsVisibleTo(peerID, r.CreatorID) {
|
||||
filtered = append(filtered, ae)
|
||||
}
|
||||
}
|
||||
r.ExploitationAuthorizations = filtered
|
||||
}
|
||||
|
||||
func (ri *AbstractResource) ClearEnv() utils.DBObject {
|
||||
ri.Env = []models.Param{}
|
||||
ri.Inputs = []models.Param{}
|
||||
ri.Outputs = []models.Param{}
|
||||
return ri
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (r *AbstractResource) StoreDraftDefault() {
|
||||
r.IsDraft = true
|
||||
//r.IsDraft = true pour le moment on passe outre.
|
||||
}
|
||||
|
||||
func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
if r.IsDraft != set.IsDrafted() && set.IsDrafted() {
|
||||
return true, set // only state can be updated
|
||||
}
|
||||
return r.IsDraft != set.IsDrafted() && set.IsDrafted(), set
|
||||
}
|
||||
|
||||
func (r *AbstractResource) CanDelete() bool {
|
||||
return r.IsDraft // only draft bookings can be deleted
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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 ConvertToPricedResource[T pricing.PricingProfileITF](t tools.DataType,
|
||||
selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int,
|
||||
selectedBookingModeIndex *int, abs ResourceInterface, request *tools.APIRequest) (pricing.PricedItemITF, error) {
|
||||
instances := map[string]string{}
|
||||
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.SetAllowedInstances(request) { // 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
|
||||
}
|
||||
}
|
||||
}
|
||||
return &PricedResource{
|
||||
Name: abs.Name,
|
||||
Logo: abs.Logo,
|
||||
ResourceID: abs.UUID,
|
||||
ResourceType: t,
|
||||
InstancesRefs: instances,
|
||||
PricingProfiles: profiles,
|
||||
CreatorID: abs.CreatorID,
|
||||
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.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)] != nil {
|
||||
variations = append(variations, abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)])
|
||||
}
|
||||
// Seed the booking configuration with the instance's historical average duration
|
||||
// so GetExplicitDurationInS() returns a realistic default out of the box.
|
||||
var bc *BookingConfiguration
|
||||
if inst != nil {
|
||||
if avg := inst.GetAverageDurationS(); avg > 0 {
|
||||
bc = &BookingConfiguration{ExplicitBookingDurationS: avg}
|
||||
}
|
||||
}
|
||||
instanceID := ""
|
||||
if inst != nil {
|
||||
instanceID = inst.GetID()
|
||||
}
|
||||
selectedPricing, _ := profile.(T)
|
||||
return &PricedResource[T]{
|
||||
Name: abs.GetName(),
|
||||
ResourceID: abs.GetID(),
|
||||
InstanceID: instanceID,
|
||||
ResourceType: t,
|
||||
Quantity: 1,
|
||||
InstancesRefs: instances,
|
||||
SelectedPricing: selectedPricing,
|
||||
Variations: variations,
|
||||
CreatorID: abs.GetCreatorID(),
|
||||
BookingConfiguration: bc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
|
||||
for _, instance := range abs.Instances {
|
||||
instance.ClearEnv()
|
||||
}
|
||||
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,41 +230,53 @@ 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 != "" {
|
||||
return
|
||||
}
|
||||
abs.Instances = verifyAuthAction[T](abs.Instances, request)
|
||||
}
|
||||
|
||||
func (d *AbstractInstanciatedResource[T]) Trim() {
|
||||
d.Type = d.GetType()
|
||||
if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: d.CreatorID}}).IsMySelf(); !ok {
|
||||
for _, instance := range d.Instances {
|
||||
instance.ClearPeerGroups()
|
||||
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
|
||||
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
|
||||
// Filter AEs: only return AEs visible to the requesting peer.
|
||||
if request != nil {
|
||||
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
inst := []ResourceInstanceITF{}
|
||||
for _, i := range abs.Instances {
|
||||
inst = append(inst, i)
|
||||
}
|
||||
return inst
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Structurally peerless instances (no creator, no partnerships, non-empty Ref)
|
||||
// are freely accessible by any requester.
|
||||
if instance.IsPeerless() {
|
||||
instances = append(instances, instance)
|
||||
continue
|
||||
}
|
||||
_, peerGroups := instance.GetPeerGroups()
|
||||
for _, peers := range peerGroups {
|
||||
if request == nil {
|
||||
continue
|
||||
}
|
||||
if grps, ok := peers[request.PeerID]; ok || config.GetConfig().Whitelist {
|
||||
if (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
|
||||
_, allOK := peers["*"]
|
||||
if grps, ok := peers[request.PeerID]; ok || allOK || config.GetConfig().Whitelist {
|
||||
if allOK || (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)
|
||||
}
|
||||
}
|
||||
@@ -132,48 +286,116 @@ func verifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.AP
|
||||
return instances
|
||||
}
|
||||
|
||||
type GeoPoint struct {
|
||||
Latitude float64 `json:"latitude,omitempty" bson:"latitude,omitempty"`
|
||||
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"`
|
||||
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
|
||||
LastUpdate time.Time `json:"last_update,omitempty" bson:"last_update,omitempty"`
|
||||
Origin OriginMeta `json:"origin,omitempty" bson:"origin,omitempty"`
|
||||
Location live.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"`
|
||||
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"`
|
||||
|
||||
AverageDurationS float64 `json:"average_duration_s,omitempty" bson:"average_duration_s,omitempty"`
|
||||
AverageDurationSamples int `json:"average_duration_samples,omitempty" bson:"average_duration_samples,omitempty"`
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) ClearEnv() {
|
||||
ri.Env = []models.Param{}
|
||||
ri.Inputs = []models.Param{}
|
||||
ri.Outputs = []models.Param{}
|
||||
// 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]) GetOrigin() OriginMeta {
|
||||
return ri.Origin
|
||||
}
|
||||
|
||||
// IsPeerless returns true when the instance has no owning peer and a non-empty
|
||||
// registry reference. This is derived from structural invariants — NOT from the
|
||||
// self-declared Origin.Type field — to prevent auth bypass via metadata manipulation:
|
||||
//
|
||||
// CreatorID == "" ∧ len(Partnerships) == 0 ∧ Origin.Ref != ""
|
||||
func (ri *ResourceInstance[T]) IsPeerless() bool {
|
||||
return ri.CreatorID == "" && len(ri.Partnerships) == 0 && ri.Origin.Ref != ""
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
|
||||
partnerships := []T{}
|
||||
for _, p := range ri.Partnerships {
|
||||
if p.GetPeerGroups()["*"] != nil || p.GetPeerGroups()[peerID] != nil {
|
||||
p.FilterPartnership(peerID)
|
||||
partnerships = append(partnerships, p)
|
||||
}
|
||||
}
|
||||
ri.Partnerships = partnerships
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) GetProfile(peerID string, partnershipIndex *int, buyingIndex *int, strategyIndex *int) pricing.PricingProfileITF {
|
||||
if ri.IsPeerless() {
|
||||
return pricing.GetDefaultPricingProfile()
|
||||
}
|
||||
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 {
|
||||
if ri.IsPeerless() {
|
||||
return []pricing.PricingProfileITF{pricing.GetDefaultPricingProfile()}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) {
|
||||
// Structurally peerless: universally accessible — wildcard on all peers.
|
||||
if ri.IsPeerless() {
|
||||
return []ResourcePartnerITF{}, []map[string][]string{{"*": {"*"}}}
|
||||
}
|
||||
groups := []map[string][]string{}
|
||||
partners := []ResourcePartnerITF{}
|
||||
for _, p := range ri.Partnerships {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -183,40 +405,176 @@ func (ri *ResourceInstance[T]) ClearPeerGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) GetAverageDurationS() float64 {
|
||||
return ri.AverageDurationS
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) UpdateAverageDuration(actualS float64) {
|
||||
buffered := actualS * 1.20
|
||||
n := float64(ri.AverageDurationSamples)
|
||||
ri.AverageDurationS = (ri.AverageDurationS*n + buffered) / (n + 1)
|
||||
ri.AverageDurationSamples++
|
||||
}
|
||||
|
||||
func (ri *ResourceInstance[T]) GetPartnerships() []ResourcePartnerITF {
|
||||
rt := []ResourcePartnerITF{}
|
||||
for _, p := range ri.Partnerships {
|
||||
rt = append(rt, p)
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
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["*"] == nil && ri.PeerGroups[peerID] == nil {
|
||||
ri.PeerGroups = map[string][]string{
|
||||
"*": {"*"},
|
||||
}
|
||||
} else {
|
||||
ri.PeerGroups = map[string][]string{}
|
||||
if ri.PeerGroups["*"] != nil {
|
||||
ri.PeerGroups["*"] = ri.PeerGroups["*"]
|
||||
}
|
||||
if ri.PeerGroups[peerID] != nil {
|
||||
ri.PeerGroups[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 {
|
||||
if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil {
|
||||
return profiles
|
||||
}
|
||||
if ri.PeerGroups["*"] != nil {
|
||||
for _, ri := range ri.PricingProfiles {
|
||||
profiles = append(profiles, ri)
|
||||
for _, i := range ri {
|
||||
profiles = append(profiles, i)
|
||||
}
|
||||
}
|
||||
if slices.Contains(groups, "*") {
|
||||
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
|
||||
case tools.SERVICE_RESOURCE.EnumIndex():
|
||||
var data ServiceResource
|
||||
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")
|
||||
}
|
||||
|
||||
type ResourceAccess struct {
|
||||
Source *models.PathSource `json:"source,omitempty" bson:"source,omitempty"`
|
||||
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
|
||||
}
|
||||
|
||||
// HasSource returns true when the access is source-based (no embedded container).
|
||||
func (a *ResourceAccess) HasSource() bool {
|
||||
return a != nil && a.Container == nil && a.Source != nil
|
||||
}
|
||||
|
||||
// HasContainer returns true when an explicit container image is provided.
|
||||
func (a *ResourceAccess) HasContainer() bool {
|
||||
return a != nil && a.Container != nil
|
||||
}
|
||||
|
||||
Regular → Executable
+138
-40
@@ -1,92 +1,190 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) *ResourceMongoAccessor[T] {
|
||||
if !slices.Contains([]tools.DataType{
|
||||
tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE,
|
||||
tools.PROCESSING_RESOURCE, tools.SERVICE_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.SERVICE_RESOURCE:
|
||||
return &ServiceResource{}
|
||||
case tools.WORKFLOW_RESOURCE:
|
||||
return &WorkflowResource{}
|
||||
case tools.DATA_RESOURCE:
|
||||
return &DataResource{}
|
||||
case tools.NATIVE_TOOL:
|
||||
return &NativeTool{}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
generateData: g,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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]) LoadOne(id string) (utils.DBObject, int, error) {
|
||||
data, code, err := dca.AbstractAccessor.LoadOne(id)
|
||||
if err == nil {
|
||||
data.(T).VerifyBuy()
|
||||
data.(T).SetAllowedInstances(dca.Request)
|
||||
return data, code, err
|
||||
}
|
||||
return data, code, err
|
||||
}
|
||||
|
||||
func (dca *resourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
|
||||
set.(T).Trim()
|
||||
return utils.GenericUpdateOne(set, id, dca, dca.generateData())
|
||||
func (dca *ResourceMongoAccessor[T]) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
|
||||
if dca.GetType() == tools.COMPUTE_RESOURCE {
|
||||
delete(set, "architecture")
|
||||
delete(set, "infrastructure")
|
||||
} else if dca.GetType() == tools.SERVICE_RESOURCE {
|
||||
delete(set, "infrastructure")
|
||||
} else if dca.GetType() == tools.STORAGE_RESOURCE {
|
||||
delete(set, "storage_type")
|
||||
}
|
||||
return utils.GenericUpdateOne(set, id, dca)
|
||||
}
|
||||
|
||||
func (dca *resourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
data.(T).Trim()
|
||||
return utils.GenericStoreOne(data, dca)
|
||||
func (dca *ResourceMongoAccessor[T]) ShouldVerifyAuth() bool {
|
||||
return false // TEMP : by pass
|
||||
}
|
||||
|
||||
func (dca *resourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
var i string
|
||||
idsToUpdate := []string{}
|
||||
var a utils.Accessor
|
||||
if dca.GetType() == tools.COMPUTE_RESOURCE {
|
||||
r := data.(*ComputeResource)
|
||||
if len(r.Instances) == 0 {
|
||||
return nil, 404, errors.New("can't create a non existing computing units resource with no instances")
|
||||
}
|
||||
a = live.NewAccessor[*live.LiveDatacenter](tools.LIVE_DATACENTER, &tools.APIRequest{Admin: true})
|
||||
res, _, _ := a.LoadOne(r.Instances[0].GetID())
|
||||
if res == nil {
|
||||
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
|
||||
}
|
||||
if !res.(*live.LiveDatacenter).IsCompatible(data.Serialize(data)) {
|
||||
return nil, 404, errors.New("live computing units target is not compatible")
|
||||
}
|
||||
i = res.GetID()
|
||||
idsToUpdate = res.(*live.LiveDatacenter).ResourcesID
|
||||
} else if dca.GetType() == tools.SERVICE_RESOURCE {
|
||||
r := data.(*ServiceResource)
|
||||
if len(r.Instances) == 0 {
|
||||
return nil, 404, errors.New("can't create a non existing service resource with no instances")
|
||||
}
|
||||
a = live.NewAccessor[*live.LiveService](tools.LIVE_SERVICE, &tools.APIRequest{Admin: true})
|
||||
res, _, _ := a.LoadOne(r.Instances[0].GetID())
|
||||
if res == nil {
|
||||
return nil, 404, errors.New("can't create a non existing service resource not reported onto compute units catalog")
|
||||
}
|
||||
if !res.(*live.LiveService).IsCompatible(data.Serialize(data)) {
|
||||
return nil, 404, errors.New("live service target is not compatible")
|
||||
}
|
||||
i = res.GetID()
|
||||
idsToUpdate = res.(*live.LiveService).ResourcesID
|
||||
} else if dca.GetType() == tools.STORAGE_RESOURCE {
|
||||
r := data.(*StorageResource)
|
||||
if len(r.Instances) == 0 {
|
||||
return nil, 404, errors.New("can't create a non existing storage resource with no instances")
|
||||
}
|
||||
a = live.NewAccessor[*live.LiveStorage](tools.LIVE_STORAGE, &tools.APIRequest{Admin: true})
|
||||
res, _, _ := a.LoadOne(r.Instances[0].GetID())
|
||||
if res == nil {
|
||||
return nil, 404, errors.New("can't create a non existing storage resource not reported onto compute units catalog")
|
||||
}
|
||||
if !res.(*live.LiveStorage).IsCompatible(data.Serialize(data)) {
|
||||
return nil, 404, errors.New("live storage target is not compatible")
|
||||
}
|
||||
i = res.GetID()
|
||||
idsToUpdate = res.(*live.LiveStorage).ResourcesID
|
||||
}
|
||||
res, code, err := utils.GenericStoreOne(data, dca)
|
||||
if res != nil && i != "" {
|
||||
idsToUpdate = append(idsToUpdate, res.GetID())
|
||||
a.UpdateOne(map[string]interface{}{
|
||||
"resources_id": idsToUpdate,
|
||||
}, i)
|
||||
}
|
||||
return res, code, err
|
||||
}
|
||||
|
||||
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
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, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
|
||||
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit)
|
||||
}
|
||||
|
||||
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, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
|
||||
if filters == nil && search == "*" {
|
||||
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
|
||||
fmt.Println("Search", d)
|
||||
d.(T).VerifyBuy()
|
||||
d.(T).SetAllowedInstances(wfa.Request)
|
||||
fmt.Println("Search2", d)
|
||||
return d
|
||||
}, isDraft, wfa)
|
||||
}, isDraft, wfa, offset, limit)
|
||||
}
|
||||
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).VerifyBuy()
|
||||
d.(T).SetAllowedInstances(wfa.Request)
|
||||
return d
|
||||
}, isDraft, wfa)
|
||||
}, isDraft, wfa, offset, limit)
|
||||
}
|
||||
|
||||
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).VerifyBuy()
|
||||
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}},
|
||||
"abstractintanciatedresource.abstractresource.type": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractintanciatedresource.abstractresource.short_description": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractintanciatedresource.abstractresource.description": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractintanciatedresource.abstractresource.owners.name": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractintanciatedresource.abstractresource.abstractobject.creator_id": {{Operator: dbs.EQUAL.String(), Value: search}},
|
||||
"abstractinstanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractinstanciatedresource.abstractresource.type": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractinstanciatedresource.abstractresource.short_description": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractinstanciatedresource.abstractresource.description": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
"abstractinstanciatedresource.abstractresource.owners.name": {{Operator: dbs.LIKE.String(), Value: search}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"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/google/uuid"
|
||||
)
|
||||
|
||||
type ServiceMode int
|
||||
|
||||
const (
|
||||
DEPLOYMENT ServiceMode = iota // deploy the service, pay for uptime — duration unbounded
|
||||
HOSTED // use an existing service, pay per call — duration per request
|
||||
)
|
||||
|
||||
func (m ServiceMode) String() string {
|
||||
return [...]string{"DEPLOYMENT", "HOSTED"}[m]
|
||||
}
|
||||
|
||||
type ServiceUsage struct {
|
||||
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"`
|
||||
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"`
|
||||
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"`
|
||||
StorageGb float64 `bson:"storage,omitempty" json:"storage,omitempty"`
|
||||
Hypothesis string `bson:"hypothesis,omitempty" json:"hypothesis,omitempty"`
|
||||
ScalingModel string `bson:"scaling_model,omitempty" json:"scaling_model,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceResourceAccess describes how to reach the service once running.
|
||||
// Populated for HOSTED instances (endpoint already known) and as a template for DEPLOYMENT.
|
||||
type ServiceResourceAccess struct {
|
||||
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"`
|
||||
Protocol live.ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
|
||||
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
|
||||
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceResource struct {
|
||||
AbstractInstanciatedResource[*ServiceInstance]
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
|
||||
Usage *ServiceUsage `bson:"usage,omitempty" json:"usage,omitempty"`
|
||||
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
|
||||
License string `json:"license,omitempty" bson:"license,omitempty"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
}
|
||||
|
||||
func (r *ServiceResource) GetType() string {
|
||||
return tools.SERVICE_RESOURCE.String()
|
||||
}
|
||||
|
||||
func (d *ServiceResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*ServiceResource](tools.SERVICE_RESOURCE, request)
|
||||
}
|
||||
|
||||
func (abs *ServiceResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
|
||||
if t != tools.SERVICE_RESOURCE {
|
||||
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Service")
|
||||
}
|
||||
p, err := ConvertToPricedResource[*ServiceResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priced := p.(*PricedResource[*ServiceResourcePricingProfile])
|
||||
return &PricedServiceResource{PricedResource: *priced}, nil
|
||||
}
|
||||
|
||||
type ServiceInstance struct {
|
||||
ResourceInstance[*ServiceResourcePartnership]
|
||||
Mode ServiceMode `json:"mode" bson:"mode" default:"0"`
|
||||
Access *ServiceResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
|
||||
MaxConcurrent int `json:"max_concurrent,omitempty" bson:"max_concurrent,omitempty"`
|
||||
}
|
||||
|
||||
func (ri *ServiceInstance) IsPeerless() bool { return false }
|
||||
|
||||
func NewServiceInstance(name string, peerID string) ResourceInstanceITF {
|
||||
return &ServiceInstance{
|
||||
ResourceInstance: ResourceInstance[*ServiceResourcePartnership]{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
UUID: uuid.New().String(),
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceResourcePartnership struct {
|
||||
ResourcePartnerShip[*ServiceResourcePricingProfile]
|
||||
}
|
||||
|
||||
// ServiceResourcePricingProfile handles both service modes:
|
||||
// - DEPLOYMENT: uptime billing via ExploitPricingProfile (pay while service is up)
|
||||
// - HOSTED: per-call billing via AccessPricingProfile (pay per request)
|
||||
type ServiceResourcePricingProfile struct {
|
||||
Mode ServiceMode `json:"mode" bson:"mode"`
|
||||
UptimePricing *pricing.ExploitPricingProfile[pricing.TimePricingStrategy] `json:"uptime_pricing,omitempty" bson:"uptime_pricing,omitempty"`
|
||||
AccessPricing *pricing.AccessPricingProfile[pricing.TimePricingStrategy] `json:"access_pricing,omitempty" bson:"access_pricing,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ServiceResourcePricingProfile) ensure() {
|
||||
if p.UptimePricing == nil {
|
||||
p.UptimePricing = &pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{}
|
||||
}
|
||||
if p.AccessPricing == nil {
|
||||
p.AccessPricing = &pricing.AccessPricingProfile[pricing.TimePricingStrategy]{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServiceResourcePricingProfile) IsPurchasable() bool {
|
||||
p.ensure()
|
||||
if p.Mode == DEPLOYMENT {
|
||||
return p.UptimePricing.IsPurchasable()
|
||||
}
|
||||
return p.AccessPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (p *ServiceResourcePricingProfile) IsBooked() bool {
|
||||
p.ensure()
|
||||
if p.Mode == DEPLOYMENT {
|
||||
return p.UptimePricing.IsBooked()
|
||||
}
|
||||
return p.AccessPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (p *ServiceResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
|
||||
p.ensure()
|
||||
if p.Mode == DEPLOYMENT {
|
||||
return p.UptimePricing.GetPurchase()
|
||||
}
|
||||
return p.AccessPricing.GetPurchase()
|
||||
}
|
||||
|
||||
func (p *ServiceResourcePricingProfile) GetOverrideStrategyValue() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (p *ServiceResourcePricingProfile) GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variations []*pricing.PricingVariation, params ...string) (float64, error) {
|
||||
p.ensure()
|
||||
if p.Mode == DEPLOYMENT {
|
||||
return p.UptimePricing.GetPriceHT(quantity, val, start, end, variations, params...)
|
||||
}
|
||||
return p.AccessPricing.GetPriceHT(quantity, val, start, end, variations, params...)
|
||||
}
|
||||
|
||||
type PricedServiceResource struct {
|
||||
PricedResource[*ServiceResourcePricingProfile]
|
||||
}
|
||||
|
||||
func (r *PricedServiceResource) ensurePricing() {
|
||||
if r.SelectedPricing == nil {
|
||||
r.SelectedPricing = &ServiceResourcePricingProfile{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PricedServiceResource) IsPurchasable() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (r *PricedServiceResource) IsBooked() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (r *PricedServiceResource) GetType() tools.DataType {
|
||||
return tools.SERVICE_RESOURCE
|
||||
}
|
||||
|
||||
func (r *PricedServiceResource) GetPriceHT() (float64, error) {
|
||||
r.ensurePricing()
|
||||
return r.PricedResource.GetPriceHT()
|
||||
}
|
||||
|
||||
// GetExplicitDurationInS returns -1 for DEPLOYMENT (unbounded uptime).
|
||||
// For HOSTED, returns the actual call window duration.
|
||||
func (a *PricedServiceResource) GetExplicitDurationInS() float64 {
|
||||
a.ensurePricing()
|
||||
if a.SelectedPricing.Mode == DEPLOYMENT {
|
||||
return -1
|
||||
}
|
||||
if a.BookingConfiguration == nil {
|
||||
a.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
if a.BookingConfiguration.ExplicitBookingDurationS != 0 {
|
||||
return a.BookingConfiguration.ExplicitBookingDurationS
|
||||
}
|
||||
if a.BookingConfiguration.UsageStart == nil || a.BookingConfiguration.UsageEnd == nil {
|
||||
return -1 // no deadline specified: open-ended
|
||||
}
|
||||
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
|
||||
}
|
||||
Regular → Executable
+86
-53
@@ -2,7 +2,6 @@ package resources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
@@ -10,8 +9,20 @@ 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"
|
||||
)
|
||||
|
||||
// EmbeddedStorageSelection records which storage capability was activated on a
|
||||
// compute unit graph node, and which pricing options were selected for it.
|
||||
// Key in WorkflowExecution.SelectedEmbeddedStorages is the compute graph node ID.
|
||||
// A nil/absent entry means no storage was activated on that compute unit.
|
||||
type EmbeddedStorageSelection struct {
|
||||
StorageIndex int `json:"storage_index" bson:"storage_index"` // index in ComputeResourceInstance.AvailableStorages
|
||||
PartnershipIndex int `json:"partnership_index" bson:"partnership_index"` // index in the storage's partnerships
|
||||
BuyingIndex int `json:"buying_index" bson:"buying_index"`
|
||||
StrategyIndex int `json:"strategy_index" bson:"strategy_index"`
|
||||
}
|
||||
|
||||
/*
|
||||
* StorageResource is a struct that represents a storage resource
|
||||
* it defines the resource storage
|
||||
@@ -23,29 +34,40 @@ type StorageResource struct {
|
||||
}
|
||||
|
||||
func (d *StorageResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor[*StorageResource](tools.STORAGE_RESOURCE, request, func() utils.DBObject { return &StorageResource{} }) // Create a new instance of the accessor
|
||||
return NewAccessor[*StorageResource](tools.STORAGE_RESOURCE, request) // Create a new instance of the accessor
|
||||
}
|
||||
|
||||
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 := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
|
||||
priced := p.(*PricedResource)
|
||||
p, err := ConvertToPricedResource[*StorageResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priced := p.(*PricedResource[*StorageResourcePricingProfile])
|
||||
return &PricedStorageResource{
|
||||
PricedResource: *priced,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ri *StorageResource) StoreDraftDefault() {
|
||||
ri.AbstractObject.StoreDraftDefault()
|
||||
ri.Env = append(ri.Env, models.Param{
|
||||
Attr: "source",
|
||||
Value: "[resource]instance.source",
|
||||
Readonly: true,
|
||||
})
|
||||
}
|
||||
|
||||
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,29 +77,19 @@ type StorageResourceInstance struct {
|
||||
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
|
||||
}
|
||||
|
||||
func (ri *StorageResourceInstance) ClearEnv() {
|
||||
ri.Credentials = nil
|
||||
ri.Env = []models.Param{}
|
||||
ri.Inputs = []models.Param{}
|
||||
ri.Outputs = []models.Param{}
|
||||
}
|
||||
// IsPeerless is always false for storage instances: a storage resource is
|
||||
// infrastructure owned by a peer and can never be declared peerless.
|
||||
func (ri *StorageResourceInstance) IsPeerless() bool { return false }
|
||||
|
||||
func (ri *StorageResourceInstance) StoreDraftDefault() {
|
||||
found := false
|
||||
for _, p := range ri.ResourceInstance.Env {
|
||||
if p.Attr == "source" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
func NewStorageResourceInstance(name string, peerID string) ResourceInstanceITF {
|
||||
return &StorageResourceInstance{
|
||||
ResourceInstance: ResourceInstance[*StorageResourcePartnership]{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
UUID: uuid.New().String(),
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
if !found {
|
||||
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
|
||||
Attr: "source",
|
||||
Value: ri.Source,
|
||||
Readonly: true,
|
||||
})
|
||||
}
|
||||
ri.ResourceInstance.StoreDraftDefault()
|
||||
}
|
||||
|
||||
type StorageResourcePartnership struct {
|
||||
@@ -105,7 +117,7 @@ func (t PrivilegeStoragePricingStrategy) String() string {
|
||||
type StorageResourcePricingStrategy int
|
||||
|
||||
const (
|
||||
PER_DATA_STORED StorageResourcePricingStrategy = iota
|
||||
PER_DATA_STORED StorageResourcePricingStrategy = iota + 7
|
||||
PER_TB_STORED
|
||||
PER_GB_STORED
|
||||
PER_MB_STORED
|
||||
@@ -117,11 +129,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,40 +168,56 @@ 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.PERMANENT
|
||||
}
|
||||
|
||||
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 {
|
||||
PricedResource
|
||||
PricedResource[*StorageResourcePricingProfile]
|
||||
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
|
||||
}
|
||||
|
||||
func (r *PricedStorageResource) ensurePricing() {
|
||||
if r.SelectedPricing == nil {
|
||||
r.SelectedPricing = &StorageResourcePricingProfile{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PricedStorageResource) IsPurchasable() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsPurchasable()
|
||||
}
|
||||
|
||||
func (r *PricedStorageResource) IsBooked() bool {
|
||||
r.ensurePricing()
|
||||
return r.SelectedPricing.IsBooked()
|
||||
}
|
||||
|
||||
func (r *PricedStorageResource) GetType() tools.DataType {
|
||||
return tools.STORAGE_RESOURCE
|
||||
}
|
||||
|
||||
func (r *PricedStorageResource) GetPrice() (float64, error) {
|
||||
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
|
||||
func (r *PricedStorageResource) GetPriceHT() (float64, error) {
|
||||
r.ensurePricing()
|
||||
if r.BookingConfiguration == nil {
|
||||
r.BookingConfiguration = &BookingConfiguration{}
|
||||
}
|
||||
now := time.Now()
|
||||
if r.UsageStart == nil {
|
||||
r.UsageStart = &now
|
||||
if r.BookingConfiguration.UsageStart == nil {
|
||||
r.BookingConfiguration.UsageStart = &now
|
||||
}
|
||||
if r.UsageEnd == nil {
|
||||
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
|
||||
r.UsageEnd = &add
|
||||
if r.BookingConfiguration.UsageEnd == nil {
|
||||
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
|
||||
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]
|
||||
}
|
||||
pricing := *r.SelectedPricing
|
||||
pricing := r.SelectedPricing
|
||||
var err error
|
||||
amountOfData := float64(1)
|
||||
if pricing.GetOverrideStrategyValue() >= 0 {
|
||||
@@ -194,5 +226,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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
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(5 * time.Minute)
|
||||
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(5 * time.Minute)
|
||||
r := resources.PricedComputeResource{
|
||||
PricedResource: resources.PricedResource[*resources.ComputeResourcePricingProfile]{
|
||||
ResourceID: "comp456",
|
||||
SelectedPricing: &resources.ComputeResourcePricingProfile{
|
||||
CPUsPrices: map[string]float64{"Xeon": 2.0},
|
||||
ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{
|
||||
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
|
||||
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0},
|
||||
},
|
||||
},
|
||||
},
|
||||
BookingConfiguration: &resources.BookingConfiguration{
|
||||
UsageStart: &start,
|
||||
UsageEnd: &end,
|
||||
ExplicitBookingDurationS: 3600,
|
||||
},
|
||||
},
|
||||
CPUsLocated: map[string]float64{"Xeon": 2},
|
||||
GPUsLocated: map[string]float64{},
|
||||
RAMLocated: 0,
|
||||
}
|
||||
|
||||
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[*resources.ComputeResourcePricingProfile]{
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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/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 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.PERMANENT
|
||||
assert.True(t, profile.IsPurchasable())
|
||||
}
|
||||
|
||||
func TestPricedDataResource_GetPriceHT(t *testing.T) {
|
||||
now := time.Now()
|
||||
later := now.Add(5 * time.Minute)
|
||||
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[*resources.DataResourcePricingProfile]{
|
||||
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[*resources.DataResourcePricingProfile]{
|
||||
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())
|
||||
}
|
||||
@@ -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[pricing.PricingProfileITF]{
|
||||
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[pricing.PricingProfileITF]{}
|
||||
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[pricing.PricingProfileITF]{SelectedPricing: mock}
|
||||
assert.True(t, r.IsPurchasable())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAndSetLocationStartEnd(t *testing.T) {
|
||||
r := &resources.PricedResource[pricing.PricingProfileITF]{}
|
||||
|
||||
now := time.Now()
|
||||
r.SetLocationStart(now)
|
||||
r.SetLocationEnd(now.Add(10 * time.Minute))
|
||||
|
||||
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[pricing.PricingProfileITF]{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(10 * time.Minute)
|
||||
r := &resources.PricedResource[pricing.PricingProfileITF]{
|
||||
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[pricing.PricingProfileITF]{}
|
||||
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[pricing.PricingProfileITF]{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[pricing.PricingProfileITF]{
|
||||
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(5 * time.Minute)
|
||||
mock := &MockPricingProfile{ReturnErr: true}
|
||||
r := &resources.PricedResource[pricing.PricingProfileITF]{
|
||||
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(5 * time.Minute)
|
||||
mock := &MockPricingProfile{ReturnCost: 10.0}
|
||||
r := &resources.PricedResource[pricing.PricingProfileITF]{
|
||||
SelectedPricing: mock,
|
||||
BookingConfiguration: &resources.BookingConfiguration{
|
||||
UsageStart: &start,
|
||||
UsageEnd: &end,
|
||||
},
|
||||
}
|
||||
price, err := r.GetPriceHT()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 10.0, price)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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(10 * time.Minute)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input PricedProcessingResource
|
||||
expected float64
|
||||
}{
|
||||
{
|
||||
name: "Nil start time, non-service",
|
||||
input: PricedProcessingResource{
|
||||
PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
|
||||
BookingConfiguration: &resources.BookingConfiguration{
|
||||
UsageStart: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: float64((5 * time.Minute).Seconds()),
|
||||
},
|
||||
{
|
||||
name: "Duration computed from start and end",
|
||||
input: PricedProcessingResource{
|
||||
PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
|
||||
BookingConfiguration: &resources.BookingConfiguration{
|
||||
UsageStart: &now,
|
||||
UsageEnd: &after,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: float64((10 * time.Minute).Seconds()),
|
||||
},
|
||||
{
|
||||
name: "Explicit duration takes precedence",
|
||||
input: PricedProcessingResource{
|
||||
PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
|
||||
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(10 * time.Minute)
|
||||
mockPricing := pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
|
||||
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{
|
||||
Price: 100.0,
|
||||
},
|
||||
}
|
||||
profile := &ProcessingResourcePricingProfile{AccessPricingProfile: mockPricing}
|
||||
price, err := profile.GetPriceHT(1, 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())
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
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: true}}
|
||||
set := &MockDBObject{isDraft: false}
|
||||
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) SetAllowedInstances(req *tools.APIRequest, instance_id ...string) []resources.ResourceInstanceITF {
|
||||
return nil
|
||||
}
|
||||
func (f *FakeResource) ConvertToPricedResource(t tools.DataType, a *int, b *int, c *int, d *int, e *int, req *tools.APIRequest) (pricing.PricedItemITF, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *FakeResource) VerifyAuth(string, *tools.APIRequest) bool { return true }
|
||||
|
||||
func TestNewAccessor_ReturnsValid(t *testing.T) {
|
||||
acc := resources.NewAccessor[*FakeResource](tools.COMPUTE_RESOURCE, &tools.APIRequest{})
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package resources_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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 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[*resources.StorageResourcePricingProfile]{
|
||||
ResourceID: "res-id",
|
||||
},
|
||||
}
|
||||
_, err := res.GetPriceHT()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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_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)
|
||||
}
|
||||
Regular → Executable
+23
-10
@@ -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 (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) {
|
||||
}
|
||||
|
||||
func (r *WorkflowResource) GetType() string {
|
||||
@@ -27,20 +30,30 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *WorkflowResource) Trim() {
|
||||
/* EMPTY */
|
||||
}
|
||||
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest) {
|
||||
/* EMPTY */
|
||||
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
|
||||
// WorkflowResource has no instances, but still carries AEs that must be
|
||||
// filtered before the resource is returned to a non-owner, non-admin peer.
|
||||
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
if request != nil {
|
||||
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
return []ResourceInstanceITF{}
|
||||
}
|
||||
|
||||
func (w *WorkflowResource) ConvertToPricedResource(
|
||||
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
|
||||
return &PricedResource{
|
||||
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[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{
|
||||
Name: w.Name,
|
||||
Logo: w.Logo,
|
||||
ResourceID: w.UUID,
|
||||
ResourceType: t,
|
||||
Quantity: 1,
|
||||
CreatorID: w.CreatorID,
|
||||
}
|
||||
}, nil // TODO ???
|
||||
}
|
||||
|
||||
// TODO : as instanciated resource !
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Regular → Executable
+135
-12
@@ -1,7 +1,10 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
@@ -28,6 +31,7 @@ const (
|
||||
*/
|
||||
type AbstractObject struct {
|
||||
UUID string `json:"id,omitempty" bson:"id,omitempty" validate:"required"`
|
||||
NotInCatalog bool `json:"not_in_catalog" bson:"not_in_catalog" default:"false"`
|
||||
Name string `json:"name,omitempty" bson:"name,omitempty" validate:"required"`
|
||||
IsDraft bool `json:"is_draft" bson:"is_draft" default:"false"`
|
||||
CreatorID string `json:"creator_id,omitempty" bson:"creator_id,omitempty"`
|
||||
@@ -37,11 +41,66 @@ 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) Extend(typ ...string) map[string][]tools.DataType {
|
||||
dt := map[string][]tools.DataType{}
|
||||
for _, t := range typ {
|
||||
switch t {
|
||||
case "creator", "user_creator", "user_updater":
|
||||
if _, ok := dt[t]; !ok {
|
||||
dt[t] = []tools.DataType{}
|
||||
}
|
||||
dt[t] = append(dt[t], tools.PEER)
|
||||
}
|
||||
}
|
||||
return dt
|
||||
}
|
||||
|
||||
func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor {
|
||||
return nil
|
||||
}
|
||||
func (r *AbstractObject) SetNotInCatalog(ok bool) {
|
||||
r.NotInCatalog = ok
|
||||
}
|
||||
func (r *AbstractObject) IsNotInCatalog() bool {
|
||||
return r.NotInCatalog
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
@@ -70,6 +129,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 +146,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 +188,108 @@ 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]) NewObj() DBObject {
|
||||
return r.New()
|
||||
}
|
||||
|
||||
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 map[string]interface{}, id string) (DBObject, int, error) {
|
||||
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "UpdateOne") {
|
||||
return nil, 404, errors.New("not implemented")
|
||||
}
|
||||
// should verify if a source is existing...
|
||||
return GenericUpdateOne(set, id, a)
|
||||
}
|
||||
|
||||
func (a *AbstractAccessor[T]) StoreOne(data DBObject) (DBObject, int, error) {
|
||||
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "StoreOne") {
|
||||
return nil, 404, errors.New("not implemented")
|
||||
}
|
||||
return GenericStoreOne(data.(T), a)
|
||||
}
|
||||
|
||||
func (a *AbstractAccessor[T]) CopyOne(data DBObject) (DBObject, int, error) {
|
||||
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "CopyOne") {
|
||||
return nil, 404, errors.New("not implemented")
|
||||
}
|
||||
return GenericStoreOne(data.(T), a)
|
||||
}
|
||||
|
||||
func (a *AbstractAccessor[T]) LoadOne(id string) (DBObject, int, error) {
|
||||
return GenericLoadOne(id, a.New(), func(d DBObject) (DBObject, int, error) {
|
||||
return d, 200, nil
|
||||
}, a)
|
||||
}
|
||||
|
||||
func (a *AbstractAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error) {
|
||||
return GenericLoadAll[T](a.GetExec(isDraft), isDraft, a, offset, limit)
|
||||
}
|
||||
|
||||
func (a *AbstractAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error) {
|
||||
return GenericSearch[T](filters, search, a.New().GetObjectFilters(search), a.GetExec(isDraft), isDraft, a, offset, limit)
|
||||
}
|
||||
|
||||
func (a *AbstractAccessor[T]) GetExec(isDraft bool) func(DBObject) ShallowDBObject {
|
||||
return func(d DBObject) ShallowDBObject {
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
// ChangeEvent is fired whenever a DB object is created, updated or deleted
|
||||
// within this process. Deleted=true means the object was removed; Object is
|
||||
// the last known snapshot before deletion.
|
||||
type ChangeEvent struct {
|
||||
DataType tools.DataType
|
||||
ID string
|
||||
Object ShallowDBObject // nil only when the load after the write failed
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
var (
|
||||
changeBusMu sync.RWMutex
|
||||
changeBus = map[tools.DataType][]chan ChangeEvent{}
|
||||
)
|
||||
|
||||
// SubscribeChanges returns a channel that receives ChangeEvents for dt
|
||||
// whenever an object of that type is written or deleted in this process.
|
||||
// Call the returned cancel function to unsubscribe; after that the channel
|
||||
// will no longer receive events (it is not closed — use a context to stop
|
||||
// reading).
|
||||
func SubscribeChanges(dt tools.DataType) (<-chan ChangeEvent, func()) {
|
||||
ch := make(chan ChangeEvent, 32)
|
||||
changeBusMu.Lock()
|
||||
changeBus[dt] = append(changeBus[dt], ch)
|
||||
changeBusMu.Unlock()
|
||||
return ch, func() {
|
||||
changeBusMu.Lock()
|
||||
subs := changeBus[dt]
|
||||
for i, c := range subs {
|
||||
if c == ch {
|
||||
changeBus[dt] = append(subs[:i], subs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
changeBusMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyChange broadcasts a ChangeEvent to all current subscribers for dt.
|
||||
// Non-blocking: events are dropped for subscribers whose buffer is full.
|
||||
func NotifyChange(dt tools.DataType, id string, obj ShallowDBObject, deleted bool) {
|
||||
changeBusMu.RLock()
|
||||
subs := changeBus[dt]
|
||||
changeBusMu.RUnlock()
|
||||
evt := ChangeEvent{DataType: dt, ID: id, Object: obj, Deleted: deleted}
|
||||
for _, ch := range subs {
|
||||
select {
|
||||
case ch <- evt:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
Regular → Executable
+147
-41
@@ -1,10 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -14,11 +17,14 @@ type Owner struct {
|
||||
}
|
||||
|
||||
func VerifyAccess(a Accessor, id string) error {
|
||||
if a == nil {
|
||||
return errors.New("no accessor to verify access")
|
||||
}
|
||||
data, _, err := a.LoadOne(id)
|
||||
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 +32,13 @@ 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.StoreDraftDefault()
|
||||
data.UpToDate(a.GetUser(), a.GetPeerID(), true)
|
||||
data.Unsign()
|
||||
data.Sign()
|
||||
f := dbs.Filters{
|
||||
Or: map[string][]dbs.Filter{
|
||||
"abstractresource.abstractobject.name": {{
|
||||
@@ -41,82 +51,125 @@ 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 {
|
||||
if cursor, _, _ := a.Search(&f, "", data.IsDrafted(), 0, 10); len(cursor) > 0 {
|
||||
return nil, 409, errors.New(a.GetType().String() + " with name " + data.GetName() + " already exists")
|
||||
}
|
||||
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 {
|
||||
a.GetLogger().Error().Msg("Could not store " + data.GetName() + " to db. Error: " + err.Error())
|
||||
return nil, code, err
|
||||
}
|
||||
return a.LoadOne(id)
|
||||
result, rcode, rerr := a.LoadOne(id)
|
||||
if rerr == nil && result != nil {
|
||||
go NotifyChange(a.GetType(), result.GetID(), result, false)
|
||||
}
|
||||
return result, rcode, rerr
|
||||
}
|
||||
|
||||
// GenericLoadOne loads one object from the database (generic)
|
||||
func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
|
||||
res, code, err := a.LoadOne(id)
|
||||
if err != nil {
|
||||
return res, code, err
|
||||
}
|
||||
if res == nil {
|
||||
return res, code, errors.New("not found")
|
||||
}
|
||||
return GenericDelete(res, a)
|
||||
}
|
||||
|
||||
func GenericDelete(res DBObject, a Accessor) (DBObject, int, error) {
|
||||
if !res.CanDelete() {
|
||||
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
|
||||
}
|
||||
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())
|
||||
_, code, err := mongo.MONGOService.DeleteOne(res.GetID(), a.GetType().String())
|
||||
if err != nil {
|
||||
a.GetLogger().Error().Msg("Could not delete " + id + " to db. Error: " + err.Error())
|
||||
a.GetLogger().Error().Msg("Could not delete " + res.GetID() + " to db. Error: " + err.Error())
|
||||
return nil, code, err
|
||||
}
|
||||
go NotifyChange(a.GetType(), res.GetID(), res, true)
|
||||
return res, 200, nil
|
||||
}
|
||||
|
||||
func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, map[string]interface{}, int, error) {
|
||||
r, c, err := a.LoadOne(id)
|
||||
if err != nil {
|
||||
return nil, nil, c, err
|
||||
}
|
||||
obj := a.NewObj()
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, obj)
|
||||
if !a.GetRequest().Admin {
|
||||
var ok bool
|
||||
ok, r = r.CanUpdate(obj)
|
||||
if !ok {
|
||||
return nil, nil, 403, errors.New("you are not allowed to update :" + a.GetType().String())
|
||||
}
|
||||
if a.ShouldVerifyAuth() && !r.VerifyAuth("update", a.GetRequest()) {
|
||||
return nil, nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
|
||||
}
|
||||
}
|
||||
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
|
||||
if a.GetPeerID() == r.GetCreatorID() {
|
||||
r.Unsign()
|
||||
r.Sign()
|
||||
}
|
||||
|
||||
loaded := r.Serialize(r) // get the loaded object
|
||||
for k, v := range change { // apply the changes, with a flatten method
|
||||
loaded[k] = v
|
||||
}
|
||||
newObj := a.NewObj()
|
||||
b, err = json.Marshal(loaded)
|
||||
if err != nil {
|
||||
return nil, loaded, 400, nil
|
||||
}
|
||||
err = json.Unmarshal(b, newObj)
|
||||
if err != nil {
|
||||
return nil, loaded, 400, nil
|
||||
}
|
||||
return newObj, loaded, 200, nil
|
||||
}
|
||||
|
||||
// GenericLoadOne loads one object from the database (generic)
|
||||
// json expected in entry is a flatted object no need to respect the inheritance hierarchy
|
||||
func GenericUpdateOne(set DBObject, id string, a Accessor, new DBObject) (DBObject, int, error) {
|
||||
r, c, err := a.LoadOne(id)
|
||||
func GenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, int, error) {
|
||||
obj, loaded, c, err := ModelGenericUpdateOne(change, id, a)
|
||||
|
||||
if err != nil {
|
||||
return nil, c, err
|
||||
}
|
||||
ok, newSet := r.CanUpdate(set)
|
||||
if !ok {
|
||||
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
|
||||
}
|
||||
set = newSet
|
||||
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
|
||||
if a.ShouldVerifyAuth() && !r.VerifyAuth(a.GetRequest()) {
|
||||
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
|
||||
}
|
||||
change := set.Serialize(set) // get the changes
|
||||
loaded := r.Serialize(r) // get the loaded object
|
||||
|
||||
for k, v := range change { // apply the changes, with a flatten method
|
||||
loaded[k] = v
|
||||
}
|
||||
id, code, err := mongo.MONGOService.UpdateOne(new.Deserialize(loaded, new), id, a.GetType().String())
|
||||
id, code, err := mongo.MONGOService.UpdateOne(obj.Deserialize(loaded, obj), id, a.GetType().String())
|
||||
if err != nil {
|
||||
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
|
||||
return nil, code, err
|
||||
}
|
||||
return a.LoadOne(id)
|
||||
result, rcode, rerr := a.LoadOne(id)
|
||||
if rerr == nil && result != nil {
|
||||
go NotifyChange(a.GetType(), result.GetID(), result, false)
|
||||
}
|
||||
return result, rcode, rerr
|
||||
}
|
||||
|
||||
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 err = res_mongo.Decode(data); err != nil {
|
||||
return nil, 400, err
|
||||
}
|
||||
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 +185,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))
|
||||
@@ -140,17 +193,27 @@ func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft
|
||||
return objs, 200, nil
|
||||
}
|
||||
|
||||
func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor) ([]ShallowDBObject, int, error) {
|
||||
res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType().String())
|
||||
func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) {
|
||||
offset := int64(0)
|
||||
limit := int64(0)
|
||||
if len(opts) > 1 {
|
||||
offset = opts[0]
|
||||
}
|
||||
res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType().String(), offset, limit)
|
||||
return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
|
||||
}
|
||||
|
||||
func GenericSearch[T DBObject](filters *dbs.Filters, search string, defaultFilters *dbs.Filters,
|
||||
f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor) ([]ShallowDBObject, int, error) {
|
||||
f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) {
|
||||
if filters == nil && search != "" {
|
||||
filters = defaultFilters
|
||||
}
|
||||
res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType().String())
|
||||
offset := int64(0)
|
||||
limit := int64(0)
|
||||
if len(opts) > 1 {
|
||||
offset = opts[0]
|
||||
}
|
||||
res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType().String(), offset, limit)
|
||||
return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
|
||||
}
|
||||
|
||||
@@ -162,5 +225,48 @@ func GenericRawUpdateOne(set DBObject, id string, a Accessor) (DBObject, int, er
|
||||
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
|
||||
return nil, code, err
|
||||
}
|
||||
return a.LoadOne(id)
|
||||
result, rcode, rerr := a.LoadOne(id)
|
||||
if rerr == nil && result != nil {
|
||||
go NotifyChange(a.GetType(), result.GetID(), result, false)
|
||||
}
|
||||
return result, rcode, rerr
|
||||
}
|
||||
|
||||
func GetMySelf(wfa Accessor) (ShallowDBObject, error) {
|
||||
datas, _, _ := wfa.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"relation": {{Operator: dbs.EQUAL.String(), Value: 1}},
|
||||
},
|
||||
}, "", false, 0, 1)
|
||||
if len(datas) > 0 && datas[0] != nil {
|
||||
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
|
||||
}
|
||||
|
||||
Regular → Executable
+16
-4
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
// ShallowDBObject is an interface that defines the basic methods shallowed version of a DBObject
|
||||
type ShallowDBObject interface {
|
||||
DBObject
|
||||
GenerateID()
|
||||
GetID() string
|
||||
GetName() string
|
||||
@@ -18,22 +19,32 @@ type ShallowDBObject interface {
|
||||
// DBObject is an interface that defines the basic methods for a DBObject
|
||||
type DBObject interface {
|
||||
GenerateID()
|
||||
Extend(typ ...string) map[string][]tools.DataType
|
||||
SetNotInCatalog(bool)
|
||||
IsNotInCatalog() bool
|
||||
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
|
||||
type Accessor interface {
|
||||
NewObj() DBObject
|
||||
GetUser() string
|
||||
GetPeerID() string
|
||||
GetGroups() []string
|
||||
@@ -46,7 +57,8 @@ type Accessor interface {
|
||||
DeleteOne(id string) (DBObject, int, error)
|
||||
CopyOne(data DBObject) (DBObject, int, error)
|
||||
StoreOne(data DBObject) (DBObject, int, error)
|
||||
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)
|
||||
LoadAll(isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error)
|
||||
UpdateOne(set map[string]interface{}, id string) (DBObject, int, error)
|
||||
Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error)
|
||||
GetExec(isDraft bool) func(DBObject) ShallowDBObject
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
+100
-36
@@ -1,8 +1,6 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
@@ -15,6 +13,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 +45,14 @@ func (wf *Graph) IsProcessing(item GraphItem) bool {
|
||||
return item.Processing != nil
|
||||
}
|
||||
|
||||
func (wf *Graph) IsService(item GraphItem) bool {
|
||||
return item.Service != nil
|
||||
}
|
||||
|
||||
func (wf *Graph) IsNativeTool(item GraphItem) bool {
|
||||
return item.NativeTool != nil
|
||||
}
|
||||
|
||||
func (wf *Graph) IsCompute(item GraphItem) bool {
|
||||
return item.Compute != nil
|
||||
}
|
||||
@@ -54,49 +69,43 @@ func (wf *Graph) IsWorkflow(item GraphItem) bool {
|
||||
return item.Workflow != nil
|
||||
}
|
||||
|
||||
func (g *Graph) GetAverageTimeRelatedToProcessingActivity(start time.Time, processings []*resources.ProcessingResource, resource resources.ResourceInterface,
|
||||
f func(GraphItem) resources.ResourceInterface, request *tools.APIRequest) (float64, float64) {
|
||||
nearestStart := float64(10000000000)
|
||||
func (wf *Graph) IsDynamic(item GraphItem) bool {
|
||||
return item.Dynamic != nil
|
||||
}
|
||||
|
||||
func (g *Graph) GetAverageTimeRelatedToProcessingActivity(processings []*resources.ProcessingResource, resource resources.ResourceInterface,
|
||||
f func(GraphItem) resources.ResourceInterface, instance int, partnership int, buying int, strategy int, bookingMode int, request *tools.APIRequest) (float64, float64, error) {
|
||||
oneIsInfinite := false
|
||||
longestDuration := float64(0)
|
||||
for _, link := range g.Links {
|
||||
for _, processing := range processings {
|
||||
var source string // source is the source of the link
|
||||
if link.Destination.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID() { // if the destination is the processing and the source is not a compute
|
||||
source = link.Source.ID
|
||||
} 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
|
||||
if !(link.Destination.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID()) &&
|
||||
!(link.Source.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID()) {
|
||||
continue
|
||||
}
|
||||
priced := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, request)
|
||||
if source != "" {
|
||||
if priced.GetLocationStart() != nil {
|
||||
near := float64(priced.GetLocationStart().Sub(start).Seconds())
|
||||
if near < nearestStart {
|
||||
nearestStart = near
|
||||
}
|
||||
|
||||
}
|
||||
if priced.GetLocationEnd() != nil {
|
||||
duration := float64(priced.GetLocationEnd().Sub(*priced.GetLocationStart()).Seconds())
|
||||
if longestDuration < duration {
|
||||
longestDuration = duration
|
||||
}
|
||||
} else {
|
||||
oneIsInfinite = true
|
||||
}
|
||||
priced, err := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, &instance, &partnership, &buying, &strategy, &bookingMode, request)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
duration := priced.GetExplicitDurationInS()
|
||||
if duration < 0 {
|
||||
oneIsInfinite = true
|
||||
} else if longestDuration < duration {
|
||||
longestDuration = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
if oneIsInfinite {
|
||||
return nearestStart, -1
|
||||
return 0, -1, nil
|
||||
}
|
||||
return nearestStart, longestDuration
|
||||
return 0, 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 +121,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 +142,58 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
|
||||
max = current
|
||||
}
|
||||
}
|
||||
return max
|
||||
return max, nil
|
||||
}
|
||||
|
||||
// DataStorageLink represents a resolved Data→Storage pair found in the graph.
|
||||
type DataStorageLink struct {
|
||||
DataItemID string
|
||||
StorageItemID string
|
||||
}
|
||||
|
||||
// GetDataStorageLinks returns all links that connect a Data item to a Storage item.
|
||||
// These links are mandatory when the Data instance has a Source configured:
|
||||
// the workflow builder uses them to know where to download the data before
|
||||
// any processing step that consumes that storage.
|
||||
func (g *Graph) GetDataStorageLinks() []DataStorageLink {
|
||||
var result []DataStorageLink
|
||||
for _, link := range g.Links {
|
||||
srcItem, srcOk := g.Items[link.Source.ID]
|
||||
dstItem, dstOk := g.Items[link.Destination.ID]
|
||||
if !srcOk || !dstOk {
|
||||
continue
|
||||
}
|
||||
if g.IsData(srcItem) && g.IsStorage(dstItem) {
|
||||
result = append(result, DataStorageLink{
|
||||
DataItemID: link.Source.ID,
|
||||
StorageItemID: link.Destination.ID,
|
||||
})
|
||||
} else if g.IsStorage(srcItem) && g.IsData(dstItem) {
|
||||
result = append(result, DataStorageLink{
|
||||
DataItemID: link.Destination.ID,
|
||||
StorageItemID: link.Source.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLinkedStorageForData returns the storage item IDs linked to a given Data item.
|
||||
func (g *Graph) GetLinkedStorageForData(dataItemID string) []string {
|
||||
var storageIDs []string
|
||||
for _, dsl := range g.GetDataStorageLinks() {
|
||||
if dsl.DataItemID == dataItemID {
|
||||
storageIDs = append(storageIDs, dsl.StorageItemID)
|
||||
}
|
||||
}
|
||||
return storageIDs
|
||||
}
|
||||
|
||||
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
|
||||
if item, ok := g.Items[id]; ok {
|
||||
if item.Data != nil {
|
||||
if item.NativeTool != 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
|
||||
@@ -141,6 +203,8 @@ func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterf
|
||||
return tools.PROCESSING_RESOURCE, item.Processing
|
||||
} else if item.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, item.Storage
|
||||
} else if item.Service != nil {
|
||||
return tools.SERVICE_RESOURCE, item.Service
|
||||
}
|
||||
}
|
||||
return tools.INVALID, nil
|
||||
|
||||
@@ -15,24 +15,32 @@ type GraphItem struct {
|
||||
}
|
||||
|
||||
func (g *GraphItem) GetResource() (tools.DataType, resources.ResourceInterface) {
|
||||
if g.Data != nil {
|
||||
return tools.DATA_RESOURCE, g.Data
|
||||
} else if g.Compute != nil {
|
||||
return tools.COMPUTE_RESOURCE, g.Compute
|
||||
} else if g.Workflow != nil {
|
||||
return tools.WORKFLOW_RESOURCE, g.Workflow
|
||||
} else if g.Processing != nil {
|
||||
return tools.PROCESSING_RESOURCE, g.Processing
|
||||
} else if g.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, g.Storage
|
||||
if g.ItemResource.Data != nil {
|
||||
return tools.DATA_RESOURCE, g.ItemResource.Data
|
||||
} else if g.ItemResource.Compute != nil {
|
||||
return tools.COMPUTE_RESOURCE, g.ItemResource.Compute
|
||||
} else if g.ItemResource.Workflow != nil {
|
||||
return tools.WORKFLOW_RESOURCE, g.ItemResource.Workflow
|
||||
} else if g.ItemResource.Processing != nil {
|
||||
return tools.PROCESSING_RESOURCE, g.ItemResource.Processing
|
||||
} else if g.ItemResource.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, g.ItemResource.Storage
|
||||
} else if g.ItemResource.NativeTool != nil {
|
||||
return tools.NATIVE_TOOL, g.ItemResource.NativeTool
|
||||
} else if g.ItemResource.Service != nil {
|
||||
return tools.SERVICE_RESOURCE, g.ItemResource.Service
|
||||
} else if g.ItemResource.Dynamic != nil {
|
||||
return tools.DYNAMIC_RESOURCE, g.ItemResource.Dynamic
|
||||
}
|
||||
return tools.INVALID, nil
|
||||
}
|
||||
|
||||
func (g *GraphItem) Clear() {
|
||||
g.Data = nil
|
||||
g.Compute = nil
|
||||
g.Workflow = nil
|
||||
g.Processing = nil
|
||||
g.Storage = nil
|
||||
g.ItemResource.Data = nil
|
||||
g.ItemResource.Compute = nil
|
||||
g.ItemResource.Workflow = nil
|
||||
g.ItemResource.Processing = nil
|
||||
g.ItemResource.Storage = nil
|
||||
g.ItemResource.Service = nil
|
||||
g.ItemResource.Dynamic = nil
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ func (l *GraphLink) IsComputeLink(g Graph) (bool, string) {
|
||||
if g.Items == nil {
|
||||
return false, ""
|
||||
}
|
||||
if d, ok := g.Items[l.Source.ID]; ok && d.Compute != nil {
|
||||
return true, d.Compute.UUID
|
||||
if d, ok := g.Items[l.Source.ID]; ok && d.ItemResource.Compute != nil {
|
||||
return true, d.ItemResource.Compute.UUID
|
||||
}
|
||||
if d, ok := g.Items[l.Destination.ID]; ok && d.Compute != nil {
|
||||
return true, d.Compute.UUID
|
||||
if d, ok := g.Items[l.Destination.ID]; ok && d.ItemResource.Compute != nil {
|
||||
return true, d.ItemResource.Compute.UUID
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PlantUML export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// plantUMLProcedures defines !procedure blocks for each resource type.
|
||||
// Parameters use the $var/$name convention of PlantUML preprocessor v2.
|
||||
// Calls are written WITHOUT inline comments (comment on the following line)
|
||||
// to avoid the "assumed sequence diagram" syntax error.
|
||||
const plantUMLProcedures = `!procedure Processing($var, $name)
|
||||
component "$name" as $var <<Processing>>
|
||||
!endprocedure
|
||||
|
||||
!procedure Data($var, $name)
|
||||
file "$name" as $var <<Data>>
|
||||
!endprocedure
|
||||
|
||||
!procedure Storage($var, $name)
|
||||
database "$name" as $var <<Storage>>
|
||||
!endprocedure
|
||||
|
||||
!procedure ComputeUnit($var, $name)
|
||||
node "$name" as $var <<ComputeUnit>>
|
||||
!endprocedure
|
||||
|
||||
!procedure WorkflowEvent($var, $name)
|
||||
usecase "$name" as $var <<WorkflowEvent>>
|
||||
!endprocedure
|
||||
|
||||
!procedure Workflow($var, $name)
|
||||
frame "$name" as $var <<Workflow>>
|
||||
!endprocedure
|
||||
`
|
||||
|
||||
// ToPlantUML serializes the workflow graph to a valid, renderable PlantUML file
|
||||
// that is also compatible with ExtractFromPlantUML (round-trip).
|
||||
// Resource and instance attributes are written as human-readable comments:
|
||||
//
|
||||
// Processing(p1, "NDVI") ' access.container.image: myrepo/ndvi:1.2, infrastructure: 0
|
||||
func (w *Workflow) ToPlantUML() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("@startuml\n\n")
|
||||
sb.WriteString(plantUMLProcedures)
|
||||
sb.WriteByte('\n')
|
||||
|
||||
varNames := plantUMLVarNames(w.Graph.Items)
|
||||
|
||||
// --- resource declarations ---
|
||||
for id, item := range w.Graph.Items {
|
||||
if line := plantUMLItemLine(varNames[id], item); line != "" {
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteByte('\n')
|
||||
|
||||
// --- links ---
|
||||
for _, link := range w.Graph.Links {
|
||||
src := varNames[link.Source.ID]
|
||||
dst := varNames[link.Destination.ID]
|
||||
if src == "" || dst == "" {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s --> %s\n", src, dst))
|
||||
if comment := plantUMLLinkComment(link); comment != "" {
|
||||
sb.WriteString("' " + comment + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n@enduml\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// plantUMLVarNames assigns short, deterministic variable names to each graph
|
||||
// item (d1, d2, p1, s1, c1, e1, wf1 …).
|
||||
func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
|
||||
counters := map[string]int{}
|
||||
varNames := map[string]string{}
|
||||
// Sort IDs for deterministic output
|
||||
ids := make([]string, 0, len(items))
|
||||
for id := range items {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
for _, id := range ids {
|
||||
prefix := plantUMLPrefix(items[id])
|
||||
counters[prefix]++
|
||||
varNames[id] = fmt.Sprintf("%s%d", prefix, counters[prefix])
|
||||
}
|
||||
return varNames
|
||||
}
|
||||
|
||||
func plantUMLPrefix(item graph.GraphItem) string {
|
||||
switch {
|
||||
case item.ItemResource.NativeTool != nil:
|
||||
return "e"
|
||||
case item.ItemResource.Data != nil:
|
||||
return "d"
|
||||
case item.ItemResource.Processing != nil:
|
||||
return "p"
|
||||
case item.ItemResource.Storage != nil:
|
||||
return "s"
|
||||
case item.ItemResource.Compute != nil:
|
||||
return "c"
|
||||
case item.ItemResource.Workflow != nil:
|
||||
return "wf"
|
||||
}
|
||||
return "u"
|
||||
}
|
||||
|
||||
// plantUMLItemLine builds the PlantUML declaration line for one graph item.
|
||||
func plantUMLItemLine(varName string, item graph.GraphItem) string {
|
||||
switch {
|
||||
case item.ItemResource.NativeTool != nil:
|
||||
// WorkflowEvent has no instance and no configurable attributes.
|
||||
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.ItemResource.NativeTool.GetName())
|
||||
|
||||
case item.ItemResource.Data != nil:
|
||||
return plantUMLResourceLine("Data", varName, item.ItemResource.Data)
|
||||
|
||||
case item.ItemResource.Processing != nil:
|
||||
return plantUMLResourceLine("Processing", varName, item.ItemResource.Processing)
|
||||
|
||||
case item.ItemResource.Storage != nil:
|
||||
return plantUMLResourceLine("Storage", varName, item.ItemResource.Storage)
|
||||
|
||||
case item.ItemResource.Compute != nil:
|
||||
return plantUMLResourceLine("ComputeUnit", varName, item.ItemResource.Compute)
|
||||
|
||||
case item.ItemResource.Workflow != nil:
|
||||
return plantUMLResourceLine("Workflow", varName, item.ItemResource.Workflow)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func plantUMLResourceLine(macro, varName string, res resources.ResourceInterface) string {
|
||||
decl := fmt.Sprintf("%s(%s, \"%s\")", macro, varName, res.GetName())
|
||||
if comment := plantUMLResourceComment(res); comment != "" {
|
||||
// Comment on the line AFTER the declaration. ExtractFromPlantUML uses
|
||||
// look-ahead to merge it back. No inline comment = no !procedure conflict.
|
||||
return decl + "\n' " + comment
|
||||
}
|
||||
return decl
|
||||
}
|
||||
|
||||
// plantUMLResourceComment merges resource-level fields with the first instance
|
||||
// fields (instance overrides resource) and formats them as human-readable pairs.
|
||||
func plantUMLResourceComment(res resources.ResourceInterface) string {
|
||||
m := plantUMLToFlatMap(res)
|
||||
if inst := res.GetSelectedInstance(nil); inst != nil {
|
||||
for k, v := range plantUMLToFlatMap(inst) {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
return plantUMLFlatMapToComment(m)
|
||||
}
|
||||
|
||||
// plantUMLLinkComment serializes StorageLinkInfos (first entry) as flat
|
||||
// human-readable pairs prefixed with "storage_link_infos.".
|
||||
func plantUMLLinkComment(link graph.GraphLink) string {
|
||||
if len(link.StorageLinkInfos) == 0 {
|
||||
return ""
|
||||
}
|
||||
infoFlat := plantUMLToFlatMap(link.StorageLinkInfos[0])
|
||||
prefixed := make(map[string]string, len(infoFlat))
|
||||
for k, v := range infoFlat {
|
||||
prefixed["storage_link_infos."+k] = v
|
||||
}
|
||||
return plantUMLFlatMapToComment(prefixed)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flat-map helpers (shared by import & export)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// plantUMLSkipFields lists JSON field names (root keys) that must never appear
|
||||
// in human-readable comments. All names are the actual JSON tags, not Go field names.
|
||||
var plantUMLSkipFields = map[string]bool{
|
||||
// AbstractObject — identity & audit (json tags)
|
||||
"id": true, "name": true, "is_draft": true, "access_mode": true, "signature": true,
|
||||
"creator_id": true, "user_creator_id": true,
|
||||
"creation_date": true, "update_date": true,
|
||||
"updater_id": true, "user_updater_id": true,
|
||||
// internal resource type identifier (AbstractResource.Type / GetType())
|
||||
"type": true,
|
||||
// relationships / pricing
|
||||
"instances": true, "partnerships": true,
|
||||
"allowed_booking_modes": true, "usage_restrictions": true,
|
||||
// display / admin
|
||||
"logo": true, "description": true, "short_description": true, "owners": true,
|
||||
// runtime params
|
||||
"env": true, "inputs": true, "outputs": true,
|
||||
// NativeTool internals
|
||||
"kind": true, "params": true,
|
||||
}
|
||||
|
||||
// zeroTimeStr is the JSON representation of Go's zero time.Time value.
|
||||
// encoding/json does not treat it as "empty" for omitempty, so we filter it explicitly.
|
||||
const zeroTimeStr = "0001-01-01T00:00:00Z"
|
||||
|
||||
// plantUMLToFlatMap marshals v to JSON and flattens the resulting object into
|
||||
// a map[string]string using dot notation for nested keys, skipping zero values
|
||||
// and known meta fields.
|
||||
func plantUMLToFlatMap(v interface{}) map[string]string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return nil
|
||||
}
|
||||
result := map[string]string{}
|
||||
plantUMLFlattenJSON(raw, "", result)
|
||||
return result
|
||||
}
|
||||
|
||||
// plantUMLFlattenJSON recursively walks a JSON object and writes scalar leaf
|
||||
// values into result using dot-notation keys.
|
||||
func plantUMLFlattenJSON(m map[string]interface{}, prefix string, result map[string]string) {
|
||||
for k, v := range m {
|
||||
fullKey := k
|
||||
if prefix != "" {
|
||||
fullKey = prefix + "." + k
|
||||
}
|
||||
// Skip fields whose root key is in the deny-list
|
||||
if plantUMLSkipFields[strings.SplitN(fullKey, ".", 2)[0]] {
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
plantUMLFlattenJSON(val, fullKey, result)
|
||||
case []interface{}:
|
||||
// Arrays are not representable in flat human-readable format; skip.
|
||||
case float64:
|
||||
if val != 0 {
|
||||
if val == float64(int64(val)) {
|
||||
result[fullKey] = strconv.FormatInt(int64(val), 10)
|
||||
} else {
|
||||
result[fullKey] = strconv.FormatFloat(val, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
case bool:
|
||||
if val {
|
||||
result[fullKey] = "true"
|
||||
}
|
||||
case string:
|
||||
if val != "" && val != zeroTimeStr {
|
||||
result[fullKey] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// plantUMLFlatMapToComment converts a flat map to a sorted "key: value, …" string.
|
||||
func plantUMLFlatMapToComment(m map[string]string) string {
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+": "+m[k])
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
+1175
-57
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user