4 Commits

Author SHA1 Message Date
pb 778ffa05a1 what is collaborative 2025-06-02 18:05:08 +02:00
pb 3c15907427 added id for logger 2025-06-02 10:34:58 +02:00
pb 9ae5f3b91d timing status checks 2025-05-28 18:19:07 +02:00
pb 3a2141aab5 adding log to measure search time 2025-05-28 16:22:26 +02:00
126 changed files with 2149 additions and 13846 deletions
-3
View File
@@ -1,3 +0,0 @@
# Force Go as the main language
*.go linguist-detectable=true
* linguist-language=Go
-190
View File
@@ -1,190 +0,0 @@
# 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 |
+1 -32
View File
@@ -9,10 +9,6 @@ import "sync"
// =================================================== // ===================================================
type Config struct { type Config struct {
IsApi bool
IsNano bool
APIPort int
NATSUrl string NATSUrl string
MongoUrl string MongoUrl string
MongoDatabase string MongoDatabase string
@@ -21,16 +17,6 @@ type Config struct {
LokiUrl string LokiUrl string
LogLevel string LogLevel string
Whitelist bool 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 { func (c Config) GetUrl() string {
@@ -51,29 +37,12 @@ func GetConfig() *Config {
return instance return instance
} }
func SetConfig(isNano bool, isAPI bool, mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int, func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *Config {
pkPath, ppPath,
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI, internalWorkspaceAPI,
internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *Config {
GetConfig().IsNano = isNano
GetConfig().IsApi = isAPI
GetConfig().MongoUrl = mongoUrl GetConfig().MongoUrl = mongoUrl
GetConfig().MongoDatabase = database GetConfig().MongoDatabase = database
GetConfig().NATSUrl = natsUrl GetConfig().NATSUrl = natsUrl
GetConfig().LokiUrl = lokiUrl GetConfig().LokiUrl = lokiUrl
GetConfig().LogLevel = logLevel GetConfig().LogLevel = logLevel
GetConfig().Whitelist = true 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() return GetConfig()
} }
+4 -3
View File
@@ -23,11 +23,12 @@ import (
* The configuration loader will give priority to the local file over the default file * The configuration loader will give priority to the local file over the default file
*/ */
func GetConfLoader(appName string) *onion.Onion { func GetConfLoader() *onion.Onion {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger() logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
AppName := GetAppName()
EnvPrefix := "OC_" EnvPrefix := "OC_"
defaultConfigFile := "/etc/oc/" + appName[3:] + ".json" defaultConfigFile := "/etc/oc/" + AppName[3:] + ".json"
localConfigFile := "./" + appName[3:] + ".json" localConfigFile := "./" + AppName[3:] + ".json"
var configFile string var configFile string
var o *onion.Onion var o *onion.Onion
l3 := GetEnvVarLayer(EnvPrefix) l3 := GetEnvVarLayer(EnvPrefix)
+86 -233
View File
@@ -2,9 +2,6 @@ package dbs
import ( import (
"fmt" "fmt"
"reflect"
"regexp"
"runtime/debug"
"strings" "strings"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
@@ -22,8 +19,6 @@ const (
GT GT
EQUAL EQUAL
NOT NOT
ELEMMATCH
OR
) )
var str = [...]string{ var str = [...]string{
@@ -36,44 +31,113 @@ var str = [...]string{
"gt", "gt",
"equal", "equal",
"not", "not",
"elemMatch",
"or",
} }
func (m Operator) String() string { func (m Operator) String() string {
return str[m] 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 { func (m Operator) ToMongoOperator(k string, value interface{}) bson.M {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r, debug.Stack()) fmt.Println("Recovered. Error:\n", r)
} }
}() }()
defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}} defaultValue := bson.M{k: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
switch m { switch m {
case LIKE: case LIKE:
return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
case EXISTS: case EXISTS:
return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$exists": ToValueOperator(StringToOperator(m.String()), value)}}
case IN: case IN:
return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$in": ToValueOperator(StringToOperator(m.String()), value)}}
case GTE: case GTE:
return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$gte": ToValueOperator(StringToOperator(m.String()), value)}}
case GT: case GT:
return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$gt": ToValueOperator(StringToOperator(m.String()), value)}}
case LTE: case LTE:
return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$lte": ToValueOperator(StringToOperator(m.String()), value)}}
case LT: case LT:
return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value, false)}} return bson.M{k: bson.M{"$lt": ToValueOperator(StringToOperator(m.String()), value)}}
case ELEMMATCH:
return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case EQUAL: case EQUAL:
return bson.M{k: value} return bson.M{k: value}
case NOT: case NOT:
return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value, false)} v := value.(Filters)
case OR: orList := bson.A{}
return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value, true)} 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}
default: default:
return defaultValue return defaultValue
} }
@@ -88,55 +152,13 @@ func StringToOperator(s string) Operator {
return LIKE return LIKE
} }
func GetBson(filters *Filters) bson.D { func ToValueOperator(operator Operator, value interface{}) interface{} {
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}}
}
}
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)) == "*" { if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
value = "" value = ""
} }
if operator == LIKE { if operator == LIKE {
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*" return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
} }
}
return value return value
} }
@@ -150,175 +172,6 @@ type Filter struct {
Value interface{} `json:"value,omitempty"` 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 {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Panic recovered FiltersFromFlatMap: %v\n", r)
}
}()
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{})
fmt.Println(jsonKey, val, bsonKey)
if !ok {
continue
}
for _, item := range val.([]interface{}) {
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"). Their JSON
// fields are promoted (flat), so bsonPrefix advances but jsonPrefix does not.
//
// For fields inside slices or maps, both the leaf json name and the full dotted
// json path (e.g. "instances.access_protocol") are registered as keys so callers
// can use either form unambiguously.
func jsonToBsonPaths(t reflect.Type, bsonPrefix string, jsonPrefix string) map[string]string {
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
t = t.Elem()
}
if t.Kind() == reflect.Map {
t = t.Elem()
for t.Kind() == reflect.Ptr {
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.
// JSON fields are promoted so jsonPrefix stays the same.
if field.Anonymous && jsonName == "" && bsonName == "" {
ft := field.Type
for ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct {
embedBsonPrefix := strings.ToLower(ft.Name())
re := regexp.MustCompile(`\[[^\]]*\]`)
embedBsonPrefix = re.ReplaceAllString(embedBsonPrefix, "")
embedBsonPrefix = strings.ReplaceAll(embedBsonPrefix, "*", "")
if bsonPrefix != "" {
embedBsonPrefix = bsonPrefix + "." + embedBsonPrefix
}
for k, v := range jsonToBsonPaths(ft, embedBsonPrefix, jsonPrefix) {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}
continue
}
if jsonName == "" || jsonName == "-" {
continue
}
if bsonName == "" || bsonName == "-" {
bsonName = jsonName
}
fullBsonPath := bsonName
if bsonPrefix != "" {
fullBsonPath = bsonPrefix + "." + bsonName
}
fullJsonPath := jsonName
if jsonPrefix != "" {
fullJsonPath = jsonPrefix + "." + jsonName
}
result[jsonName] = fullBsonPath
// Also register the full dotted JSON path so callers can use
// "instances.access_protocol" instead of just "access_protocol".
if fullJsonPath != jsonName {
if _, exists := result[fullJsonPath]; !exists {
result[fullJsonPath] = fullBsonPath
}
}
ft := field.Type
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
ft = ft.Elem()
}
if ft.Kind() == reflect.Map {
ft = ft.Elem()
for ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
}
if ft.Kind() == reflect.Struct {
for k, v := range jsonToBsonPaths(ft, fullBsonPath, fullJsonPath) {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}
}
return result
}
type Input = map[string]interface{} type Input = map[string]interface{}
func InputToBson(i Input, isUpdate bool) bson.D { func InputToBson(i Input, isUpdate bool) bson.D {
+32 -19
View File
@@ -247,7 +247,10 @@ func (m *MongoDB) StoreOne(obj interface{}, id string, collection_name string) (
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil { if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return "", 503, err return "", 503, err
} }
doc := map[string]interface{}{"_id": id} var doc map[string]interface{}
b, _ := bson.Marshal(obj)
bson.Unmarshal(b, &doc)
doc["_id"] = id
targetDBCollection := CollectionMap[collection_name] targetDBCollection := CollectionMap[collection_name]
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel() //defer cancel()
@@ -258,7 +261,7 @@ func (m *MongoDB) StoreOne(obj interface{}, id string, collection_name string) (
return "", 409, err return "", 409, err
} }
return m.UpdateOne(obj, id, collection_name) return id, 200, nil
} }
func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResult, int, error) { func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResult, int, error) {
@@ -267,9 +270,6 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
} }
filter := bson.M{"_id": id} filter := bson.M{"_id": id}
targetDBCollection := CollectionMap[collection_name] targetDBCollection := CollectionMap[collection_name]
if targetDBCollection == nil {
return nil, 503, errors.New("collection " + collection_name + " not initialized")
}
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel() //defer cancel()
@@ -282,20 +282,36 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
return res, 200, nil return res, 200, nil
} }
func (m *MongoDB) Search(filters *dbs.Filters, collection_name string, offset int64, limit int64) (*mongo.Cursor, int, error) { func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.Cursor, int, error) {
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil { if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return nil, 503, err return nil, 503, err
} }
opts := options.Find() opts := options.Find()
opts.SetLimit(1000)
targetDBCollection := CollectionMap[collection_name] targetDBCollection := CollectionMap[collection_name]
if targetDBCollection == nil { orList := bson.A{}
return nil, 503, errors.New("collection " + collection_name + " not initialized") andList := bson.A{}
f := bson.D{}
if filters != nil {
for k, filter := range filters.Or {
for _, ff := range filter {
orList = append(orList, dbs.StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range filters.And {
for _, ff := range filter {
andList = append(andList, dbs.StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
} }
f := dbs.GetBson(filters)
opts.SetSkip(offset) // OFFSET
opts.SetLimit(limit) // LIMIT
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel() // defer cancel()
@@ -331,8 +347,7 @@ func (m *MongoDB) LoadFilter(filter map[string]interface{}, collection_name stri
return res, 200, nil return res, 200, nil
} }
func (m *MongoDB) LoadAll(collection_name string, offset int64, limit int64) (*mongo.Cursor, int, error) { func (m *MongoDB) LoadAll(collection_name string) (*mongo.Cursor, int, error) {
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil { if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return nil, 503, err return nil, 503, err
} }
@@ -340,10 +355,8 @@ func (m *MongoDB) LoadAll(collection_name string, offset int64, limit int64) (*m
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel() //defer cancel()
findOptions := options.Find()
findOptions.SetSkip(offset) // OFFSET res, err := targetDBCollection.Find(MngoCtx, bson.D{})
findOptions.SetLimit(limit) // LIMIT
res, err := targetDBCollection.Find(MngoCtx, bson.D{}, findOptions)
if err != nil { if err != nil {
// m.Logger.Error().Msg("Couldn't find any resources. Error : " + err.Error()) // m.Logger.Error().Msg("Couldn't find any resources. Error : " + err.Error())
return nil, 404, err return nil, 404, err
+2 -2
View File
@@ -194,7 +194,7 @@ AccessPricingProfile ^-- ProcessingResourcePricingProfile
ExploitPricingProfile ^-- ComputeResourcePricingProfile ExploitPricingProfile ^-- ComputeResourcePricingProfile
ExploitPricingProfile ^-- StorageResourcePricingProfile ExploitPricingProfile ^-- StorageResourcePricingProfile
interface PricingProfileITF { interface PricingProfileITF {
GetPriceHT(quantity float64, val float64, start date, end date, request) float64 GetPrice(quantity float64, val float64, start date, end date, request) float64
IsPurchased() bool IsPurchased() bool
} }
class AccessPricingProfile { class AccessPricingProfile {
@@ -319,7 +319,7 @@ Workflow "1 " --* "many " ExploitResourceSet
class Workflow {} class Workflow {}
interface PricedItemITF { interface PricedItemITF {
GetPriceHT(request) float64, error getPrice(request) float64, error
} }
@enduml @enduml
-214
View File
@@ -1,214 +0,0 @@
# 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 |
+114 -433
View File
@@ -6,8 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os"
"slices"
"strings" "strings"
"runtime/debug" "runtime/debug"
@@ -17,19 +15,11 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs/mongo" "cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models" "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"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule" "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/order"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources" "cloud.o-forge.io/core/oc-lib/models/resources"
"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/utils"
w2 "cloud.o-forge.io/core/oc-lib/models/workflow" w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution" "cloud.o-forge.io/core/oc-lib/models/workflow_execution"
@@ -37,10 +27,7 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
beego "github.com/beego/beego/v2/server/web" beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context" "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/goraz/onion"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@@ -64,60 +51,11 @@ const (
RULE = tools.RULE RULE = tools.RULE
BOOKING = tools.BOOKING BOOKING = tools.BOOKING
ORDER = tools.ORDER 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
POLICY = tools.POLICY
) )
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 // will turn into standards api hostnames
func (d LibDataEnum) API() string { func (d LibDataEnum) API() string {
return tools.Str[d] return tools.DefaultAPI[d]
} }
// will turn into standards name // will turn into standards name
@@ -159,42 +97,25 @@ type LibData struct {
} }
func InitDaemon(appName string) { func InitDaemon(appName string) {
beego.BConfig.AppName = appName
config.SetAppName(appName) // set the app name to the logger to define the main log chan config.SetAppName(appName) // set the app name to the logger to define the main log chan
// create a temporary console logger for init // create a temporary console logger for init
logs.SetLogger(logs.CreateLogger("main")) logs.SetLogger(logs.CreateLogger("main"))
// Load the right config file // Load the right config file
o := GetConfLoader(appName) o := GetConfLoader()
// resources.InitNative()
// feed the library with the loaded config // feed the library with the loaded config
SetConfig( SetConfig(
o.GetBoolDefault("IS_NANO", false),
o.GetBoolDefault("IS_API", true),
o.GetStringDefault("MONGO_URL", "mongodb://127.0.0.1:27017"), o.GetStringDefault("MONGO_URL", "mongodb://127.0.0.1:27017"),
o.GetStringDefault("MONGO_DATABASE", "DC_myDC"), o.GetStringDefault("MONGO_DATABASE", "DC_myDC"),
o.GetStringDefault("NATS_URL", "nats://localhost:4222"), o.GetStringDefault("NATS_URL", "nats://localhost:4222"),
o.GetStringDefault("LOKI_URL", ""), o.GetStringDefault("LOKI_URL", ""),
o.GetStringDefault("LOG_LEVEL", "info"), 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"),
) )
if config.GetConfig().IsApi {
// Beego init // Beego init
beego.BConfig.AppName = appName beego.BConfig.AppName = appName
beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080) beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080)
beego.BConfig.WebConfig.DirectoryIndex = true beego.BConfig.WebConfig.DirectoryIndex = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger" beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
}
} }
type IDTokenClaims struct { type IDTokenClaims struct {
@@ -214,53 +135,6 @@ type Claims struct {
Session SessionClaims `json:"session"` 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) { func ExtractTokenInfo(request http.Request) (string, string, []string) {
reqToken := request.Header.Get("Authorization") reqToken := request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ") splitToken := strings.Split(reqToken, "Bearer ")
@@ -270,57 +144,27 @@ func ExtractTokenInfo(request http.Request) (string, string, []string) {
reqToken = splitToken[1] reqToken = splitToken[1]
} }
if reqToken != "" { if reqToken != "" {
return extractFromToken(reqToken, "user_id"), extractFromToken(reqToken, "peer_id"), strings.Split(extractFromToken(reqToken, "groups"), ",") 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 "", "", []string{} return "", "", []string{}
} }
func extractFromToken(token string, attr string) string { func Init(appName 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) InitDaemon(appName)
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 := &tools.API{}
api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo(), extraRoutes...) api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo())
}
} }
// //
@@ -342,12 +186,8 @@ func GetLogger() zerolog.Logger {
* @param logLevel string * @param logLevel string
* @return *Config * @return *Config
*/ */
func SetConfig(isNano bool, isApi bool, mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *config.Config {
port int, pppath string, pkpath string, cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel)
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() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack()))) tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
@@ -381,37 +221,72 @@ func GetConfig() *config.Config {
* The configuration loader will give priority to the local file over the default file * The configuration loader will give priority to the local file over the default file
*/ */
func GetConfLoader(appName string) *onion.Onion { func GetConfLoader() *onion.Onion {
return config.GetConfLoader(appName) return config.GetConfLoader()
} }
type Request struct { type Request struct {
Collection LibDataEnum collection LibDataEnum
User string user string
PeerID string peerID string
Groups []string groups []string
Caller *tools.HTTPCaller caller *tools.HTTPCaller
admin bool
} }
func NewRequest(collection LibDataEnum, user string, peerID string, groups []string, caller *tools.HTTPCaller) *Request { 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 NewRequestInfoAdmin(collection LibDataEnum, user string, groups []string, caller *tools.HTTPCaller) *Request { func ToScheduler(m interface{}) (n *workflow_execution.WorkflowSchedule) {
p, err := GetMySelf() defer func() {
peerID := "" if r := recover(); r != nil {
if p != nil && err == nil { return
peerID = p.GetID()
} }
return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller, admin: true} }()
return m.(*workflow_execution.WorkflowSchedule)
} }
func NewRequestAdmin(collection LibDataEnum, caller *tools.HTTPCaller) *Request { func (r *Request) Schedule(wfID string, scheduler *workflow_execution.WorkflowSchedule) (*workflow_execution.WorkflowSchedule, error) {
return &Request{Collection: collection, Caller: caller, admin: true} ws, _, _, err := scheduler.Schedules(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
if err != nil {
return nil, err
}
fmt.Println("BAM", ws)
return ws, nil
}
func (r *Request) CheckBooking(wfID string, start string, end string, durationInS float64, cron string) bool {
ok, _, _, _, err := workflow_execution.NewScheduler(start, end, durationInS, cron).CheckBooking(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
if err != nil {
fmt.Println(err)
return false
}
return ok
}
func (r *Request) DraftOrder(scheduler *workflow_execution.WorkflowSchedule) (*order.Order, error) {
o := &order.Order{}
if err := o.DraftOrder(scheduler, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}); err != nil {
return nil, err
}
return o, nil
} }
/*
func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.WorkflowSchedule) error { func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.WorkflowSchedule) error {
return o.Pay(scheduler, &tools.APIRequest{ return o.Pay(scheduler, &tools.APIRequest{
Caller: r.caller, Caller: r.caller,
@@ -419,9 +294,8 @@ func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.Wo
PeerID: r.peerID, PeerID: r.peerID,
Groups: r.groups, Groups: r.groups,
}) })
return nil
} }
*/
/* /*
* Search will search for the data in the database * Search will search for the data in the database
* @param filters *dbs.Filters * @param filters *dbs.Filters
@@ -430,20 +304,19 @@ func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.Wo
* @param c ...*tools.HTTPCaller * @param c ...*tools.HTTPCaller
* @return data LibDataShallow * @return data LibDataShallow
*/ */
func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool, offset int64, limit int64) (data LibDataShallow) { func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data LibDataShallow) {
defer func() { // recover the panic defer func() { // recover the panic
if r := recover(); r != nil { if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Search : "+fmt.Sprintf("%v", r))) 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())} 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{ d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin, }).Search(filters, word, isDraft)
}).Search(filters, word, isDraft, offset, limit)
if err != nil { if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()} data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
return return
@@ -458,20 +331,19 @@ func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool, offset
* @param c ...*tools.HTTPCaller * @param c ...*tools.HTTPCaller
* @return data LibDataShallow * @return data LibDataShallow
*/ */
func (r *Request) LoadAll(isDraft bool, offset int64, limit int64) (data LibDataShallow) { func (r *Request) LoadAll(isDraft bool) (data LibDataShallow) {
defer func() { // recover the panic defer func() { // recover the panic
if r := recover(); r != nil { if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in LoadAll : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack()))) 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())} 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{ d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin, }).LoadAll(isDraft)
}).LoadAll(isDraft, offset, limit)
if err != nil { if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()} data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
return return
@@ -494,12 +366,11 @@ 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())} 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{ d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin,
}).LoadOne(id) }).LoadOne(id)
if err != nil { if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()} data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -524,14 +395,13 @@ func (r *Request) UpdateOne(set map[string]interface{}, id string) (data LibData
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())} 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{ d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin, }).UpdateOne(model.Deserialize(set, model), id)
}).UpdateOne(set, id)
if err != nil { if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()} data = LibData{Data: d, Code: code, Err: err.Error()}
return return
@@ -554,12 +424,11 @@ 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())} 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{ d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin,
}).DeleteOne(id) }).DeleteOne(id)
if err != nil { if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()} data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -583,13 +452,12 @@ 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())} 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{ d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin,
}).StoreOne(model.Deserialize(object, model)) }).StoreOne(model.Deserialize(object, model))
if err != nil { if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()} data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -613,13 +481,12 @@ 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())} 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{ d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.Caller, Caller: r.caller,
Username: r.User, Username: r.user,
PeerID: r.PeerID, PeerID: r.peerID,
Groups: r.Groups, Groups: r.groups,
Admin: r.admin,
}).CopyOne(model.Deserialize(object, model)) }).CopyOne(model.Deserialize(object, model))
if err != nil { if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()} data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -709,189 +576,3 @@ func (l *LibData) ToOrder() *order.Order {
} }
return nil 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
}
}
Executable → Regular
+12 -61
View File
@@ -1,75 +1,28 @@
module cloud.o-forge.io/core/oc-lib module cloud.o-forge.io/core/oc-lib
go 1.25.0 go 1.22.0
require ( require (
github.com/beego/beego/v2 v2.3.8 github.com/beego/beego/v2 v2.3.1
github.com/go-playground/validator/v10 v10.22.0 github.com/go-playground/validator/v10 v10.22.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/goraz/onion v0.1.3 github.com/goraz/onion v0.1.3
github.com/libp2p/go-libp2p/core v0.43.0-rc2
github.com/nats-io/nats.go v1.37.0 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/rs/zerolog v1.33.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.9.0
k8s.io/apimachinery v0.35.1
) )
require ( 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/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/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 ( require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
go.mongodb.org/mongo-driver v1.16.0 go.mongodb.org/mongo-driver v1.16.0
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.22.0 // indirect
) )
require ( require (
@@ -83,6 +36,7 @@ require (
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
@@ -91,19 +45,16 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.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/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect
golang.org/x/crypto v0.44.0 // indirect golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.1
k8s.io/client-go v0.35.1
) )
replace github.com/libp2p/go-libp2p/core => github.com/libp2p/go-libp2p v0.47.0
Executable → Regular
+25 -150
View File
@@ -1,8 +1,6 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/beego/beego/v2 v2.3.1 h1:7MUKMpJYzOXtCUsTEoXOxsDV/UcHw6CPbaWMlthVNsc=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beego/beego/v2 v2.3.1/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc=
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q= github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
@@ -12,34 +10,15 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs= github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -48,18 +27,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -69,76 +42,34 @@ github.com/goraz/onion v0.1.3/go.mod h1:XEmz1XoBz+wxTgWB8NwuvRm4RAu3vKxvrmYtzK+X
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-libp2p/core v0.43.0-rc2 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc=
github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo=
github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-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 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
@@ -146,10 +77,6 @@ github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDm
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -162,8 +89,12 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -174,23 +105,10 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -202,33 +120,23 @@ github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dh
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -239,60 +147,27 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+5 -4
View File
@@ -59,12 +59,13 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
// A bit unsafe since we don't know what could be stored in the event // A bit unsafe since we don't know what could be stored in the event
// but we can't access this object once passed to the multilevel writter // but we can't access this object once passed to the multilevel writter
for k, v := range event { for k,v := range(event){
if k != "level" && k != "time" && k != "message" { if k != "level" && k != "time" && k != "message"{
labels[k] = fmt.Sprintf("%v", v) labels[k] = v.(string)
} }
} }
// Format the timestamp in nanoseconds // Format the timestamp in nanoseconds
timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond)) timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond))
@@ -86,7 +87,7 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
//fmt.Printf("Sending payload to Loki: %s\n", string(payloadBytes)) //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 { if err != nil {
return 0, fmt.Errorf("failed to create HTTP request: %w", err) return 0, fmt.Errorf("failed to create HTTP request: %w", err)
} }
-56
View File
@@ -1,56 +0,0 @@
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)
}
@@ -1,23 +0,0 @@
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"},
},
}
}
-421
View File
@@ -1,421 +0,0 @@
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
}
-22
View File
@@ -1,22 +0,0 @@
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{} },
},
}
}
-146
View File
@@ -1,146 +0,0 @@
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
}
@@ -1,22 +0,0 @@
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{} },
},
}
}
-151
View File
@@ -1,151 +0,0 @@
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
}
@@ -1,22 +0,0 @@
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{} },
},
}
}
@@ -1,82 +0,0 @@
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
}
}
-136
View File
@@ -1,136 +0,0 @@
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
}
@@ -1,22 +0,0 @@
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{} },
},
}
}
-194
View File
@@ -1,194 +0,0 @@
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
}
@@ -1,22 +0,0 @@
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{} },
},
}
}
-89
View File
@@ -1,89 +0,0 @@
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)
}
-2
View File
@@ -1,2 +0,0 @@
# Billing process
Scheduler process a drafted order + a first bill corresponding to every once buying.
+43 -77
View File
@@ -3,11 +3,12 @@ package booking
import ( import (
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
/* /*
@@ -15,18 +16,11 @@ import (
*/ */
type Booking struct { type Booking struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name) utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
FromNano string `json:"from_nano,omitempty" bson:"from_nano,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 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 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 WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"` ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`
State enum.BookingStatus `json:"state" bson:"state"` // State is the state of the booking 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 ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking
@@ -35,57 +29,36 @@ type Booking struct {
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty" validate:"required"` // ResourceType is the type of the resource ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty" validate:"required"` // ResourceType is the type of the resource
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty" validate:"required"` // could be a Compute or a Storage ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty" validate:"required"` // could be a Compute or a Storage
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
// Authorization: identifies who created this draft and the Check session it belongs to.
// Used to verify UPDATE and DELETE orders from remote schedulers.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
// 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"`
} }
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume { // CheckBooking checks if a booking is possible on a specific compute resource
m := map[string]map[string]models.MetricResume{} func (wfa *Booking) Check(id string, start time.Time, end *time.Time, parrallelAllowed int) (bool, error) {
for instance, snapshot := range b.ExecutionMetrics { // check if
m[instance] = map[string]models.MetricResume{} if end == nil {
for _, metric := range snapshot { // if no end... then Book like a savage
for _, mm := range metric.Metrics { e := start.Add(time.Hour)
if resume, ok := m[instance][mm.Name]; !ok { end = &e
m[instance][mm.Name] = models.MetricResume{
Delta: 0,
LastValue: mm.Value,
} }
} else { accessor := NewAccessor(nil)
delta := resume.LastValue - mm.Value l := logs.GetLogger().With().Str("Search Check", "Booking").Logger()
if delta == 0 { l.Debug().Msg("Starting to search")
resume.Delta = delta res, code, err := accessor.Search(&dbs.Filters{
} else { 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
resume.Delta = (resume.Delta + delta) / 2 "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)
l.Debug().Msg("Search finished")
if code != 200 {
return false, err
} }
resume.LastValue = mm.Value return len(res) <= parrallelAllowed, nil
m[instance][mm.Name] = resume
}
}
}
}
return m
} }
func (d *Booking) GetDelayForLaunch() time.Duration { func (d *Booking) GetDelayForLaunch() time.Duration {
@@ -93,10 +66,10 @@ func (d *Booking) GetDelayForLaunch() time.Duration {
} }
func (d *Booking) GetDelayForFinishing() time.Duration { func (d *Booking) GetDelayForFinishing() time.Duration {
if d.ExpectedEndDate == nil || d.RealEndDate == nil { if d.ExpectedEndDate == nil {
return time.Duration(0) return time.Duration(0)
} }
return d.RealEndDate.Sub(*d.ExpectedEndDate) return d.RealEndDate.Sub(d.ExpectedStartDate)
} }
func (d *Booking) GetUsualDuration() time.Duration { func (d *Booking) GetUsualDuration() time.Duration {
@@ -118,33 +91,26 @@ func (d *Booking) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request) // Create a new instance of the accessor
} }
func (d *Booking) VerifyAuth(callName string, request *tools.APIRequest) bool { func (d *Booking) VerifyAuth(request *tools.APIRequest) bool {
return true return true
} }
func (r *Booking) StoreDraftDefault() { func (r *Booking) StoreDraftDefault() {
r.IsDraft = true r.IsDraft = false
r.State = enum.DRAFT
} }
func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) { func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Booking) if !r.IsDraft && r.State != set.(*Booking).State || r.RealStartDate != set.(*Booking).RealStartDate || r.RealEndDate != set.(*Booking).RealEndDate {
if !r.IsDraft && r.State != incoming.State || r.RealStartDate != incoming.RealStartDate || r.RealEndDate != incoming.RealEndDate { return true, &Booking{
patch := &Booking{ State: set.(*Booking).State,
State: incoming.State, RealStartDate: set.(*Booking).RealStartDate,
RealStartDate: incoming.RealStartDate, RealEndDate: set.(*Booking).RealEndDate,
RealEndDate: incoming.RealEndDate, } // only state can be updated
}
// Auto-set RealStartDate when transitioning to STARTED and not already set
if r.State != enum.STARTED && incoming.State == enum.STARTED && patch.RealStartDate == nil {
now := time.Now()
patch.RealStartDate = &now
}
return true, patch
} }
// TODO : HERE WE CAN HANDLE THE CASE WHERE THE BOOKING IS DELAYED OR EXCEEDING OR ending sooner
return r.IsDraft, set return r.IsDraft, set
} }
func (r *Booking) CanDelete() bool { func (r *Booking) CanDelete() bool {
return true // only draft bookings can be deleted return r.IsDraft // only draft bookings can be deleted
} }
+38 -25
View File
@@ -4,25 +4,24 @@ import (
"errors" "errors"
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs/mongo" "cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type BookingMongoAccessor struct { type bookingMongoAccessor struct {
utils.AbstractAccessor[*Booking] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the BookingMongoAccessor // New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor { func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
return &BookingMongoAccessor{ return &bookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Booking]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.BOOKING, Type: tools.BOOKING,
New: func() *Booking { return &Booking{} },
}, },
} }
} }
@@ -30,25 +29,32 @@ func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (a *BookingMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) { func (a *bookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
if set["state"] == nil { return utils.GenericDeleteOne(id, a)
return nil, 400, errors.New("state is required")
}
set = map[string]interface{}{
"state": set["state"],
}
return utils.GenericUpdateOne(set, id, a)
} }
func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) { if set.(*Booking).State == 0 {
return nil, 400, errors.New("state is required")
}
realSet := &Booking{State: set.(*Booking).State}
return utils.GenericUpdateOne(realSet, id, a, &Booking{})
}
func (a *bookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *bookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now() now := time.Now()
now = now.Add(time.Second * -60) now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
// Direct raw delete to avoid infinite recursion: return utils.GenericDeleteOne(d.GetID(), a)
// GenericDeleteOne calls a.LoadOne which would re-enter this callback.
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
return nil, 410, errors.New("draft booking expired and deleted")
} }
if (d.(*Booking).ExpectedEndDate) == nil { if (d.(*Booking).ExpectedEndDate) == nil {
d.(*Booking).State = enum.FORGOTTEN d.(*Booking).State = enum.FORGOTTEN
@@ -61,13 +67,20 @@ func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
}, a) }, a)
} }
func (a *BookingMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject { 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 {
return func(d utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now() now := time.Now()
now = now.Add(time.Second * -60) now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
// Direct raw delete to avoid infinite recursion (same as LoadOne callback). utils.GenericDeleteOne(d.GetID(), a)
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
return nil return nil
} }
if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) {
-13
View File
@@ -1,13 +0,0 @@
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
)
-567
View File
@@ -1,567 +0,0 @@
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 (0100).
type PlannerSlot struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
InstanceID string `json:"instance_id,omitempty"` // instance targeted by this booking
BookingID string `json:"booking_id,omitempty"` // empty in shallow mode
Usage map[string]float64 `json:"usage,omitempty"` // dimension -> % of max (0-100)
}
// PlannerITF is the interface used by Planify to check resource availability.
// *Planner satisfies this interface.
type PlannerITF interface {
NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time
}
// Planner is a volatile (non-persisted) object that organises bookings by resource.
// 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
}
-87
View File
@@ -1,87 +0,0 @@
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() ao.CollaborativeAreaRule.CreatedAt = time.Now().UTC()
} }
func (ao *CollaborativeArea) VerifyAuth(callName string, request *tools.APIRequest) bool { func (ao *CollaborativeArea) VerifyAuth(request *tools.APIRequest) bool {
if (ao.AllowedPeersGroup != nil || config.GetConfig().Whitelist) && request != nil { if (ao.AllowedPeersGroup != nil || config.GetConfig().Whitelist) && request != nil {
if grps, ok := ao.AllowedPeersGroup[request.PeerID]; ok || config.GetConfig().Whitelist { if grps, ok := ao.AllowedPeersGroup[request.PeerID]; ok || config.GetConfig().Whitelist {
if slices.Contains(grps, "*") || (!ok && config.GetConfig().Whitelist) { if slices.Contains(grps, "*") || (!ok && config.GetConfig().Whitelist) {
@@ -84,16 +84,20 @@ func (ao *CollaborativeArea) VerifyAuth(callName string, request *tools.APIReque
} }
} }
} }
return ao.AbstractObject.VerifyAuth(callName, request) return ao.AbstractObject.VerifyAuth(request)
} }
func (d *CollaborativeArea) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *CollaborativeArea) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request) // Create a new instance of the accessor
} }
func (d *CollaborativeArea) Trim() *CollaborativeArea {
return d
}
func (d *CollaborativeArea) StoreDraftDefault() { func (d *CollaborativeArea) StoreDraftDefault() {
d.AllowedPeersGroup = map[string][]string{ d.AllowedPeersGroup = map[string][]string{
d.CreatorID: {"*"}, d.CreatorID: []string{"*"},
} }
d.IsDraft = false d.IsDraft = false
} }
@@ -17,7 +17,7 @@ import (
// SharedWorkspace is a struct that represents a collaborative area // SharedWorkspace is a struct that represents a collaborative area
type collaborativeAreaMongoAccessor struct { type collaborativeAreaMongoAccessor struct {
utils.AbstractAccessor[*CollaborativeArea] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
workspaceAccessor utils.Accessor workspaceAccessor utils.Accessor
workflowAccessor utils.Accessor workflowAccessor utils.Accessor
@@ -27,11 +27,10 @@ type collaborativeAreaMongoAccessor struct {
func NewAccessor(request *tools.APIRequest) *collaborativeAreaMongoAccessor { func NewAccessor(request *tools.APIRequest) *collaborativeAreaMongoAccessor {
return &collaborativeAreaMongoAccessor{ return &collaborativeAreaMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*CollaborativeArea]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.COLLABORATIVE_AREA, Type: tools.COLLABORATIVE_AREA,
New: func() *CollaborativeArea { return &CollaborativeArea{} },
}, },
workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request), workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request),
workflowAccessor: (&w.Workflow{}).GetAccessor(request), workflowAccessor: (&w.Workflow{}).GetAccessor(request),
@@ -53,8 +52,8 @@ func (a *collaborativeAreaMongoAccessor) DeleteOne(id string) (utils.DBObject, i
} }
// UpdateOne updates a collaborative area in the database, given its ID and the new data, it automatically share to peers if the workspace is shared // UpdateOne updates a collaborative area in the database, given its ID and the new data, it automatically share to peers if the workspace is shared
func (a *collaborativeAreaMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) { func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
res, code, err := utils.GenericUpdateOne(set, id, a) res, code, err := utils.GenericUpdateOne(set.(*CollaborativeArea).Trim(), id, a, &CollaborativeArea{})
// a.deleteToPeer(res.(*CollaborativeArea)) // delete the collaborative area on the peer // a.deleteToPeer(res.(*CollaborativeArea)) // delete the collaborative area on the peer
a.sharedWorkflow(res.(*CollaborativeArea), id) // replace all shared workflows a.sharedWorkflow(res.(*CollaborativeArea), id) // replace all shared workflows
a.sharedWorkspace(res.(*CollaborativeArea), id) // replace all collaborative areas (not shared worspace obj but workspace one) a.sharedWorkspace(res.(*CollaborativeArea), id) // replace all collaborative areas (not shared worspace obj but workspace one)
@@ -64,19 +63,14 @@ func (a *collaborativeAreaMongoAccessor) UpdateOne(set map[string]interface{}, i
// StoreOne stores a collaborative area in the database, it automatically share to peers if the workspace is shared // StoreOne stores a collaborative area in the database, it automatically share to peers if the workspace is shared
func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{ _, id := (&peer.Peer{}).IsMySelf() // get the local peer
Admin: true, data.(*CollaborativeArea).Clear(id) // set the creator
})) // 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 // retrieve or proper peer
if data.(*CollaborativeArea).CollaborativeAreaRule != nil { if data.(*CollaborativeArea).CollaborativeAreaRule != nil {
data.(*CollaborativeArea).CollaborativeAreaRule = &CollaborativeAreaRule{} data.(*CollaborativeArea).CollaborativeAreaRule = &CollaborativeAreaRule{}
} }
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = pp.GetID() data.(*CollaborativeArea).CollaborativeAreaRule.Creator = id
d, code, err := utils.GenericStoreOne(data, a) d, code, err := utils.GenericStoreOne(data.(*CollaborativeArea).Trim(), a)
if code == 200 { if code == 200 {
a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows
a.sharedWorkspace(d.(*CollaborativeArea), d.GetID()) // create all collaborative areas a.sharedWorkspace(d.(*CollaborativeArea), d.GetID()) // create all collaborative areas
@@ -85,13 +79,19 @@ func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DB
return data, code, err return data, code, err
} }
// CopyOne copies a CollaborativeArea in the database
func (a *collaborativeAreaMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return a.StoreOne(data)
}
func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils.Accessor) []T { func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils.Accessor) []T {
var new []T var new []T
res, code, _ := a.Search(&dbs.Filters{ res, code, _ := a.Search(&dbs.Filters{
Or: map[string][]dbs.Filter{ Or: map[string][]dbs.Filter{
"abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}}, "abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}},
}, },
}, "", isDrafted, 0, int64(len(arr))) }, "", isDrafted)
fmt.Println(res, arr, isDrafted, a)
if code == 200 { if code == 200 {
for _, r := range res { for _, r := range res {
new = append(new, r.(T)) new = append(new, r.(T))
@@ -125,10 +125,23 @@ func (a *collaborativeAreaMongoAccessor) enrich(sharedWorkspace *CollaborativeAr
return sharedWorkspace return sharedWorkspace
} }
func (a *collaborativeAreaMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject { func (a *collaborativeAreaMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return func(d utils.DBObject) utils.ShallowDBObject { return utils.GenericLoadOne[*CollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return a.enrich(d.(*CollaborativeArea), isDraft, a.Request) 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)
} }
/* /*
@@ -140,9 +153,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
eld := eldest.(*CollaborativeArea) eld := eldest.(*CollaborativeArea)
if eld.Workspaces != nil { // update all your workspaces in the eldest by replacing shared ref by an empty string if eld.Workspaces != nil { // update all your workspaces in the eldest by replacing shared ref by an empty string
for _, v := range eld.Workspaces { for _, v := range eld.Workspaces {
a.workspaceAccessor.UpdateOne(map[string]interface{}{ a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: ""}, v)
"shared": "",
}, v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
continue continue
} }
@@ -158,10 +169,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
} }
if shared.Workspaces != nil { if shared.Workspaces != nil {
for _, v := range shared.Workspaces { // update all the collaborative areas for _, v := range shared.Workspaces { // update all the collaborative areas
workspace, code, _ := a.workspaceAccessor.UpdateOne( workspace, code, _ := a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: shared.UUID}, v) // add the shared ref to workspace
map[string]interface{}{
"shared": shared.UUID,
}, v) // add the shared ref to workspace
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
continue continue
} }
@@ -201,7 +209,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
} // kick the shared reference in your old shared workflow } // kick the shared reference in your old shared workflow
n := &w.Workflow{} n := &w.Workflow{}
n.Shared = new n.Shared = new
a.workflowAccessor.UpdateOne(n.Serialize(n), v) a.workflowAccessor.UpdateOne(n, v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
continue continue
} }
@@ -223,7 +231,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
s := data.(*w.Workflow) s := data.(*w.Workflow)
if !slices.Contains(s.Shared, id) { if !slices.Contains(s.Shared, id) {
s.Shared = append(s.Shared, id) s.Shared = append(s.Shared, id)
workflow, code, _ := a.workflowAccessor.UpdateOne(s.Serialize(s), v) workflow, code, _ := a.workflowAccessor.UpdateOne(s, v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil { if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
continue continue
} }
@@ -246,8 +254,6 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
// because you have no reference to the remote shared workflow // because you have no reference to the remote shared workflow
} }
// TODO it's a Shared API Problem with OC-DISCOVERY
// sharedWorkspace is a function that shares the collaborative area to the peers // sharedWorkspace is a function that shares the collaborative area to the peers
func (a *collaborativeAreaMongoAccessor) deleteToPeer(shared *CollaborativeArea) { func (a *collaborativeAreaMongoAccessor) deleteToPeer(shared *CollaborativeArea) {
a.contactPeer(shared, tools.POST) a.contactPeer(shared, tools.POST)
@@ -265,9 +271,7 @@ func (a *collaborativeAreaMongoAccessor) contactPeer(shared *CollaborativeArea,
paccess := (&peer.Peer{}) paccess := (&peer.Peer{})
for k := range shared.AllowedPeersGroup { for k := range shared.AllowedPeersGroup {
if ok, _ := utils.IsMySelf(k, (&peer.Peer{}).GetAccessor(&tools.APIRequest{ if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: k}}).IsMySelf(); ok || (shared.IsSent && meth == tools.POST) || (!shared.IsSent && meth != tools.POST) {
Admin: true,
})); ok || (shared.IsSent && meth == tools.POST) || (!shared.IsSent && meth != tools.POST) {
continue continue
} }
shared.IsSent = meth == tools.POST shared.IsSent = meth == tools.POST
+1 -1
View File
@@ -24,6 +24,6 @@ func (d *Rule) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) return NewAccessor(request)
} }
func (d *Rule) VerifyAuth(callName string, request *tools.APIRequest) bool { func (d *Rule) VerifyAuth(request *tools.APIRequest) bool {
return true return true
} }
@@ -1,23 +1,62 @@
package rule package rule
import ( import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type ruleMongoAccessor struct { type ruleMongoAccessor struct {
utils.AbstractAccessor[*Rule] utils.AbstractAccessor
} }
// New creates a new instance of the ruleMongoAccessor // New creates a new instance of the ruleMongoAccessor
func NewAccessor(request *tools.APIRequest) *ruleMongoAccessor { func NewAccessor(request *tools.APIRequest) *ruleMongoAccessor {
return &ruleMongoAccessor{ return &ruleMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Rule]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.RULE.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.RULE.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.RULE, Type: tools.RULE,
New: func() *Rule { return &Rule{} },
}, },
} }
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *ruleMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *ruleMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &Rule{})
}
func (a *ruleMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *ruleMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *ruleMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Rule](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *ruleMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Rule](a.getExec(), isDraft, a)
}
func (a *ruleMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Rule](filters, search, (&Rule{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *ruleMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}
@@ -1,22 +1,56 @@
package shallow_collaborative_area package shallow_collaborative_area
import ( import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type shallowSharedWorkspaceMongoAccessor struct { type shallowSharedWorkspaceMongoAccessor struct {
utils.AbstractAccessor[*ShallowCollaborativeArea] utils.AbstractAccessor
} }
func NewAccessor(request *tools.APIRequest) *shallowSharedWorkspaceMongoAccessor { func NewAccessor(request *tools.APIRequest) *shallowSharedWorkspaceMongoAccessor {
return &shallowSharedWorkspaceMongoAccessor{ return &shallowSharedWorkspaceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*ShallowCollaborativeArea]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request, // Set the caller Request: request, // Set the caller
Type: tools.COLLABORATIVE_AREA, Type: tools.COLLABORATIVE_AREA,
New: func() *ShallowCollaborativeArea { return &ShallowCollaborativeArea{} },
}, },
} }
} }
func (a *shallowSharedWorkspaceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set.(*ShallowCollaborativeArea), id, a, &ShallowCollaborativeArea{})
}
func (a *shallowSharedWorkspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*ShallowCollaborativeArea), a)
}
func (a *shallowSharedWorkspaceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return a.StoreOne(data)
}
func (a *shallowSharedWorkspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*ShallowCollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*ShallowCollaborativeArea](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*ShallowCollaborativeArea](filters, search, (&ShallowCollaborativeArea{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, a)
}
-16
View File
@@ -1,16 +0,0 @@
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 / ...)
*/
-10
View File
@@ -1,7 +1,5 @@
package enum package enum
import "fmt"
type InfrastructureType int type InfrastructureType int
const ( const (
@@ -20,11 +18,3 @@ func (t InfrastructureType) String() string {
func InfrastructureList() []InfrastructureType { func InfrastructureList() []InfrastructureType {
return []InfrastructureType{DOCKER, KUBERNETES, SLURM, HW, CONDOR} return []InfrastructureType{DOCKER, KUBERNETES, SLURM, HW, CONDOR}
} }
func (d InfrastructureType) Compare(indexStr interface{}) bool {
return fmt.Sprintf("%v", indexStr) == fmt.Sprintf("%v", d.EnumIndex()) || fmt.Sprintf("%v", indexStr) == d.String()
}
func (d InfrastructureType) EnumIndex() int {
return int(d)
}
-10
View File
@@ -1,7 +1,5 @@
package enum package enum
import "fmt"
type StorageSize int type StorageSize int
// StorageType - Enum that defines the type of storage // StorageType - Enum that defines the type of storage
@@ -56,11 +54,3 @@ func (t StorageType) String() string {
func TypeList() []StorageType { func TypeList() []StorageType {
return []StorageType{FILE, STREAM, API, DATABASE, S3, MEMORY, HARDWARE, AZURE, GCS} return []StorageType{FILE, STREAM, API, DATABASE, S3, MEMORY, HARDWARE, AZURE, GCS}
} }
func (d StorageType) Compare(indexStr interface{}) bool {
return fmt.Sprintf("%v", indexStr) == fmt.Sprintf("%v", d.EnumIndex()) || fmt.Sprintf("%v", indexStr) == d.String()
}
func (d StorageType) EnumIndex() int {
return int(d)
}
+1 -3
View File
@@ -32,7 +32,6 @@ const (
FORGOTTEN FORGOTTEN
DELAYED DELAYED
CANCELLED CANCELLED
IN_PREPARATION
) )
var str = [...]string{ var str = [...]string{
@@ -44,7 +43,6 @@ var str = [...]string{
"forgotten", "forgotten",
"delayed", "delayed",
"cancelled", "cancelled",
"in_preparation",
} }
func FromInt(i int) string { func FromInt(i int) string {
@@ -62,5 +60,5 @@ func (d BookingStatus) EnumIndex() int {
// List // List
func StatusList() []BookingStatus { func StatusList() []BookingStatus {
return []BookingStatus{DRAFT, SCHEDULED, STARTED, FAILURE, SUCCESS, FORGOTTEN, DELAYED, CANCELLED, IN_PREPARATION} return []BookingStatus{DRAFT, SCHEDULED, STARTED, FAILURE, SUCCESS, FORGOTTEN, DELAYED, CANCELLED}
} }
+5 -65
View File
@@ -1,73 +1,13 @@
package models package models
import "sort"
// SortedArgValues returns arg Values: readonly args first (sorted by Index), then non-readonly in order.
func SortedArgValues(args []Arg) []string {
var ro, nro []Arg
for _, a := range args {
if a.IsReadonly {
ro = append(ro, a)
} else {
nro = append(nro, a)
}
}
sort.Slice(ro, func(i, j int) bool { return ro[i].Index < ro[j].Index })
out := make([]string, 0, len(args))
for _, a := range ro {
out = append(out, a.Value)
}
for _, a := range nro {
out = append(out, a.Value)
}
return out
}
// ReadonlyArgValues returns only the readonly arg values sorted by Index.
func ReadonlyArgValues(args []Arg) []string {
var ro []Arg
for _, a := range args {
if a.IsReadonly {
ro = append(ro, a)
}
}
sort.Slice(ro, func(i, j int) bool { return ro[i].Index < ro[j].Index })
out := make([]string, 0, len(ro))
for _, a := range ro {
out = append(out, a.Value)
}
return out
}
// NonReadonlyArgValues returns only the non-readonly arg values in their order.
func NonReadonlyArgValues(args []Arg) []string {
out := make([]string, 0)
for _, a := range args {
if !a.IsReadonly {
out = append(out, a.Value)
}
}
return out
}
type Arg struct {
Value string `json:"value,omitempty" bson:"value,omitempty"` // Image is the container image TEMPO
Index int `json:"index,omitempty" bson:"index,omitempty"`
IsReadonly bool `json:"is_readonly,omitempty" bson:"is_readonly,omitempty"`
}
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 []Arg `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 { type Container struct {
PathSource
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO 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 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 { type Expose struct {
+1 -1
View File
@@ -12,7 +12,7 @@ type Param struct {
Value string `json:"value,omitempty" bson:"value,omitempty"` Value string `json:"value,omitempty" bson:"value,omitempty"`
Origin string `json:"origin,omitempty" bson:"origin,omitempty"` Origin string `json:"origin,omitempty" bson:"origin,omitempty"`
Readonly bool `json:"readonly" bson:"readonly" default:"true"` Readonly bool `json:"readonly" bson:"readonly" default:"true"`
Required bool `json:"required" bson:"required" default:"true"` Optionnal bool `json:"optionnal" bson:"optionnal" default:"true"`
} }
type InOutputs struct { type InOutputs struct {
-17
View File
@@ -1,17 +0,0 @@
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"`
}
Executable → Regular
+15 -17
View File
@@ -7,38 +7,36 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 { func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 {
near := float64(-1) // unset sentinel near := float64(10000000000) // set a high value
for _, items := range planned { // loop through the planned items for _, items := range planned { // loop through the planned items
for _, priced := range items { // loop through the priced items for _, priced := range items { // loop through the priced items
if priced.GetLocationStart() == nil { // if the start is nil, if priced.GetLocationStart() == nil { // if the start is nil,
continue // skip the iteration continue // skip the iteration
} }
newS := priced.GetLocationStart() // get the start newS := priced.GetLocationStart() // get the start
diff := newS.Sub(start).Seconds() // get the difference if newS.Sub(start).Seconds() < near { // if the difference between the start and the new start is less than the nearest start
if near < 0 || diff < near { // if the difference is less than the nearest start near = newS.Sub(start).Seconds()
near = diff
} }
} }
} }
if near < 0 {
return 0 // no items found, start at the given start time
}
return near return near
} }
// GetPlannerLongestTime returns the sum of all processing+service durations. func GetPlannerLongestTime(end *time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 {
// Returns -1 if any item is open-ended (no deadline). if end == nil {
func GetPlannerLongestTime(planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
longestTime := float64(0)
for _, dt := range []tools.DataType{tools.PROCESSING_RESOURCE, tools.SERVICE_RESOURCE} {
for _, priced := range planned[dt] {
d := priced.GetExplicitDurationInS()
if d < 0 {
return -1 return -1
} }
longestTime += d longestTime := float64(0)
for _, priced := range planned[tools.PROCESSING_RESOURCE] {
if priced.GetLocationEnd() == nil {
continue
} }
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 return longestTime
} }
+2 -10
View File
@@ -3,26 +3,18 @@ package pricing
import ( import (
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type PricedItemITF interface { type PricedItemITF interface {
GetID() string GetID() string
GetName() string
GetInstanceID() string
GetType() tools.DataType GetType() tools.DataType
IsPurchasable() bool IsPurchased() bool
IsBooked() bool
GetQuantity() int
AddQuantity(amount int)
GetBookingMode() enum.BookingMode
GetCreatorID() string GetCreatorID() string
SelectPricing() PricingProfileITF
GetLocationStart() *time.Time GetLocationStart() *time.Time
SetLocationStart(start time.Time) SetLocationStart(start time.Time)
SetLocationEnd(end time.Time) SetLocationEnd(end time.Time)
GetLocationEnd() *time.Time GetLocationEnd() *time.Time
GetExplicitDurationInS() float64 GetExplicitDurationInS() float64
GetPriceHT() (float64, error) GetPrice() (float64, error)
} }
+2 -49
View File
@@ -5,11 +5,9 @@ import (
) )
type PricingProfileITF interface { type PricingProfileITF interface {
IsBooked() bool GetPrice(quantity float64, val float64, start time.Time, end time.Time, params ...string) (float64, error)
IsPurchasable() bool IsPurchased() bool
GetPurchase() BuyingStrategy
GetOverrideStrategyValue() int GetOverrideStrategyValue() int
GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variation []*PricingVariation, params ...string) (float64, error)
} }
type RefundType int type RefundType int
@@ -28,61 +26,16 @@ func RefundTypeList() []RefundType {
return []RefundType{REFUND_DEAD_END, REFUND_ON_ERROR, REFUND_ON_EARLY_END} 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 type AccessPricingProfile[T Strategy] struct { // only use for acces such as : DATA && PROCESSING
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 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 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 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 { func (b *AccessPricingProfile[T]) GetOverrideStrategyValue() int {
return -1 return -1
} }
func GetDefaultPricingProfile() PricingProfileITF {
return &AccessPricingProfile[TimePricingStrategy]{
Pricing: PricingStrategy[TimePricingStrategy]{
Price: 0,
Currency: "EUR",
BuyingStrategy: SUBSCRIPTION,
TimePricingStrategy: PER_SECOND,
},
}
}
type ExploitPrivilegeStrategy int type ExploitPrivilegeStrategy int
const ( const (
+14 -108
View File
@@ -7,65 +7,20 @@ import (
"time" "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 type BuyingStrategy int
// should except... on
const ( const (
SUBSCRIPTION BuyingStrategy = iota // is a permanent buying ( predictible ) UNLIMITED BuyingStrategy = iota
UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible ) SUBSCRIPTION
PERMANENT // a defined subscription ( predictible ) PAY_PER_USE
// PAY_PER_USE // per request. ( unpredictible )
) )
func (t BuyingStrategy) String() string { func (t BuyingStrategy) String() string {
return [...]string{"SUBSCRIPTION", "UNDEFINED_SUBSCRIPTION", "PERMANENT"}[t] return [...]string{"UNLIMITED", "SUBSCRIPTION", "PAY PER USE"}[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 { func BuyingStrategyList() []BuyingStrategy {
return []BuyingStrategy{SUBSCRIPTION, UNDEFINED_SUBSCRIPTION, PERMANENT} return []BuyingStrategy{UNLIMITED, SUBSCRIPTION, PAY_PER_USE}
} }
type Strategy interface { type Strategy interface {
@@ -89,10 +44,6 @@ func (t TimePricingStrategy) String() string {
return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t] return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t]
} }
func TimePricingStrategyListStr() []string {
return []string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}
}
func TimePricingStrategyList() []TimePricingStrategy { func TimePricingStrategyList() []TimePricingStrategy {
return []TimePricingStrategy{ONCE, PER_SECOND, PER_MINUTE, PER_HOUR, PER_DAY, PER_WEEK, PER_MONTH} return []TimePricingStrategy{ONCE, PER_SECOND, PER_MINUTE, PER_HOUR, PER_DAY, PER_WEEK, PER_MONTH}
} }
@@ -111,11 +62,13 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
fromAverageDuration := after.Sub(now).Seconds() fromAverageDuration := after.Sub(now).Seconds()
var tEnd time.Time var tEnd time.Time
fromDateDuration := float64(0) if end == nil {
if end != nil { tEnd = start.Add(1 * time.Hour)
} else {
tEnd = *end tEnd = *end
fromDateDuration = tEnd.Sub(start).Seconds()
} }
fromDateDuration := tEnd.Sub(start).Seconds()
if fromAverageDuration > fromDateDuration { if fromAverageDuration > fromDateDuration {
return fromAverageDuration return fromAverageDuration
} }
@@ -124,9 +77,6 @@ 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) { func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInSecond float64, start time.Time, end *time.Time) (float64, error) {
locationDurationInSecond = getAverageTimeInSecond(locationDurationInSecond, start, end) locationDurationInSecond = getAverageTimeInSecond(locationDurationInSecond, start, end)
if locationDurationInSecond <= 0 {
return 0, nil
}
priceStr := fmt.Sprintf("%v", price) priceStr := fmt.Sprintf("%v", price)
p, err := strconv.ParseFloat(priceStr, 64) p, err := strconv.ParseFloat(priceStr, 64)
if err != nil { if err != nil {
@@ -151,48 +101,19 @@ func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInS
return 0, errors.New("pricing strategy not found") return 0, errors.New("pricing strategy not found")
} }
// may suppress in pricing strategy -> to set in map
type PricingStrategy[T Strategy] struct { type PricingStrategy[T Strategy] struct {
Price float64 `json:"price" bson:"price" default:"0"` // Price is the Price 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 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 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 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 OverrideStrategy T `json:"override_strategy" bson:"override_strategy" default:"-1"` // Modulation is the modulation of the pricing
} }
func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time, variations []*PricingVariation) (float64, error) { func (p PricingStrategy[T]) GetPrice(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time) (float64, error) {
switch p.BuyingStrategy { if p.BuyingStrategy == SUBSCRIPTION {
case SUBSCRIPTION: return BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
price, err := BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end) } else if p.BuyingStrategy == UNLIMITED {
if err != nil {
return 0, err
}
if variations != nil {
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.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 return p.Price * float64(amountOfData), nil
} }
@@ -208,18 +129,3 @@ func (p PricingStrategy[T]) GetTimePricingStrategy() TimePricingStrategy {
func (p PricingStrategy[T]) GetOverrideStrategy() T { func (p PricingStrategy[T]) GetOverrideStrategy() T {
return p.OverrideStrategy 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
}
}
-129
View File
@@ -1,129 +0,0 @@
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)
}
@@ -1,45 +0,0 @@
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
}
@@ -1,38 +0,0 @@
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)
}
-13
View File
@@ -1,13 +0,0 @@
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)
}
-93
View File
@@ -1,93 +0,0 @@
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
}
-47
View File
@@ -1,47 +0,0 @@
package live
import (
"fmt"
"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 {
fmt.Println("COMPARE <", r.Infrastructure.Compare(service["infrastructure"]), "> AND <", service["architecture"], "> <", r.Architecture, ">")
return r.Infrastructure.Compare(service["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
}
-57
View File
@@ -1,57 +0,0 @@
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
}
}
-46
View File
@@ -1,46 +0,0 @@
package live
import (
"fmt"
"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 {
fmt.Println("COMPARE <", service["infrastructure"], "> <", r.Infrastructure, ">")
return r.Infrastructure.Compare(service["infrastructure"])
}
-38
View File
@@ -1,38 +0,0 @@
package live
import (
"fmt"
"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 {
fmt.Println("COMPARE <", r.StorageType.Compare(service["storage_type"]), ">")
return r.StorageType.Compare(service["storage_type"])
}
func (d *LiveStorage) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveStorage](tools.LIVE_STORAGE, request) // Create a new instance of the accessor
}
-120
View File
@@ -1,120 +0,0 @@
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)
}
+6 -33
View File
@@ -2,18 +2,8 @@ package models
import ( import (
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/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/order"
"cloud.o-forge.io/core/oc-lib/models/peer/policy"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource" "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/tools"
"cloud.o-forge.io/core/oc-lib/models/booking" "cloud.o-forge.io/core/oc-lib/models/booking"
@@ -21,6 +11,7 @@ import (
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule" "cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
resource "cloud.o-forge.io/core/oc-lib/models/resources" 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" w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution" "cloud.o-forge.io/core/oc-lib/models/workflow_execution"
w3 "cloud.o-forge.io/core/oc-lib/models/workspace" w3 "cloud.o-forge.io/core/oc-lib/models/workspace"
@@ -30,14 +21,12 @@ import (
This package contains the models used in the application This package contains the models used in the application
It's used to create the models dynamically It's used to create the models dynamically
*/ */
var ModelsCatalog = map[string]func() utils.DBObject{ var models = map[string]func() utils.DBObject{
tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} }, tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} },
tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} }, tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} },
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} }, tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
tools.STORAGE_RESOURCE.String(): func() utils.DBObject { return &resource.StorageResource{} }, tools.STORAGE_RESOURCE.String(): func() utils.DBObject { return &resource.StorageResource{} },
tools.PROCESSING_RESOURCE.String(): func() utils.DBObject { return &resource.ProcessingResource{} }, 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.String(): func() utils.DBObject { return &w2.Workflow{} },
tools.WORKFLOW_EXECUTION.String(): func() utils.DBObject { return &workflow_execution.WorkflowExecution{} }, tools.WORKFLOW_EXECUTION.String(): func() utils.DBObject { return &workflow_execution.WorkflowExecution{} },
tools.WORKSPACE.String(): func() utils.DBObject { return &w3.Workspace{} }, tools.WORKSPACE.String(): func() utils.DBObject { return &w3.Workspace{} },
@@ -49,38 +38,22 @@ var ModelsCatalog = map[string]func() utils.DBObject{
tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} }, tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} },
tools.ORDER.String(): func() utils.DBObject { return &order.Order{} }, tools.ORDER.String(): func() utils.DBObject { return &order.Order{} },
tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} }, 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{} },
tools.POLICY.String(): func() utils.DBObject { return &policy.Policy{} },
} }
// Model returns the model object based on the model type // Model returns the model object based on the model type
func Model(model int) utils.DBObject { func Model(model int) utils.DBObject {
log := logs.GetLogger() log := logs.GetLogger()
if model < 0 || model >= len(tools.Str) { if _, ok := models[tools.FromInt(model)]; ok {
log.Error().Msg("Can't find model: index out of range") return models[tools.FromInt(model)]()
return nil
} }
key := tools.FromInt(model) log.Error().Msg("Can't find model " + tools.FromInt(model) + ".")
if _, ok := ModelsCatalog[key]; ok {
return ModelsCatalog[key]()
}
log.Error().Msg("Can't find model " + key + ".")
return nil return nil
} }
// GetModelsNames returns the names of the models // GetModelsNames returns the names of the models
func GetModelsNames() []string { func GetModelsNames() []string {
names := []string{} names := []string{}
for name := range ModelsCatalog { for name := range models {
names = append(names, name) names = append(names, name)
} }
return names return names
+287 -13
View File
@@ -1,13 +1,19 @@
package order package order
import ( import (
"errors"
"fmt"
"sync"
"time" "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/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/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/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
@@ -17,17 +23,12 @@ import (
type Order struct { type Order struct {
utils.AbstractObject utils.AbstractObject
ExecutionsID string `json:"executions_id" bson:"executions_id" validate:"required"` 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"` Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"` SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Bookings []*booking.Booking `json:"bookings" bson:"bookings"` Total float64 `json:"total" bson:"total" validate:"required"`
// 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() { func (r *Order) StoreDraftDefault() {
@@ -45,14 +46,287 @@ func (r *Order) CanDelete() bool {
return r.IsDraft // only draft order can be deleted return r.IsDraft // only draft order can be deleted
} }
func (o *Order) Quantity() int { func (o *Order) DraftOrder(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
return len(o.Purchases) + len(o.Purchases) // set the draft order from the model
if err := o.draftStoreFromModel(scheduler, request); err != nil {
return err
}
return nil
} }
func (d *Order) SetName(_ string) { 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)
}
func (d *Order) SetName() {
d.Name = d.UUID + "_order_" + "_" + time.Now().UTC().Format("2006-01-02T15:04:05") d.Name = d.UUID + "_order_" + "_" + time.Now().UTC().Format("2006-01-02T15:04:05")
} }
func (d *Order) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *Order) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the 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
+44 -4
View File
@@ -1,24 +1,64 @@
package order package order
import ( import (
"errors"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type orderMongoAccessor struct { type orderMongoAccessor struct {
utils.AbstractAccessor[*Order] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the orderMongoAccessor // New creates a new instance of the orderMongoAccessor
func NewAccessor(request *tools.APIRequest) *orderMongoAccessor { func NewAccessor(request *tools.APIRequest) *orderMongoAccessor {
return &orderMongoAccessor{ return &orderMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Order]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.ORDER, Type: tools.ORDER,
New: func() *Order { return &Order{} },
NotImplemented: []string{"CopyOne"},
}, },
} }
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *orderMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *orderMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &Order{})
}
func (a *orderMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return 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
}
}
-92
View File
@@ -1,92 +0,0 @@
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)
}
-11
View File
@@ -1,11 +0,0 @@
package organization
// Organization holds descriptive data about a peer's organization.
// It is optional — a peer without an organization has a nil Organization field.
type Organization struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Description string `json:"description,omitempty" bson:"description,omitempty"`
Website string `json:"website,omitempty" bson:"website,omitempty"`
Sector string `json:"sector,omitempty" bson:"sector,omitempty"`
Country string `json:"country,omitempty" bson:"country,omitempty"`
}
+25 -182
View File
@@ -2,216 +2,45 @@ package peer
import ( import (
"fmt" "fmt"
"strings"
"time"
"cloud.o-forge.io/core/oc-lib/models/organization"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/biter777/countries"
) )
type PeerPerm int // now write a go enum for the state partner with self, blacklist, partner
type PeerState int
const ( const (
READ PeerRelation = iota NONE PeerState = iota
WRITE
MONITOR
)
type PeerRelation int
const (
NONE PeerRelation = iota
SELF SELF
PARTNER PARTNER
BLACKLIST BLACKLIST
PENDING_PARTNER
MASTER
NANO
PENDING_NANO
PENDING_MASTER
ORGANIZATION_MASTER
ORGANIZATION_MEMBER
ORGANIZATION_PARTNER
ORGANIZATION_MASTER_PENDING
ORGANIZATION_MEMBER_PENDING
) )
var path = []string{ func (m PeerState) String() string {
"known", "self", "partner", "blacklist", "pending_partner", return [...]string{"NONE", "SELF", "PARTNER", "BLACKLIST"}[m]
"master", "nano", "pending_nano", "pending_master",
"organization_master", "organization_member", "organization_partner",
"organization_master_pending", "organization_member_pending",
} }
func GetRelationPath(str string) int { func (m PeerState) EnumIndex() int {
for i, p := range path {
fmt.Println("GetRelationPath", i, p)
if str == p {
return i
}
}
return -1
}
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) 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 // Peer is a struct that represents a peer
type Peer struct { type Peer struct {
utils.AbstractObject utils.AbstractObject
IsNano bool `json:"is_nano" bson:"is_nano"` Url string `json:"url" bson:"url" validate:"required"` // Url is the URL of the peer (base64url)
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 WalletAddress string `json:"wallet_address" bson:"wallet_address" validate:"required"` // WalletAddress is the wallet address of the peer
PublicKey string `json:"public_key" bson:"public_key" validate:"required"` // PublicKey is the public key of the peer PublicKey string `json:"public_key" bson:"public_key" validate:"required"` // PublicKey is the public key of the peer
Relation PeerRelation `json:"relation" bson:"relation" default:"0"` State PeerState `json:"state" bson:"state" default:"0"`
ServicesState map[string]int `json:"services_state,omitempty" bson:"services_state,omitempty"` ServicesState map[string]int `json:"services_state,omitempty" bson:"services_state,omitempty"`
FailedExecution []PeerExecution `json:"failed_execution" bson:"failed_execution"` // FailedExecution is the list of failed executions, to be retried FailedExecution []PeerExecution `json:"failed_execution" bson:"failed_execution"` // FailedExecution is the list of failed executions, to be retried
// 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"`
// OrganizationMasterID is the MongoDB _id of the peer acting as this node's
// organization master. Set automatically when an ORGANIZATION_MASTER relation
// is validated (equivalent of MasterID for the Nano/Master hierarchy).
OrganizationMasterID string `json:"organization_master_id,omitempty" bson:"organization_master_id,omitempty"`
// Organization holds optional descriptive data about the peer's organization.
// Null when the peer has not registered any organization data.
Organization *organization.Organization `json:"organization,omitempty" bson:"organization,omitempty"`
// PolicyID references the Policy document that governs which inbound
// libp2p streams are authorized for this peer.
// When empty, all non-vital streams are denied by default.
PolicyID string `json:"policy_id,omitempty" bson:"policy_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 (ri *Peer) Extend(typ ...string) map[string][]tools.DataType { func (ao *Peer) VerifyAuth(request *tools.APIRequest) bool {
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)
case "policy":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.POLICY)
}
}
return ext
}
func (ao *Peer) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true 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 // AddExecution adds an execution to the list of failed executions
func (ao *Peer) AddExecution(exec PeerExecution) { func (ao *Peer) AddExecution(exec PeerExecution) {
found := false found := false
@@ -237,8 +66,18 @@ func (ao *Peer) RemoveExecution(exec PeerExecution) {
ao.FailedExecution = new 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 // 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) (map[string]interface{}, error) { func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
p.UUID = peerID p.UUID = peerID
return cache.LaunchPeerExecution(peerID, dataID, dt, method, body, caller) // Launch the execution on the peer through the cache return cache.LaunchPeerExecution(peerID, dataID, dt, method, body, caller) // Launch the execution on the peer through the cache
} }
@@ -246,3 +85,7 @@ func (d *Peer) GetAccessor(request *tools.APIRequest) utils.Accessor {
data := NewAccessor(request) // Create a new instance of the accessor data := NewAccessor(request) // Create a new instance of the accessor
return data return data
} }
func (r *Peer) CanDelete() bool {
return false // only draft order can be deleted
}
+45 -24
View File
@@ -28,69 +28,90 @@ type PeerCache struct {
} }
// urlFormat formats the URL of the peer with the data type API function // urlFormat formats the URL of the peer with the data type API function
func urlFormat(hostUrl string, dt tools.DataType) string { func (p *PeerCache) urlFormat(hostUrl string, dt tools.DataType) string {
return hostUrl + "/" + strings.ReplaceAll(dt.String(), "oc-", "") // localhost is replaced by the local peer URL
// because localhost must collide on a web request security protocol
/*localhost := ""
if strings.Contains(hostUrl, "localhost") {
localhost = "localhost"
}
if strings.Contains(hostUrl, "127.0.0.1") {
localhost = "127.0.0.1"
}
if localhost != "" {
r := regexp.MustCompile("(" + localhost + ":[0-9]+)")
t := r.FindString(hostUrl)
if t != "" {
hostUrl = strings.Replace(hostUrl, t, dt.API()+":8080/oc", -1)
} else {
hostUrl = strings.ReplaceAll(hostUrl, localhost, dt.API()+":8080/oc")
}
} else {*/
hostUrl = hostUrl + "/" + strings.ReplaceAll(dt.API(), "oc-", "")
//}
fmt.Println("Contacting", hostUrl)
return hostUrl
} }
// checkPeerStatus checks the status of a peer // checkPeerStatus checks the status of a peer
func CheckPeerStatus(peerID string, appName string) (*Peer, bool) { func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool) {
api := tools.API{} api := tools.API{}
access := NewShallowAccessor() access := NewShallowAccessor()
res, code, _ := access.LoadOne(peerID) // Load the peer from db res, code, _ := access.LoadOne(peerID) // Load the peer from db
if code != 200 { // no peer no party if code != 200 { // no peer no party
return nil, false return nil, false
} }
url := urlFormat(res.(*Peer).APIUrl, tools.PEER) + "/status" // Format the URL url := p.urlFormat(res.(*Peer).Url, tools.PEER) + "/status" // Format the URL
state, services := api.CheckRemotePeer(url) state, services := api.CheckRemotePeer(url)
res.(*Peer).ServicesState = services // Update the services states of the peer res.(*Peer).ServicesState = services // Update the services states of the peer
access.UpdateOne(res.Serialize(res), peerID) // Update the peer in the db access.UpdateOne(res, peerID) // Update the peer in the db
return res.(*Peer), state != tools.DEAD && services[appName] == 0 // Return the peer and its status return res.(*Peer), state != tools.DEAD && services[appName] == 0 // Return the peer and its status
} }
// LaunchPeerExecution launches an execution on a peer // 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 // 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, func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
dt tools.DataType, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (map[string]interface{}, error) { dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
fmt.Println("Launching peer execution on", caller.GetUrls(), dt, method) fmt.Println("Launching peer execution on", caller.URLS, dt, method)
methods := caller.GetUrls()[dt] // Get the methods url of the data type methods := caller.URLS[dt] // Get the methods url of the data type
if m, ok := methods[method]; !ok || m == "" { if m, ok := methods[method]; !ok || m == "" {
return map[string]interface{}{}, errors.New("Requested method " + method.String() + " not declared in HTTPCaller") return nil, errors.New("Requested method " + method.String() + " not declared in HTTPCaller")
} }
path := methods[method] // Get the path corresponding to the action we want to execute path := 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) 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 := "" url := ""
// Check the status of the peer // Check the status of the peer
if mypeer, ok := CheckPeerStatus(peerID, dt.String()); !ok && mypeer != nil { if mypeer, ok := p.checkPeerStatus(peerID, dt.API()); !ok && mypeer != nil {
// If the peer is not reachable, add the execution to the failed executions list // If the peer is not reachable, add the execution to the failed executions list
pexec := &PeerExecution{ pexec := &PeerExecution{
Method: method.String(), Method: method.String(),
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) Url: p.urlFormat((mypeer.Url), dt) + path, // the url is constitued of : host URL + resource path + action path (ex : mypeer.com/datacenter/resourcetype/path/to/action)
Body: body, Body: body,
DataType: dt.EnumIndex(), DataType: dt.EnumIndex(),
DataID: dataID, DataID: dataID,
} }
mypeer.AddExecution(*pexec) mypeer.AddExecution(*pexec)
NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
return map[string]interface{}{}, errors.New("peer is " + peerID + " not reachable") return nil, errors.New("peer is " + peerID + " not reachable")
} else { } else {
if mypeer == nil { if mypeer == nil {
return map[string]interface{}{}, errors.New("peer " + peerID + " not found") return nil, errors.New("peer " + peerID + " not found")
} }
// If the peer is reachable, launch the execution // If the peer is reachable, launch the execution
url = urlFormat((mypeer.APIUrl), dt) + path // Format the URL url = p.urlFormat((mypeer.Url), dt) + path // Format the URL
tmp := mypeer.FailedExecution // Get the failed executions list tmp := mypeer.FailedExecution // Get the failed executions list
mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list
NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
for _, v := range tmp { // Retry the failed executions for _, v := range tmp { // Retry the failed executions
go p.Exec(v.Url, tools.ToMethod(v.Method), v.Body, caller) go p.exec(v.Url, tools.ToMethod(v.Method), v.Body, caller)
} }
} }
return p.Exec(url, method, body, caller) // Execute the method return nil, p.exec(url, method, body, caller) // Execute the method
} }
// exec executes the method on the peer // exec executes the method on the peer
func (p *PeerCache) Exec(url string, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (map[string]interface{}, error) { func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) error {
var b []byte var b []byte
var err error var err error
if method == tools.POST { // Execute the POST method if it's a POST method if method == tools.POST { // Execute the POST method if it's a POST method
@@ -102,16 +123,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 if method == tools.DELETE { // Execute the DELETE method if it's a DELETE method
b, err = caller.CallDelete(url, "") b, err = caller.CallDelete(url, "")
} }
var m map[string]interface{}
if err != nil { if err != nil {
return m, err return err
} }
var m map[string]interface{}
err = json.Unmarshal(b, &m) err = json.Unmarshal(b, &m)
if err != nil { if err != nil {
return m, err return err
} }
if e, ok := m["error"]; ok && e != "<nil>" && e != "" { // Check if there is an error in the response if e, ok := m["error"]; ok && e != "<nil>" && e != "" { // Check if there is an error in the response
return m, errors.New(fmt.Sprintf("%v", m["error"])) return errors.New(fmt.Sprintf("%v", m["error"]))
} }
return m, nil return nil
} }
+46 -35
View File
@@ -10,68 +10,80 @@ import (
) )
type peerMongoAccessor struct { type peerMongoAccessor struct {
utils.AbstractAccessor[*Peer] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
OverrideAuth bool overrideAuth bool
} }
// New creates a new instance of the peerMongoAccessor // New creates a new instance of the peerMongoAccessor
func NewShallowAccessor() *peerMongoAccessor { func NewShallowAccessor() *peerMongoAccessor {
return &peerMongoAccessor{ return &peerMongoAccessor{
OverrideAuth: true, overrideAuth: true,
AbstractAccessor: utils.AbstractAccessor[*Peer]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Type: tools.PEER, Type: tools.PEER,
New: func() *Peer { return &Peer{} },
}, },
} }
} }
func NewAccessor(request *tools.APIRequest) *peerMongoAccessor { func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
return &peerMongoAccessor{ return &peerMongoAccessor{
OverrideAuth: false, overrideAuth: false,
AbstractAccessor: utils.AbstractAccessor[*Peer]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.PEER, Type: tools.PEER,
New: func() *Peer { return &Peer{} },
}, },
} }
} }
func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool { 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 * Nothing special here, just the basic CRUD operations
*/ */
func (a *peerMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
func (wfa *peerMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, wfa)
}
func (wfa *peerMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set.(*Peer), id, wfa, &Peer{})
}
func (wfa *peerMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Peer), wfa)
}
func (wfa *peerMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, wfa)
}
func (dca *peerMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Peer](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, dca)
}
func (wfa *peerMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Peer](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (wfa *peerMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Peer](filters, search, wfa.getDefaultFilter(search),
func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (a *peerMongoAccessor) getDefaultFilter(search string) *dbs.Filters {
if i, err := strconv.Atoi(search); err == nil { if i, err := strconv.Atoi(search); err == nil {
m := map[string][]dbs.Filter{ // search by name if no filters are provided
"relation": {{Operator: dbs.EQUAL.String(), Value: i}},
}
if i == PARTNER.EnumIndex() {
m["verify"] = []dbs.Filter{{Operator: dbs.EQUAL.String(), Value: false}}
}
return &dbs.Filters{ return &dbs.Filters{
Or: m, Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"state": {{Operator: dbs.EQUAL.String(), Value: i}},
},
} }
} else { } else {
if search == "*" { if search == "*" {
@@ -81,7 +93,6 @@ func (a *peerMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
Or: map[string][]dbs.Filter{ // search by name if no filters are provided Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"url": {{Operator: dbs.LIKE.String(), Value: search}}, "url": {{Operator: dbs.LIKE.String(), Value: search}},
"peer_id": {{Operator: dbs.LIKE.String(), Value: search}},
}, },
} }
} }
-30
View File
@@ -1,30 +0,0 @@
package policy
import (
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
// Policy defines which inbound libp2p streams are authorized for a peer.
// Vital streams (planner, considers, minio/admiralty config, source-presign,
// verify, observe, heartbeat) are always allowed regardless of policy.
type Policy struct {
utils.AbstractObject
// Resource CRUD
AllowSearch bool `json:"allow_search" bson:"allow_search"`
AllowCreate bool `json:"allow_create" bson:"allow_create"`
AllowUpdate bool `json:"allow_update" bson:"allow_update"`
AllowDelete bool `json:"allow_delete" bson:"allow_delete"`
// Resource freshness tracking
AllowRegisterWatcher bool `json:"allow_register_watcher" bson:"allow_register_watcher"`
AllowUnregisterWatcher bool `json:"allow_unregister_watcher" bson:"allow_unregister_watcher"`
// Organization partner confirmation
AllowOrgPartnerConfirm bool `json:"allow_org_partner_confirm" bson:"allow_org_partner_confirm"`
}
func (p *Policy) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
@@ -1,31 +0,0 @@
package policy
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 policyMongoAccessor struct {
utils.AbstractAccessor[*Policy]
}
func NewAccessor(request *tools.APIRequest) *policyMongoAccessor {
return &policyMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Policy]{
Logger: logs.CreateLogger(tools.POLICY.String()),
Request: request,
Type: tools.POLICY,
New: func() *Policy { return &Policy{} },
},
}
}
func (a *policyMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
return &dbs.Filters{
Or: map[string][]dbs.Filter{
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
},
}
}
-109
View File
@@ -1,109 +0,0 @@
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)
}
Executable → Regular
+43 -94
View File
@@ -2,17 +2,14 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models" "cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
) )
/* /*
@@ -26,63 +23,47 @@ type ComputeResource struct {
} }
func (d *ComputeResource) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *ComputeResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ComputeResource](tools.COMPUTE_RESOURCE, request) return NewAccessor[*ComputeResource](tools.COMPUTE_RESOURCE, request, func() utils.DBObject { return &ComputeResource{} })
} }
func (r *ComputeResource) GetType() string { func (r *ComputeResource) GetType() string {
return tools.COMPUTE_RESOURCE.String() return tools.COMPUTE_RESOURCE.String()
} }
func (abs *ComputeResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) { func (abs *ComputeResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
if t != tools.COMPUTE_RESOURCE { if t != tools.COMPUTE_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Compute") return nil
} }
p, err := ConvertToPricedResource[*ComputeResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request) p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
if err != nil { priced := p.(*PricedResource)
return nil, err
}
priced := p.(*PricedResource[*ComputeResourcePricingProfile])
return &PricedComputeResource{ return &PricedComputeResource{
PricedResource: *priced, PricedResource: *priced,
}, nil }
}
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 ComputeResourceInstance struct { type ComputeResourceInstance struct {
ResourceInstance[*ComputeResourcePartnership] ResourceInstance[*ComputeResourcePartnership]
Source string `json:"source,omitempty" bson:"source,omitempty"` 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"` SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"` PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"` AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,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 map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*live.ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"` Nodes []*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 { type ComputeResourcePartnership struct {
ResourcePartnerShip[*ComputeResourcePricingProfile] ResourcePartnerShip[*ComputeResourcePricingProfile]
MinGaranteedCPUsCores map[string]float64 `json:"garanteed_cpus,omitempty" bson:"garanteed_cpus,omitempty"` MaxAllowedCPUsCores map[string]int `json:"allowed_cpus,omitempty" bson:"allowed_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"` MaxAllowedGPUsMemoryGB map[string]float64 `json:"allowed_gpus,omitempty" bson:"allowed_gpus,omitempty"`
MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"` MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"`
} }
@@ -95,20 +76,8 @@ type ComputeResourcePricingProfile struct {
RAMPrice float64 `json:"ram_price" bson:"ram_price" default:"-1"` // RAMPrice is the price of the RAM RAMPrice float64 `json:"ram_price" bson:"ram_price" default:"-1"` // RAMPrice is the price of the RAM
} }
func (p *ComputeResourcePricingProfile) IsPurchasable() bool { func (p *ComputeResourcePricingProfile) IsPurchased() bool {
fmt.Println("Buying", p.Pricing.BuyingStrategy) return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
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 { func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
@@ -117,20 +86,17 @@ func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
// NOT A PROPER QUANTITY // NOT A PROPER QUANTITY
// amountOfData is the number of CPUs, GPUs or RAM dependings on the params // amountOfData is the number of CPUs, GPUs or RAM dependings on the params
func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, variation []*pricing.PricingVariation, params ...string) (float64, error) { func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, params ...string) (float64, error) {
if len(params) < 1 { if len(params) < 1 {
return 0, errors.New("params must be set") return 0, errors.New("params must be set")
} }
pp := float64(0) pp := float64(0)
model := "" model := params[1]
if len(params) > 1 {
model = params[1]
}
if strings.Contains(params[0], "cpus") && len(params) > 1 { if strings.Contains(params[0], "cpus") && len(params) > 1 {
if _, ok := p.CPUsPrices[model]; ok { if _, ok := p.CPUsPrices[model]; ok {
p.Pricing.Price = p.CPUsPrices[model] p.Pricing.Price = p.CPUsPrices[model]
} }
r, err := p.Pricing.GetPriceHT(amountOfData, explicitDuration, start, &end, variation) r, err := p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -141,7 +107,7 @@ func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explici
if _, ok := p.GPUsPrices[model]; ok { if _, ok := p.GPUsPrices[model]; ok {
p.Pricing.Price = p.GPUsPrices[model] p.Pricing.Price = p.GPUsPrices[model]
} }
r, err := p.Pricing.GetPriceHT(amountOfData, explicitDuration, start, &end, variation) r, err := p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -151,7 +117,7 @@ func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explici
if p.RAMPrice >= 0 { if p.RAMPrice >= 0 {
p.Pricing.Price = p.RAMPrice p.Pricing.Price = p.RAMPrice
} }
r, err := p.Pricing.GetPriceHT(float64(amountOfData), explicitDuration, start, &end, variation) r, err := p.Pricing.GetPrice(float64(amountOfData), explicitDuration, start, &end)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -161,61 +127,44 @@ func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explici
} }
type PricedComputeResource struct { type PricedComputeResource struct {
PricedResource[*ComputeResourcePricingProfile] PricedResource
CPUsLocated map[string]float64 `json:"cpus_in_use" bson:"cpus_in_use"` // CPUsInUse is the list of CPUs in use CPUsLocated map[string]float64 `json:"cpus_in_use" bson:"cpus_in_use"` // CPUsInUse is the list of CPUs in use
GPUsLocated map[string]float64 `json:"gpus_in_use" bson:"gpus_in_use"` // GPUsInUse is the list of GPUs in use GPUsLocated map[string]float64 `json:"gpus_in_use" bson:"gpus_in_use"` // GPUsInUse is the list of GPUs in use
RAMLocated float64 `json:"ram_in_use" bson:"ram_in_use"` // RAMInUse is the RAM in use RAMLocated float64 `json:"ram_in_use" bson:"ram_in_use"` // RAMInUse is the RAM in use
} }
func (r *PricedComputeResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ComputeResourcePricingProfile{}
}
}
func (r *PricedComputeResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedComputeResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedComputeResource) GetType() tools.DataType { func (r *PricedComputeResource) GetType() tools.DataType {
return tools.COMPUTE_RESOURCE return tools.COMPUTE_RESOURCE
} }
func (r *PricedComputeResource) GetPriceHT() (float64, error) { func (r *PricedComputeResource) GetPrice() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
now := time.Now() now := time.Now()
if r.BookingConfiguration.UsageStart == nil { if r.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now r.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute)) add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add r.UsageEnd = &add
} }
pricing := r.SelectedPricing 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
price := float64(0) price := float64(0)
for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} { for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} {
for model, amountOfData := range l { for model, amountOfData := range l {
cpus, err := pricing.GetPriceHT(float64(amountOfData), cpus, err := pricing.GetPrice(float64(amountOfData), r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd, "cpus", model)
r.BookingConfiguration.ExplicitBookingDurationS, *r.BookingConfiguration.UsageStart,
*r.BookingConfiguration.UsageEnd, r.Variations, "cpus", model)
if err != nil { if err != nil {
return 0, err return 0, err
} }
price += cpus price += cpus
} }
} }
ram, err := pricing.GetPriceHT(r.RAMLocated, r.BookingConfiguration.ExplicitBookingDurationS, ram, err := pricing.GetPrice(r.RAMLocated, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd, "ram")
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations, "ram")
if err != nil { if err != nil {
return 0, err return 0, err
} }
-14
View File
@@ -1,14 +0,0 @@
package resources
// Consent represents a consent request attached to a resource.
// ConsentString is the question displayed to the user.
// Optional, when true, means the user may decline without blocking scheduling.
// A nil Optional is treated as required (false).
type Consent struct {
ConsentString string `json:"consent_string" bson:"consent_string"`
Optional *bool `json:"optional,omitempty" bson:"optional,omitempty"`
}
func (c Consent) IsOptional() bool {
return c.Optional != nil && *c.Optional
}
Executable → Regular
+47 -71
View File
@@ -2,13 +2,13 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/models" "cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
) )
/* /*
@@ -30,50 +30,46 @@ type DataResource struct {
} }
func (d *DataResource) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *DataResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*DataResource](tools.DATA_RESOURCE, request) // Create a new instance of the accessor return NewAccessor[*DataResource](tools.DATA_RESOURCE, request, func() utils.DBObject { return &DataResource{} }) // Create a new instance of the accessor
} }
func (r *DataResource) GetType() string { func (r *DataResource) GetType() string {
return tools.DATA_RESOURCE.String() return tools.DATA_RESOURCE.String()
} }
func (ri *DataResource) StoreDraftDefault() { func (abs *DataResource) ConvertToPricedResource(
ri.AbstractObject.StoreDraftDefault() t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
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 { if t != tools.DATA_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data") return nil
} }
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request) p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
if err != nil { priced := p.(*PricedResource)
return nil, err
}
priced := p.(*PricedResource[*DataResourcePricingProfile])
return &PricedDataResource{ return &PricedDataResource{
PricedResource: *priced, PricedResource: *priced,
}, nil }
} }
type DataInstance struct { type DataInstance struct {
ResourceInstance[*DataResourcePartnership] ResourceInstance[*DataResourcePartnership]
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
} }
func NewDataInstance(name string, peerID string) ResourceInstanceITF { func (ri *DataInstance) StoreDraftDefault() {
return &DataInstance{ found := false
ResourceInstance: ResourceInstance[*DataResourcePartnership]{ for _, p := range ri.ResourceInstance.Env {
AbstractObject: utils.AbstractObject{ if p.Attr == "source" {
UUID: uuid.New().String(), found = true
Name: name, break
},
},
} }
}
if !found {
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
Attr: "source",
Value: ri.Source,
Readonly: true,
})
}
ri.ResourceInstance.StoreDraftDefault()
} }
type DataResourcePartnership struct { type DataResourcePartnership struct {
@@ -86,7 +82,7 @@ type DataResourcePartnership struct {
type DataResourcePricingStrategy int type DataResourcePricingStrategy int
const ( const (
PER_DOWNLOAD DataResourcePricingStrategy = iota + 7 PER_DOWNLOAD DataResourcePricingStrategy = iota
PER_TB_DOWNLOADED PER_TB_DOWNLOADED
PER_GB_DOWNLOADED PER_GB_DOWNLOADED
PER_MB_DOWNLOADED PER_MB_DOWNLOADED
@@ -94,9 +90,7 @@ const (
) )
func (t DataResourcePricingStrategy) String() string { func (t DataResourcePricingStrategy) String() string {
l := pricing.TimePricingStrategyListStr() return [...]string{"PER DOWNLOAD", "PER TB DOWNLOADED", "PER GB DOWNLOADED", "PER MB DOWNLOADED", "PER KB DOWNLOADED"}[t]
l = append(l, []string{"PER DOWNLOAD", "PER TB DOWNLOADED", "PER GB DOWNLOADED", "PER MB DOWNLOADED", "PER KB DOWNLOADED"}...)
return l[t]
} }
func DataResourcePricingStrategyList() []DataResourcePricingStrategy { func DataResourcePricingStrategyList() []DataResourcePricingStrategy {
@@ -108,9 +102,7 @@ func ToDataResourcePricingStrategy(i int) DataResourcePricingStrategy {
} }
func (t DataResourcePricingStrategy) GetStrategy() string { func (t DataResourcePricingStrategy) GetStrategy() string {
l := pricing.TimePricingStrategyListStr() return [...]string{"PER_DOWNLOAD", "PER_GB", "PER_MB", "PER_KB"}[t]
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 { func (t DataResourcePricingStrategy) GetStrategyValue() int {
@@ -141,54 +133,40 @@ func (p *DataResourcePricingProfile) GetOverrideStrategyValue() int {
return p.Pricing.OverrideStrategy.GetStrategyValue() return p.Pricing.OverrideStrategy.GetStrategyValue()
} }
func (p *DataResourcePricingProfile) IsPurchasable() bool { func (p *DataResourcePricingProfile) GetPrice(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.BuyingStrategy == pricing.PERMANENT return p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
} }
func (p *DataResourcePricingProfile) IsBooked() bool { func (p *DataResourcePricingProfile) IsPurchased() bool {
// TODO WHAT ABOUT PAY PER USE... it's a complicate CASE return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
return p.Pricing.BuyingStrategy != pricing.PERMANENT
} }
type PricedDataResource struct { type PricedDataResource struct {
PricedResource[*DataResourcePricingProfile] PricedResource
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"` UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
} }
func (r *PricedDataResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &DataResourcePricingProfile{}
}
}
func (r *PricedDataResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedDataResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedDataResource) GetType() tools.DataType { func (r *PricedDataResource) GetType() tools.DataType {
return tools.DATA_RESOURCE return tools.DATA_RESOURCE
} }
func (r *PricedDataResource) GetPriceHT() (float64, error) { func (r *PricedDataResource) GetPrice() (float64, error) {
r.ensurePricing() fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
now := time.Now() now := time.Now()
if r.BookingConfiguration.UsageStart == nil { if r.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now r.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute)) add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add r.UsageEnd = &add
} }
pricing := r.SelectedPricing 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
var err error var err error
amountOfData := float64(1) amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 { if pricing.GetOverrideStrategyValue() >= 0 {
@@ -197,7 +175,5 @@ func (r *PricedDataResource) GetPriceHT() (float64, error) {
return 0, err 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)
} }
-505
View File
@@ -1,505 +0,0 @@
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 dbs.Filters `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"`
}
// WorkspaceCandidatesProvider can be set by the workspace package to supply
// contextual workspace resources for a given DataType and request without
// creating a circular import (workspace → resources → workspace).
// When set, SetAllowedInstances uses workspace-scoped resources instead of
// the full catalog for requests that carry a username.
var WorkspaceCandidatesProvider func(dt tools.DataType, request *tools.APIRequest) []ResourceInterface
func (d *DynamicResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return nil
}
func (d *DynamicResource) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
if WorkspaceCandidatesProvider != nil {
candidates := WorkspaceCandidatesProvider(d.Type, request)
return d.SetAllowedInstancesFromSet(candidates, request, instance_id...)
}
d.Instances = []ResourceInstanceITF{}
d.sortAndResetInstances()
return d.Instances
}
// SetAllowedInstancesFromSet fills d.Instances from a pre-loaded workspace resource set
// instead of querying the catalog. Filters are applied in-memory against the candidates.
// Called by WorkspaceResourceSet.Fill so dynamic resources only see workspace-scoped resources.
func (d *DynamicResource) SetAllowedInstancesFromSet(candidates []ResourceInterface, request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
d.Instances = []ResourceInstanceITF{}
d.PeerIds = map[int]string{}
d.ResourceIds = map[int]string{}
for _, res := range candidates {
if !d.matchesFilters(res) {
continue
}
for _, i := range res.SetAllowedInstances(request, instance_id...) {
d.PeerIds[len(d.Instances)] = res.GetCreatorID()
d.ResourceIds[len(d.Instances)] = res.GetID()
d.Instances = append(d.Instances, i)
}
}
d.sortAndResetInstances()
return d.Instances
}
func (d *DynamicResource) sortAndResetInstances() {
if d.SortRules != nil {
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.Instances = sorted
}
d.WatchedDynamicResource = []string{}
}
// matchesFilters applies d.Filters in-memory against a serialized resource.
// Keys in d.Filters are JSON tag names; Serialize returns JSON tag names — no bson conversion needed.
func (d *DynamicResource) matchesFilters(res ResourceInterface) bool {
if len(d.Filters.And) == 0 && len(d.Filters.Or) == 0 {
return true
}
m := res.Serialize(res)
for field, fs := range d.Filters.And {
vals := nestedVals(m, strings.Split(field, "."))
for _, f := range fs {
if !anyMatchesOp(vals, f) {
return false
}
}
}
if len(d.Filters.Or) > 0 {
matched := false
for field, fs := range d.Filters.Or {
vals := nestedVals(m, strings.Split(field, "."))
for _, f := range fs {
if anyMatchesOp(vals, f) {
matched = true
break
}
}
if matched {
break
}
}
if !matched {
return false
}
}
return true
}
// nestedVals navigates a dot-path into m and collects all leaf values.
// Arrays at any level are expanded: each element is recursed into.
func nestedVals(m map[string]interface{}, path []string) []interface{} {
if len(path) == 0 || m == nil {
return nil
}
val, ok := m[path[0]]
if !ok {
return nil
}
if len(path) == 1 {
if arr, ok := val.([]interface{}); ok {
return arr
}
return []interface{}{val}
}
rest := path[1:]
switch v := val.(type) {
case map[string]interface{}:
return nestedVals(v, rest)
case []interface{}:
var out []interface{}
for _, elem := range v {
if em, ok := elem.(map[string]interface{}); ok {
out = append(out, nestedVals(em, rest)...)
}
}
return out
}
return nil
}
// anyMatchesOp returns true if at least one value in vals satisfies filter f.
func anyMatchesOp(vals []interface{}, f dbs.Filter) bool {
if f.Operator == dbs.EXISTS.String() {
exists := len(vals) > 0 && vals[0] != nil
want := true
if b, ok := f.Value.(bool); ok {
want = b
}
return exists == want
}
if f.Operator == dbs.IN.String() {
list, ok := f.Value.([]interface{})
if !ok {
return false
}
for _, v := range vals {
sv := fmt.Sprintf("%v", v)
for _, item := range list {
if sv == fmt.Sprintf("%v", item) {
return true
}
}
}
return false
}
for _, v := range vals {
if opMatches(v, f) {
return true
}
}
return false
}
func opMatches(val interface{}, f dbs.Filter) bool {
switch f.Operator {
case dbs.EQUAL.String():
return fmt.Sprintf("%v", val) == fmt.Sprintf("%v", f.Value)
case dbs.NOT.String():
return fmt.Sprintf("%v", val) != fmt.Sprintf("%v", f.Value)
case dbs.LIKE.String():
return strings.Contains(strings.ToLower(fmt.Sprintf("%v", val)), strings.ToLower(fmt.Sprintf("%v", f.Value)))
case dbs.GT.String(), dbs.GTE.String(), dbs.LT.String(), dbs.LTE.String():
return numericCmp(val, f.Value, f.Operator)
case dbs.ELEMMATCH.String():
arr, ok := val.([]interface{})
if !ok {
return false
}
sub, ok := f.Value.(map[string]interface{})
if !ok {
return false
}
for _, elem := range arr {
em, ok := elem.(map[string]interface{})
if !ok {
continue
}
allOk := true
for k, sv := range sub {
if fmt.Sprintf("%v", em[k]) != fmt.Sprintf("%v", sv) {
allOk = false
break
}
}
if allOk {
return true
}
}
return false
}
return false
}
func numericCmp(a, b interface{}, op string) bool {
fa, aOk := toFloat64(a)
fb, bOk := toFloat64(b)
if !aOk || !bOk {
sa, sb := fmt.Sprintf("%v", a), fmt.Sprintf("%v", b)
switch op {
case dbs.GT.String():
return sa > sb
case dbs.GTE.String():
return sa >= sb
case dbs.LT.String():
return sa < sb
case dbs.LTE.String():
return sa <= sb
}
return false
}
switch op {
case dbs.GT.String():
return fa > fb
case dbs.GTE.String():
return fa >= fb
case dbs.LT.String():
return fa < fb
case dbs.LTE.String():
return fa <= fb
}
return false
}
func toFloat64(v interface{}) (float64, bool) {
switch n := v.(type) {
case float64:
return n, true
case float32:
return float64(n), true
case int:
return float64(n), true
case int32:
return float64(n), true
case int64:
return float64(n), true
}
return 0, false
}
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() {
fmt.Println(inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy), d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy)
if inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy) != nil {
d.SelectedPartnershipIndex = &i
break
}
}
if d.SelectedPartnershipIndex == nil {
i := 0
d.SelectedPartnershipIndex = &i
}
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")
}
@@ -1,223 +0,0 @@
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
}
Executable → Regular
+6 -30
View File
@@ -1,58 +1,34 @@
package resources package resources
import ( 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/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type PricedResourceITF interface {
pricing.PricedItemITF
}
type ResourceInterface interface { type ResourceInterface interface {
utils.DBObject utils.DBObject
FilterPeer(peerID string) *dbs.Filters Trim()
GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF
ConvertToPricedResource(t tools.DataType, a *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, b *int, request *tools.APIRequest) (pricing.PricedItemITF, error)
GetType() string GetType() string
GetSelectedInstance() utils.DBObject
ClearEnv() utils.DBObject ClearEnv() utils.DBObject
VerifyBuy() SetAllowedInstances(request *tools.APIRequest)
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
GetExploitationAuthorizations() []ExploitationAuthorization
GetConsents() []Consent
} }
type ResourceInstanceITF interface { type ResourceInstanceITF interface {
utils.DBObject utils.DBObject
GetID() string GetID() string
GetName() string GetName() string
GetOrigin() OriginMeta StoreDraftDefault()
IsPeerless() bool ClearEnv()
FilterInstance(peerID string)
GetProfile(peerID string, partnershipIndex *int, buying *int, strategy *int) pricing.PricingProfileITF
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string)
ClearPeerGroups() ClearPeerGroups()
GetPartnerships() []ResourcePartnerITF
GetAverageDurationS() float64
UpdateAverageDuration(actualS float64)
} }
type ResourcePartnerITF interface { type ResourcePartnerITF interface {
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() map[string][]string GetPeerGroups() map[string][]string
ClearPeerGroups() ClearPeerGroups()
GetProfile(buying *int, strategy *int) pricing.PricingProfileITF
FilterPartnership(peerID string)
} }
Executable → Regular
-145
View File
@@ -11,143 +11,12 @@ type ResourceSet struct {
Processings []string `bson:"processings,omitempty" json:"processings,omitempty"` Processings []string `bson:"processings,omitempty" json:"processings,omitempty"`
Computes []string `bson:"computes,omitempty" json:"computes,omitempty"` Computes []string `bson:"computes,omitempty" json:"computes,omitempty"`
Workflows []string `bson:"workflows,omitempty" json:"workflows,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"`
// Runtime-only resource objects — not persisted. Populated by Fill() from the ID lists above.
// Use WorkspaceResourceSet when full object persistence is needed (workspace fluid catalog).
DynamicResources []*DynamicResource `bson:"-" json:"dynamic_resources,omitempty"`
DataResources []*DataResource `bson:"-" json:"data_resources,omitempty"` DataResources []*DataResource `bson:"-" json:"data_resources,omitempty"`
StorageResources []*StorageResource `bson:"-" json:"storage_resources,omitempty"` StorageResources []*StorageResource `bson:"-" json:"storage_resources,omitempty"`
ProcessingResources []*ProcessingResource `bson:"-" json:"processing_resources,omitempty"` ProcessingResources []*ProcessingResource `bson:"-" json:"processing_resources,omitempty"`
ComputeResources []*ComputeResource `bson:"-" json:"compute_resources,omitempty"` ComputeResources []*ComputeResource `bson:"-" json:"compute_resources,omitempty"`
WorkflowResources []*WorkflowResource `bson:"-" json:"workflow_resources,omitempty"` WorkflowResources []*WorkflowResource `bson:"-" json:"workflow_resources,omitempty"`
NativeTools []*NativeTool `bson:"-" json:"native_tools,omitempty"`
ServiceResources []*ServiceResource `bson:"-" json:"service_resources,omitempty"`
}
// WorkspaceResourceSet mirrors ResourceSet but persists complete resource objects to MongoDB.
// Use this in workspace documents where the workspace acts as a fluid resource catalog.
// The *Resource fields are loaded from bson on read; Fill() skips catalog lookup when they are
// already populated.
type WorkspaceResourceSet struct {
Datas []string `bson:"datas,omitempty" json:"datas,omitempty"`
Storages []string `bson:"storages,omitempty" json:"storages,omitempty"`
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 []*DynamicResource `bson:"dynamic_resources,omitempty" json:"dynamic_resources,omitempty"`
DataResources []*DataResource `bson:"data_resources,omitempty" json:"data_resources,omitempty"`
StorageResources []*StorageResource `bson:"storage_resources,omitempty" json:"storage_resources,omitempty"`
ProcessingResources []*ProcessingResource `bson:"processing_resources,omitempty" json:"processing_resources,omitempty"`
ComputeResources []*ComputeResource `bson:"compute_resources,omitempty" json:"compute_resources,omitempty"`
WorkflowResources []*WorkflowResource `bson:"workflow_resources,omitempty" json:"workflow_resources,omitempty"`
NativeTools []*NativeTool `bson:"native_tools,omitempty" json:"native_tools,omitempty"`
ServiceResources []*ServiceResource `bson:"service_resources,omitempty" json:"service_resources,omitempty"`
}
func (r *WorkspaceResourceSet) Clear() {
r.DataResources = nil
r.StorageResources = nil
r.ProcessingResources = nil
r.ComputeResources = nil
r.WorkflowResources = nil
r.ServiceResources = nil
r.DynamicResources = nil
r.NativeTools = nil
}
// Fill populates *Resource fields from their ID lists. When a field is already non-nil
// (loaded from the workspace MongoDB document), the catalog lookup is skipped for that type.
func (r *WorkspaceResourceSet) Fill(request *tools.APIRequest) {
if r.DataResources == nil {
for _, id := range r.Datas {
if d, _, e := (&DataResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.DataResources = append(r.DataResources, d.(*DataResource))
}
}
}
if r.ComputeResources == nil {
for _, id := range r.Computes {
if d, _, e := (&ComputeResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.ComputeResources = append(r.ComputeResources, d.(*ComputeResource))
}
}
}
if r.StorageResources == nil {
for _, id := range r.Storages {
if d, _, e := (&StorageResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.StorageResources = append(r.StorageResources, d.(*StorageResource))
}
}
}
if r.ProcessingResources == nil {
for _, id := range r.Processings {
if d, _, e := (&ProcessingResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.ProcessingResources = append(r.ProcessingResources, d.(*ProcessingResource))
}
}
}
if r.WorkflowResources == nil {
for _, id := range r.Workflows {
if d, _, e := (&WorkflowResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.WorkflowResources = append(r.WorkflowResources, d.(*WorkflowResource))
}
}
}
if r.ServiceResources == nil {
for _, id := range r.Services {
if d, _, e := (&ServiceResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.ServiceResources = append(r.ServiceResources, d.(*ServiceResource))
}
}
}
if r.DynamicResources == nil {
for _, id := range r.Dynamics {
if d, _, e := (&DynamicResource{}).GetAccessor(request).LoadOne(id); e == nil {
r.DynamicResources = append(r.DynamicResources, d.(*DynamicResource))
}
}
}
for _, d := range r.DynamicResources {
var candidates []ResourceInterface
switch d.Type {
case tools.COMPUTE_RESOURCE:
for _, c := range r.ComputeResources {
candidates = append(candidates, c)
}
case tools.DATA_RESOURCE:
for _, c := range r.DataResources {
candidates = append(candidates, c)
}
case tools.STORAGE_RESOURCE:
for _, c := range r.StorageResources {
candidates = append(candidates, c)
}
case tools.PROCESSING_RESOURCE:
for _, c := range r.ProcessingResources {
candidates = append(candidates, c)
}
case tools.WORKFLOW_RESOURCE:
for _, c := range r.WorkflowResources {
candidates = append(candidates, c)
}
case tools.SERVICE_RESOURCE:
for _, c := range r.ServiceResources {
candidates = append(candidates, c)
}
}
if len(candidates) > 0 {
d.SetAllowedInstancesFromSet(candidates, request)
} else {
d.SetAllowedInstances(request)
}
}
} }
func (r *ResourceSet) Clear() { func (r *ResourceSet) Clear() {
@@ -156,8 +25,6 @@ func (r *ResourceSet) Clear() {
r.ProcessingResources = nil r.ProcessingResources = nil
r.ComputeResources = nil r.ComputeResources = nil
r.WorkflowResources = nil r.WorkflowResources = nil
r.ServiceResources = nil
r.DynamicResources = nil
} }
func (r *ResourceSet) Fill(request *tools.APIRequest) { func (r *ResourceSet) Fill(request *tools.APIRequest) {
@@ -168,8 +35,6 @@ func (r *ResourceSet) Fill(request *tools.APIRequest) {
(&StorageResource{}): r.Storages, (&StorageResource{}): r.Storages,
(&ProcessingResource{}): r.Processings, (&ProcessingResource{}): r.Processings,
(&WorkflowResource{}): r.Workflows, (&WorkflowResource{}): r.Workflows,
(&ServiceResource{}): r.Services,
(&DynamicResource{}): r.Dynamics,
} { } {
for _, id := range v { for _, id := range v {
d, _, e := k.GetAccessor(request).LoadOne(id) d, _, e := k.GetAccessor(request).LoadOne(id)
@@ -185,17 +50,10 @@ func (r *ResourceSet) Fill(request *tools.APIRequest) {
r.ProcessingResources = append(r.ProcessingResources, d.(*ProcessingResource)) r.ProcessingResources = append(r.ProcessingResources, d.(*ProcessingResource))
case *WorkflowResource: case *WorkflowResource:
r.WorkflowResources = append(r.WorkflowResources, d.(*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 { type ItemResource struct {
@@ -204,7 +62,4 @@ type ItemResource struct {
Storage *StorageResource `bson:"storage,omitempty" json:"storage,omitempty"` Storage *StorageResource `bson:"storage,omitempty" json:"storage,omitempty"`
Compute *ComputeResource `bson:"compute,omitempty" json:"compute,omitempty"` Compute *ComputeResource `bson:"compute,omitempty" json:"compute,omitempty"`
Workflow *WorkflowResource `bson:"workflow,omitempty" json:"workflow,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"`
} }
-81
View File
@@ -1,81 +0,0 @@
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)
}
}
}
-23
View File
@@ -1,23 +0,0 @@
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]
}
@@ -1,22 +0,0 @@
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
}
+45 -107
View File
@@ -2,154 +2,92 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type BookingConfiguration struct { type PricedResource struct {
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
Mode booking.BookingMode `json:"mode,omitempty" bson:"mode,omitempty"`
}
type PricedResource[T pricing.PricingProfileITF] struct {
Name string `json:"name,omitempty" bson:"name,omitempty"` Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"` Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"` InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
SelectedPricing T `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"` PricingProfiles []pricing.PricingProfileITF `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"` SelectedPricing *pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
BookingConfiguration *BookingConfiguration `json:"booking_configuration,omitempty" bson:"booking_configuration,omitempty"` ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
Variations []*pricing.PricingVariation `json:"pricing_variations" bson:"pricing_variations"` 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"` CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"` ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
InstanceID string `json:"instance_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"` ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
} }
func (abs *PricedResource[T]) GetQuantity() int { func (abs *PricedResource) GetID() string {
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 return abs.ResourceID
} }
func (abs *PricedResource[T]) GetName() string { func (abs *PricedResource) GetType() tools.DataType {
return abs.Name
}
func (abs *PricedResource[T]) GetInstanceID() string {
return abs.InstanceID
}
func (abs *PricedResource[T]) GetType() tools.DataType {
return abs.ResourceType return abs.ResourceType
} }
func (abs *PricedResource[T]) GetCreatorID() string { func (abs *PricedResource) GetCreatorID() string {
return abs.CreatorID return abs.CreatorID
} }
// IsPurchasable and IsBooked fall back to false when SelectedPricing is a nil interface. func (abs *PricedResource) IsPurchased() bool {
// Concrete types (PricedComputeResource, etc.) override these and guarantee non-nil pricing. if abs.SelectedPricing == nil {
func (abs *PricedResource[T]) IsPurchasable() bool {
if any(abs.SelectedPricing) == nil {
return false return false
} }
return abs.SelectedPricing.IsPurchasable() return (*abs.SelectedPricing).IsPurchased()
} }
func (abs *PricedResource[T]) IsBooked() bool { func (abs *PricedResource) GetLocationEnd() *time.Time {
if any(abs.SelectedPricing) == nil { return abs.UsageEnd
return false
}
return abs.SelectedPricing.IsBooked()
} }
func (abs *PricedResource[T]) GetLocationEnd() *time.Time { func (abs *PricedResource) GetLocationStart() *time.Time {
if abs.BookingConfiguration == nil { return abs.UsageStart
return nil
}
return abs.BookingConfiguration.UsageEnd
} }
func (abs *PricedResource[T]) GetLocationStart() *time.Time { func (abs *PricedResource) SetLocationStart(start time.Time) {
if abs.BookingConfiguration == nil { abs.UsageStart = &start
return nil
}
return abs.BookingConfiguration.UsageStart
} }
func (abs *PricedResource[T]) SetLocationStart(start time.Time) { func (abs *PricedResource) SetLocationEnd(end time.Time) {
if abs.BookingConfiguration == nil { abs.UsageEnd = &end
abs.BookingConfiguration = &BookingConfiguration{}
}
abs.BookingConfiguration.UsageStart = &start
} }
func (abs *PricedResource[T]) SetLocationEnd(end time.Time) { func (abs *PricedResource) GetExplicitDurationInS() float64 {
if abs.BookingConfiguration == nil { if abs.ExplicitBookingDurationS == 0 {
abs.BookingConfiguration = &BookingConfiguration{} if abs.UsageEnd == nil && abs.UsageStart == nil {
return time.Duration(1 * time.Hour).Seconds()
} }
abs.BookingConfiguration.UsageEnd = &end if abs.UsageEnd == nil {
add := abs.UsageStart.Add(time.Duration(1 * time.Hour))
abs.UsageEnd = &add
}
return abs.UsageEnd.Sub(*abs.UsageStart).Seconds()
}
return abs.ExplicitBookingDurationS
} }
func (abs *PricedResource[T]) GetBookingMode() booking.BookingMode { func (r *PricedResource) GetPrice() (float64, error) {
if abs.BookingConfiguration == nil { fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
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() now := time.Now()
if r.BookingConfiguration == nil { if r.UsageStart == nil {
r.BookingConfiguration = &BookingConfiguration{} r.UsageStart = &now
} }
if r.BookingConfiguration.UsageStart == nil { if r.UsageEnd == nil {
r.BookingConfiguration.UsageStart = &now add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
} }
if r.BookingConfiguration.UsageEnd == nil { if r.SelectedPricing == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute)) if len(r.PricingProfiles) == 0 {
r.BookingConfiguration.UsageEnd = &add return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID)
} }
if any(r.SelectedPricing) == nil { r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set for resource " + r.ResourceID)
} }
pricing := r.SelectedPricing pricing := *r.SelectedPricing
return pricing.GetPriceHT(1, 0, *r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations) return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd)
} }
Executable → Regular
+23 -75
View File
@@ -1,7 +1,6 @@
package resources package resources
import ( import (
"errors"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
@@ -9,7 +8,6 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
) )
type ProcessingUsage struct { type ProcessingUsage struct {
@@ -28,66 +26,30 @@ type ProcessingUsage struct {
*/ */
type ProcessingResource struct { type ProcessingResource struct {
AbstractInstanciatedResource[*ProcessingInstance] AbstractInstanciatedResource[*ProcessingInstance]
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"` 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
OpenSource bool `json:"open_source" bson:"open_source" default:"false"` OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
// IsService marks a long-running processing that acts as a persistent service. License string `json:"license,omitempty" bson:"license,omitempty"`
// 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"` 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 { func (r *ProcessingResource) GetType() string {
return tools.PROCESSING_RESOURCE.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 { type ProcessingInstance struct {
ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]] ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access Access *ProcessingResourceAccess `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 { type PricedProcessingResource struct {
PricedResource[*ProcessingResourcePricingProfile] PricedResource
} IsService bool
func (r *PricedProcessingResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ProcessingResourcePricingProfile{}
}
}
func (r *PricedProcessingResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedProcessingResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedProcessingResource) GetPriceHT() (float64, error) {
r.ensurePricing()
return r.PricedResource.GetPriceHT()
} }
func (r *PricedProcessingResource) GetType() tools.DataType { func (r *PricedProcessingResource) GetType() tools.DataType {
@@ -95,44 +57,30 @@ func (r *PricedProcessingResource) GetType() tools.DataType {
} }
func (a *PricedProcessingResource) GetExplicitDurationInS() float64 { func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
if a.BookingConfiguration == nil { if a.ExplicitBookingDurationS == 0 {
a.BookingConfiguration = &BookingConfiguration{} if a.IsService || a.UsageStart == nil {
if a.IsService {
return -1
} }
if a.BookingConfiguration.ExplicitBookingDurationS == 0 { return time.Duration(1 * time.Hour).Seconds()
if a.BookingConfiguration.UsageStart == nil {
return (5 * time.Minute).Seconds()
} }
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds() return a.UsageEnd.Sub(*a.UsageStart).Seconds()
} }
return a.BookingConfiguration.ExplicitBookingDurationS return a.ExplicitBookingDurationS
} }
func (d *ProcessingResource) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *ProcessingResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request) // Create a new instance of the accessor return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request, func() utils.DBObject { return &ProcessingResource{} }) // Create a new instance of the accessor
}
func (abs *ProcessingResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.PROCESSING_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
}
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*DataResourcePricingProfile])
return &PricedDataResource{
PricedResource: *priced,
}, nil
} }
type ProcessingResourcePricingProfile struct { type ProcessingResourcePricingProfile struct {
pricing.AccessPricingProfile[pricing.TimePricingStrategy] // AccessPricingProfile is the pricing profile of a data it means that we can access the data for an amount of time pricing.AccessPricingProfile[pricing.TimePricingStrategy] // AccessPricingProfile is the pricing profile of a data it means that we can access the data for an amount of time
} }
func (p *ProcessingResourcePricingProfile) IsPurchasable() bool { func (p *ProcessingResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy == pricing.PERMANENT return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
} }
func (p *ProcessingResourcePricingProfile) IsBooked() bool { func (p *ProcessingResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.BuyingStrategy != pricing.PERMANENT return p.Pricing.GetPrice(amountOfData, val, start, &end)
} }
@@ -9,50 +9,11 @@ import (
type PurchaseResource struct { type PurchaseResource struct {
utils.AbstractObject utils.AbstractObject
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"` EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"` ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"` ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"`
// Authorization: identifies who created this draft and the Check session it belongs to.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
} }
func (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 { func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request) // Create a new instance of the accessor
} }
@@ -3,23 +3,23 @@ package purchase_resource
import ( import (
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type PurchaseResourceMongoAccessor struct { type purchaseResourceMongoAccessor struct {
utils.AbstractAccessor[*PurchaseResource] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the bookingMongoAccessor // New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor { func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
return &PurchaseResourceMongoAccessor{ return &purchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*PurchaseResource]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type
Request: request, Request: request,
Type: tools.PURCHASE_RESOURCE, Type: tools.PURCHASE_RESOURCE,
New: func() *PurchaseResource { return &PurchaseResource{} },
}, },
} }
} }
@@ -27,26 +27,46 @@ func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
/* /*
* Nothing special here, just the basic CRUD operations * Nothing special here, just the basic CRUD operations
*/ */
func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *purchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (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) {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDelete(d, a) utils.GenericDeleteOne(id, a)
return nil, 404, nil return nil, 404, nil
} }
return d, 200, nil return d, 200, nil
}, a) }, a)
} }
func (a *PurchaseResourceMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject { 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 {
return func(d utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDelete(d, a) utils.GenericDeleteOne(d.GetID(), a)
return nil return nil
} }
return d return d
} }
} }
func (dca *PurchaseResourceMongoAccessor) ShouldVerifyAuth() bool {
return false // TEMP : by pass
}
@@ -1,56 +0,0 @@
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())
})
}
Executable → Regular
+80 -449
View File
@@ -1,239 +1,86 @@
package resources package resources
import ( import (
"encoding/json"
"errors"
"fmt"
"slices" "slices"
"time"
"cloud.o-forge.io/core/oc-lib/config" "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/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/biter777/countries" "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 // AbstractResource is the struct containing all of the attributes commons to all ressources
type AbstractResource struct { type AbstractResource struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name) utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
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 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 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 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 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 Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource
UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"` UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"` SelectedInstanceIndex *int `json:"selected_instance_index,omitempty" bson:"selected_instance_index,omitempty"` // SelectedInstance is the selected instance
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"`
// Consents lists the consent questions the user must acknowledge before
// scheduling this resource. Consents with Optional=true may be skipped.
Consents []Consent `json:"consents,omitempty" bson:"consents,omitempty"`
} }
func (ri *AbstractResource) Extend(typ ...string) map[string][]tools.DataType { func (r *AbstractResource) GetSelectedInstance() utils.DBObject {
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 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
}
// GetConsents returns the consent questions declared by this resource.
func (r *AbstractResource) GetConsents() []Consent {
return r.Consents
}
// 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 { func (r *AbstractResource) GetType() string {
return tools.INVALID.String() return tools.INVALID.String()
} }
func (r *AbstractResource) StoreDraftDefault() { func (r *AbstractResource) StoreDraftDefault() {
//r.IsDraft = true pour le moment on passe outre. r.IsDraft = true
} }
func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) { func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
fmt.Println("IsDrafted", r.IsDraft, set.IsDrafted()) if r.IsDraft != set.IsDrafted() && set.IsDrafted() {
return r.IsDraft || set.IsDrafted(), set 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
} }
type AbstractInstanciatedResource[T ResourceInstanceITF] struct { type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
AbstractResource // AbstractResource contains the basic fields of an object (id, name) AbstractResource // AbstractResource contains the basic fields of an object (id, name)
Instances []T `json:"instances,omitempty" bson:"instances,omitempty"` // Bill is the bill of the resource // Bill is the bill of the resource
Instances []T `json:"instances,omitempty" bson:"instances,omitempty"`
} }
func (abs *AbstractInstanciatedResource[T]) AddInstances(instance ResourceInstanceITF) { func (abs *AbstractInstanciatedResource[T]) ConvertToPricedResource(
abs.Instances = append(abs.Instances, instance.(T)) t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
}
func ConvertToPricedResource[T pricing.PricingProfileITF](t tools.DataType,
selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int,
selectedBookingModeIndex *int, abs ResourceInterface, request *tools.APIRequest) (pricing.PricedItemITF, error) {
instances := map[string]string{} instances := map[string]string{}
var profile pricing.PricingProfileITF profiles := []pricing.PricingProfileITF{}
var inst ResourceInstanceITF for _, instance := range abs.Instances {
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() instances[instance.GetID()] = instance.GetName()
profiles := instance.GetPricingsProfiles(request.PeerID, request.Groups) profiles = instance.GetPricingsProfiles(request.PeerID, request.Groups)
if len(profiles) > 0 {
profile = profiles[0]
break
} }
} return &PricedResource{
} Name: abs.Name,
if profile == nil { Logo: abs.Logo,
/*if ok, _ := utils.IsMySelf(request.PeerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{ ResourceID: abs.UUID,
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, ResourceType: t,
Quantity: 1,
InstancesRefs: instances, InstancesRefs: instances,
SelectedPricing: selectedPricing, PricingProfiles: profiles,
Variations: variations, CreatorID: abs.CreatorID,
CreatorID: abs.GetCreatorID(), }
BookingConfiguration: bc,
}, nil
} }
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) ResourceInstanceITF { func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
if selected != nil && len(r.Instances) > *selected { for _, instance := range abs.Instances {
return r.Instances[*selected] 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]
} }
if len(r.Instances) > 0 { if len(r.Instances) > 0 {
return r.Instances[0] return r.Instances[0]
@@ -241,53 +88,41 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
return nil return nil
} }
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF { func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest) {
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) { if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" {
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...) return
// Filter AEs: only return AEs visible to the requesting peer.
if request != nil {
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
} }
} abs.Instances = verifyAuthAction[T](abs.Instances, request)
inst := []ResourceInstanceITF{}
for _, i := range abs.Instances {
inst = append(inst, i)
}
return inst
} }
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(callName string, request *tools.APIRequest) bool { func (d *AbstractInstanciatedResource[T]) Trim() {
return len(VerifyAuthAction(abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(callName, request) d.Type = d.GetType()
if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: d.CreatorID}}).IsMySelf(); !ok {
for _, instance := range d.Instances {
instance.ClearPeerGroups()
}
}
} }
func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest, instanceID ...string) []T { func (abs *AbstractInstanciatedResource[T]) VerifyAuth(request *tools.APIRequest) bool {
return len(verifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request)
}
func verifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T {
instances := []T{} instances := []T{}
for _, instance := range baseInstance { 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() _, peerGroups := instance.GetPeerGroups()
for _, peers := range peerGroups { for _, peers := range peerGroups {
if request == nil { if request == nil {
continue continue
} }
_, allOK := peers["*"] if grps, ok := peers[request.PeerID]; ok || config.GetConfig().Whitelist {
if grps, ok := peers[request.PeerID]; ok || allOK || config.GetConfig().Whitelist { if (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
if allOK || (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance) instances = append(instances, instance)
// TODO filter Partners + Profiles...
continue continue
} }
for _, grp := range grps { for _, grp := range grps {
if slices.Contains(request.Groups, grp) { if slices.Contains(request.Groups, grp) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance) instances = append(instances, instance)
} }
} }
@@ -297,116 +132,48 @@ func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.AP
return instances 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 { type ResourceInstance[T ResourcePartnerITF] struct {
utils.AbstractObject utils.AbstractObject
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"` Location GeoPoint `json:"location,omitempty" bson:"location,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"` Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,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"`
} }
// TODO should kicks all selection func (ri *ResourceInstance[T]) ClearEnv() {
ri.Env = []models.Param{}
func NewInstance[T ResourcePartnerITF](name string) *ResourceInstance[T] { ri.Inputs = []models.Param{}
return &ResourceInstance[T]{ ri.Outputs = []models.Param{}
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 { func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
if ri.IsPeerless() {
return []pricing.PricingProfileITF{pricing.GetDefaultPricingProfile()}
}
pricings := []pricing.PricingProfileITF{} pricings := []pricing.PricingProfileITF{}
for _, p := range ri.Partnerships { for _, p := range ri.Partnerships {
pricings = append(pricings, p.GetPricingsProfiles(peerID, groups)...) 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 return pricings
} }
func (ri *ResourceInstance[T]) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) { 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{} groups := []map[string][]string{}
partners := []ResourcePartnerITF{} partners := []ResourcePartnerITF{}
for _, p := range ri.Partnerships { for _, p := range ri.Partnerships {
partners = append(partners, p) partners = append(partners, p)
groups = append(groups, p.GetPeerGroups()) 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 return partners, groups
} }
@@ -416,176 +183,40 @@ 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 { type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"` Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"`
PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"` PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
PricingProfiles map[int]map[int]T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"` PricingProfiles []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 { func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
profiles := []pricing.PricingProfileITF{} profiles := []pricing.PricingProfileITF{}
if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil { if ri.PeerGroups[peerID] != nil {
return profiles
}
if ri.PeerGroups["*"] != nil {
for _, ri := range ri.PricingProfiles { for _, ri := range ri.PricingProfiles {
for _, i := range ri { profiles = append(profiles, ri)
profiles = append(profiles, i)
} }
if slices.Contains(groups, "*") {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
} }
return profiles return profiles
} }
for _, p := range ri.PeerGroups[peerID] { for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) || slices.Contains(groups, "*") { if slices.Contains(groups, p) {
for _, ri := range ri.PricingProfiles { for _, ri := range ri.PricingProfiles {
for _, i := range ri { profiles = append(profiles, ri)
profiles = append(profiles, i)
}
} }
return profiles 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 return profiles
} }
func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string { 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 return rp.PeerGroups
} }
func (rp *ResourcePartnerShip[T]) ClearPeerGroups() { func (rp *ResourcePartnerShip[T]) ClearPeerGroups() {
rp.PeerGroups = map[string][]string{} 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
}
+40 -337
View File
@@ -1,389 +1,92 @@
package resources package resources
import ( import (
"encoding/json"
"errors"
"fmt"
"slices" "slices"
"cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"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/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type ResourceMongoAccessor[T ResourceInterface] struct { type resourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
} generateData func() utils.DBObject
func sourceFromAccess(access *ResourceAccess) string {
if access == nil {
return ""
}
if access.Container != nil && access.Container.Source != "" {
return access.Container.Source
}
if access.Source != nil && access.Source.Source != "" {
return access.Source.Source
}
return ""
}
func upsertSourceParam(outputs []models.Param, source string) []models.Param {
for i, p := range outputs {
if p.Attr == "source" {
outputs[i].Value = source
return outputs
}
}
return append(outputs, models.Param{Attr: "source", Value: source, Readonly: true})
}
func applyAccessSourceOutput(data utils.DBObject) {
switch r := data.(type) {
case *ProcessingResource:
for _, inst := range r.Instances {
if src := sourceFromAccess(inst.Access); src != "" {
r.Outputs = upsertSourceParam(r.Outputs, src)
return
}
}
case *DataResource:
for _, inst := range r.Instances {
if src := sourceFromAccess(inst.Access); src != "" {
r.Outputs = upsertSourceParam(r.Outputs, src)
return
}
}
}
} }
// New creates a new instance of the computeMongoAccessor // New creates a new instance of the computeMongoAccessor
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest) *ResourceMongoAccessor[T] { func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *resourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{ if !slices.Contains([]tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE, tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE}, t) {
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 nil
} }
return &ResourceMongoAccessor[T]{ return &resourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor[ResourceInterface]{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request, Request: request,
Type: t, Type: t,
New: func() ResourceInterface {
switch t {
case tools.COMPUTE_RESOURCE:
return &ComputeResource{}
case tools.STORAGE_RESOURCE:
return &StorageResource{}
case tools.PROCESSING_RESOURCE:
return &ProcessingResource{}
case tools.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 * Nothing special here, just the basic CRUD operations
*/ */
func (dca *resourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) { return utils.GenericDeleteOne(id, dca)
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
} }
var workspaceResourceTypes = []tools.DataType{ func (dca *resourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
tools.COMPUTE_RESOURCE, set.(T).Trim()
tools.DATA_RESOURCE, return utils.GenericUpdateOne(set, id, dca, dca.generateData())
tools.PROCESSING_RESOURCE,
tools.STORAGE_RESOURCE,
tools.WORKFLOW_RESOURCE,
tools.SERVICE_RESOURCE,
} }
func emitResourceNATS(method tools.NATSMethod, dt tools.DataType, payload []byte) { func (dca *resourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
if !slices.Contains(workspaceResourceTypes, dt) { data.(T).Trim()
return return utils.GenericStoreOne(data, dca)
}
tools.NewNATSCaller().SetNATSPub(method, tools.NATSResponse{
FromApp: config.GetAppName(),
Datatype: dt,
Method: int(method),
Payload: payload,
})
} }
func (dca *ResourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) { func (dca *resourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
data, code, err := dca.AbstractAccessor.LoadOne(id)
if err != nil {
return data, code, err
}
res, code, err := dca.AbstractAccessor.DeleteOne(id)
if err == nil && data != nil {
b, _ := json.Marshal(data)
go emitResourceNATS(tools.REMOVE_RESOURCE, dca.GetType(), b)
}
return res, code, err
}
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")
}
if dca.GetType() == tools.PROCESSING_RESOURCE || dca.GetType() == tools.DATA_RESOURCE {
if merged, _, _, err := utils.ModelGenericUpdateOne(set, id, dca); err == nil {
applyAccessSourceOutput(merged)
if serialized := merged.Serialize(merged); serialized != nil {
set["outputs"] = serialized["outputs"]
}
}
}
return utils.GenericUpdateOne(set, id, dca)
}
func (dca *ResourceMongoAccessor[T]) ShouldVerifyAuth() bool {
return false // TEMP : by pass
}
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())
fmt.Println(res, 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 service 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 storage 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
}
applyAccessSourceOutput(data)
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)
}
if err == nil && res != nil {
b, _ := json.Marshal(res)
go emitResourceNATS(tools.CREATE_RESOURCE, dca.GetType(), b)
}
return res, code, err
}
// PurgedResourcePayload holds a silently-deleted resource's type and serialized payload.
type PurgedResourcePayload struct {
DT tools.DataType
Payload []byte
}
// purgeByType searches and silently deletes all resources of type T created by creatorID.
// Uses AbstractAccessor.DeleteOne directly to bypass the NATS-emitting override.
func purgeByType[T ResourceInterface](dt tools.DataType, creatorID string) []PurgedResourcePayload {
a := NewAccessor[T](dt, nil)
if a == nil {
return nil
}
res, _, _ := a.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"creator_id": {{Operator: dbs.EQUAL.String(), Value: creatorID}},
},
}, "", false, 0, 10000)
var result []PurgedResourcePayload
for _, item := range res {
b, err := json.Marshal(item)
if err != nil {
continue
}
a.AbstractAccessor.DeleteOne(item.GetID())
result = append(result, PurgedResourcePayload{DT: dt, Payload: b})
}
return result
}
// PurgeCreatorResources deletes all catalog resources created by creatorPeerID from
// the DB without emitting NATS. Used for non-blacklist peer privilege downgrades where
// workspace state should be left untouched.
func PurgeCreatorResources(creatorPeerID string) []PurgedResourcePayload {
var result []PurgedResourcePayload
result = append(result, purgeByType[*ComputeResource](tools.COMPUTE_RESOURCE, creatorPeerID)...)
result = append(result, purgeByType[*DataResource](tools.DATA_RESOURCE, creatorPeerID)...)
result = append(result, purgeByType[*ProcessingResource](tools.PROCESSING_RESOURCE, creatorPeerID)...)
result = append(result, purgeByType[*StorageResource](tools.STORAGE_RESOURCE, creatorPeerID)...)
result = append(result, purgeByType[*WorkflowResource](tools.WORKFLOW_RESOURCE, creatorPeerID)...)
result = append(result, purgeByType[*ServiceResource](tools.SERVICE_RESOURCE, creatorPeerID)...)
return result
}
// FilterMapFromResourcePayload deserializes a resource payload by DataType, zeros out
// the AbstractInstanciatedResource (and its AbstractResource / Instances sub-fields),
// then marshals back to get only the concrete type's own JSON fields.
// Returns nil for WORKFLOW_RESOURCE and unknown types.
// JSON keys only — not BSON paths.
func FilterMapFromResourcePayload(dt tools.DataType, payload []byte) map[string]interface{} {
var m map[string]interface{}
switch dt {
case tools.COMPUTE_RESOURCE:
var r ComputeResource
if json.Unmarshal(payload, &r) != nil {
return nil
}
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*ComputeResourceInstance]{}
b, _ := json.Marshal(r)
json.Unmarshal(b, &m)
case tools.DATA_RESOURCE:
var r DataResource
if json.Unmarshal(payload, &r) != nil {
return nil
}
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*DataInstance]{}
b, _ := json.Marshal(r)
json.Unmarshal(b, &m)
case tools.PROCESSING_RESOURCE:
var r ProcessingResource
if json.Unmarshal(payload, &r) != nil {
return nil
}
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*ProcessingInstance]{}
b, _ := json.Marshal(r)
json.Unmarshal(b, &m)
case tools.STORAGE_RESOURCE:
var r StorageResource
if json.Unmarshal(payload, &r) != nil {
return nil
}
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*StorageResourceInstance]{}
b, _ := json.Marshal(r)
json.Unmarshal(b, &m)
case tools.SERVICE_RESOURCE:
var r ServiceResource
if json.Unmarshal(payload, &r) != nil {
return nil
}
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*ServiceInstance]{}
b, _ := json.Marshal(r)
json.Unmarshal(b, &m)
case tools.WORKFLOW_RESOURCE:
var r WorkflowResource
if json.Unmarshal(payload, &r) != nil {
return nil
}
r.AbstractResource = AbstractResource{}
b, _ := json.Marshal(r)
json.Unmarshal(b, &m)
default:
return nil
}
return m
}
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return dca.StoreOne(data) return dca.StoreOne(data)
} }
func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) { func (dca *resourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit) 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]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) { func (wfa *resourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
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) {
if filters == nil && search == "*" { if filters == nil && search == "*" {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject { return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
fmt.Println("Search", d)
d.(T).VerifyBuy()
d.(T).SetAllowedInstances(wfa.Request) d.(T).SetAllowedInstances(wfa.Request)
fmt.Println("Search2", d)
return d return d
}, isDraft, wfa, offset, limit) }, isDraft, wfa)
} }
return utils.GenericSearch[T](filters, search, wfa.GetObjectFilters(search), return utils.GenericSearch[T](filters, search, wfa.getResourceFilter(search),
func(d utils.DBObject) utils.ShallowDBObject { func(d utils.DBObject) utils.ShallowDBObject {
d.(T).VerifyBuy()
d.(T).SetAllowedInstances(wfa.Request) d.(T).SetAllowedInstances(wfa.Request)
return d return d
}, isDraft, wfa, offset, limit) }, isDraft, wfa)
} }
func (a *ResourceMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject { func (abs *resourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters {
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{ return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided
"abstractinstanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.type": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractintanciatedresource.abstractresource.type": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.short_description": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractintanciatedresource.abstractresource.short_description": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.description": {{Operator: dbs.LIKE.String(), Value: search}}, "abstractintanciatedresource.abstractresource.description": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.owners.name": {{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}},
}, },
} }
} }
-198
View File
@@ -1,198 +0,0 @@
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()
}
Executable → Regular
+53 -86
View File
@@ -2,6 +2,7 @@ package resources
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
@@ -9,20 +10,8 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
) )
// 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 * StorageResource is a struct that represents a storage resource
* it defines the resource storage * it defines the resource storage
@@ -34,40 +23,29 @@ type StorageResource struct {
} }
func (d *StorageResource) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *StorageResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*StorageResource](tools.STORAGE_RESOURCE, request) // Create a new instance of the accessor return NewAccessor[*StorageResource](tools.STORAGE_RESOURCE, request, func() utils.DBObject { return &StorageResource{} }) // Create a new instance of the accessor
} }
func (r *StorageResource) GetType() string { func (r *StorageResource) GetType() string {
return tools.STORAGE_RESOURCE.String() return tools.STORAGE_RESOURCE.String()
} }
func (abs *StorageResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) { func (abs *StorageResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
if t != tools.STORAGE_RESOURCE { if t != tools.STORAGE_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Storage") return nil
} }
p, err := ConvertToPricedResource[*StorageResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request) p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
if err != nil { priced := p.(*PricedResource)
return nil, err
}
priced := p.(*PricedResource[*StorageResourcePricingProfile])
return &PricedStorageResource{ return &PricedStorageResource{
PricedResource: *priced, 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 { type StorageResourceInstance struct {
ResourceInstance[*StorageResourcePartnership] 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 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"` Local bool `bson:"local" json:"local"`
SecurityLevel string `bson:"security_level,omitempty" json:"security_level,omitempty"` 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 SizeType enum.StorageSize `bson:"size_type" json:"size_type" default:"0"` // SizeType is the type of the storage size
@@ -77,19 +55,29 @@ type StorageResourceInstance struct {
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
} }
// IsPeerless is always false for storage instances: a storage resource is func (ri *StorageResourceInstance) ClearEnv() {
// infrastructure owned by a peer and can never be declared peerless. ri.Credentials = nil
func (ri *StorageResourceInstance) IsPeerless() bool { return false } ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
}
func NewStorageResourceInstance(name string, peerID string) ResourceInstanceITF { func (ri *StorageResourceInstance) StoreDraftDefault() {
return &StorageResourceInstance{ found := false
ResourceInstance: ResourceInstance[*StorageResourcePartnership]{ for _, p := range ri.ResourceInstance.Env {
AbstractObject: utils.AbstractObject{ if p.Attr == "source" {
UUID: uuid.New().String(), found = true
Name: name, break
},
},
} }
}
if !found {
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
Attr: "source",
Value: ri.Source,
Readonly: true,
})
}
ri.ResourceInstance.StoreDraftDefault()
} }
type StorageResourcePartnership struct { type StorageResourcePartnership struct {
@@ -117,7 +105,7 @@ func (t PrivilegeStoragePricingStrategy) String() string {
type StorageResourcePricingStrategy int type StorageResourcePricingStrategy int
const ( const (
PER_DATA_STORED StorageResourcePricingStrategy = iota + 7 PER_DATA_STORED StorageResourcePricingStrategy = iota
PER_TB_STORED PER_TB_STORED
PER_GB_STORED PER_GB_STORED
PER_MB_STORED PER_MB_STORED
@@ -129,15 +117,11 @@ func StorageResourcePricingStrategyList() []StorageResourcePricingStrategy {
} }
func (t StorageResourcePricingStrategy) String() string { func (t StorageResourcePricingStrategy) String() string {
l := pricing.TimePricingStrategyListStr() return [...]string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}[t]
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 { func (t StorageResourcePricingStrategy) GetStrategy() string {
l := pricing.TimePricingStrategyListStr() return [...]string{"PER_DATA_STORED", "PER_GB_STORED", "PER_MB_STORED", "PER_KB_STORED"}[t]
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 { func (t StorageResourcePricingStrategy) GetStrategyValue() int {
@@ -168,56 +152,40 @@ 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 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) IsPurchasable() bool { func (p *StorageResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy == pricing.PERMANENT return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
} }
func (p *StorageResourcePricingProfile) IsBooked() bool { func (p *StorageResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
if p.Pricing.BuyingStrategy == pricing.PERMANENT { return p.Pricing.GetPrice(amountOfData, val, start, &end)
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
} }
type PricedStorageResource struct { type PricedStorageResource struct {
PricedResource[*StorageResourcePricingProfile] PricedResource
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"` UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
} }
func (r *PricedStorageResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &StorageResourcePricingProfile{}
}
}
func (r *PricedStorageResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedStorageResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedStorageResource) GetType() tools.DataType { func (r *PricedStorageResource) GetType() tools.DataType {
return tools.STORAGE_RESOURCE return tools.STORAGE_RESOURCE
} }
func (r *PricedStorageResource) GetPriceHT() (float64, error) { func (r *PricedStorageResource) GetPrice() (float64, error) {
r.ensurePricing() fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
now := time.Now() now := time.Now()
if r.BookingConfiguration.UsageStart == nil { if r.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now r.UsageStart = &now
} }
if r.BookingConfiguration.UsageEnd == nil { if r.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute)) add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.BookingConfiguration.UsageEnd = &add r.UsageEnd = &add
} }
pricing := r.SelectedPricing 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
var err error var err error
amountOfData := float64(1) amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 { if pricing.GetOverrideStrategyValue() >= 0 {
@@ -226,6 +194,5 @@ func (r *PricedStorageResource) GetPriceHT() (float64, error) {
return 0, err return 0, err
} }
} }
return pricing.GetPriceHT(amountOfData, r.BookingConfiguration.ExplicitBookingDurationS, return pricing.GetPrice(amountOfData, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd)
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
} }
-118
View File
@@ -1,118 +0,0 @@
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)
}
-104
View File
@@ -1,104 +0,0 @@
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())
}
@@ -1,149 +0,0 @@
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)
})
}
-106
View File
@@ -1,106 +0,0 @@
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())
}
-119
View File
@@ -1,119 +0,0 @@
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)
}
-74
View File
@@ -1,74 +0,0 @@
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)
}
-55
View File
@@ -1,55 +0,0 @@
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)
}
Executable → Regular
+10 -23
View File
@@ -16,10 +16,7 @@ type WorkflowResource struct {
} }
func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request) return NewAccessor[*ComputeResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
}
func (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) {
} }
func (r *WorkflowResource) GetType() string { func (r *WorkflowResource) GetType() string {
@@ -30,30 +27,20 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
return d return d
} }
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF { func (d *WorkflowResource) Trim() {
// WorkflowResource has no instances, but still carries AEs that must be /* EMPTY */
// filtered before the resource is returned to a non-owner, non-admin peer. }
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) { func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest) {
if request != nil { /* EMPTY */
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
} }
func (r *WorkflowResource) GetSelectedInstance(selected *int) ResourceInstanceITF { func (w *WorkflowResource) ConvertToPricedResource(
return nil t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
} return &PricedResource{
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, Name: w.Name,
Logo: w.Logo, Logo: w.Logo,
ResourceID: w.UUID, ResourceID: w.UUID,
ResourceType: t, ResourceType: t,
Quantity: 1,
CreatorID: w.CreatorID, CreatorID: w.CreatorID,
}, nil // TODO ??? }
} }
// TODO : as instanciated resource !
-38
View File
@@ -1,38 +0,0 @@
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)
}
}
Executable → Regular
+12 -139
View File
@@ -1,10 +1,7 @@
package utils package utils
import ( import (
"crypto/sha256"
"encoding/json" "encoding/json"
"errors"
"slices"
"time" "time"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
@@ -31,7 +28,6 @@ const (
*/ */
type AbstractObject struct { type AbstractObject struct {
UUID string `json:"id,omitempty" bson:"id,omitempty" validate:"required"` 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"` Name string `json:"name,omitempty" bson:"name,omitempty" validate:"required"`
IsDraft bool `json:"is_draft" bson:"is_draft" default:"false"` IsDraft bool `json:"is_draft" bson:"is_draft" default:"false"`
CreatorID string `json:"creator_id,omitempty" bson:"creator_id,omitempty"` CreatorID string `json:"creator_id,omitempty" bson:"creator_id,omitempty"`
@@ -41,70 +37,11 @@ type AbstractObject struct {
UpdaterID string `json:"updater_id,omitempty" bson:"updater_id,omitempty"` UpdaterID string `json:"updater_id,omitempty" bson:"updater_id,omitempty"`
UserUpdaterID string `json:"user_updater_id,omitempty" bson:"user_updater_id,omitempty"` UserUpdaterID string `json:"user_updater_id,omitempty" bson:"user_updater_id,omitempty"`
AccessMode AccessMode `json:"access_mode" bson:"access_mode" default:"0"` AccessMode AccessMode `json:"access_mode" bson:"access_mode" default:"0"`
Signature []byte `bson:"signature,omitempty" json:"signature,omitempty"`
}
func (ri *AbstractObject) 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 { func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor {
return nil 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) SetDraft(draft bool) {
r.IsDraft = draft
}
func (r *AbstractObject) SetName(name string) {
r.Name = name
}
func (r *AbstractObject) GenerateID() { func (r *AbstractObject) GenerateID() {
if r.UUID == "" { if r.UUID == "" {
@@ -133,10 +70,6 @@ func (ao AbstractObject) GetID() string {
return ao.UUID return ao.UUID
} }
func (ao AbstractObject) GetSignature() []byte {
return ao.Signature
}
// GetName implements ShallowDBObject. // GetName implements ShallowDBObject.
func (ao AbstractObject) GetName() string { func (ao AbstractObject) GetName() string {
return ao.Name return ao.Name
@@ -150,19 +83,17 @@ func (ao *AbstractObject) UpToDate(user string, peer string, create bool) {
ao.UpdateDate = time.Now() ao.UpdateDate = time.Now()
ao.UpdaterID = peer ao.UpdaterID = peer
ao.UserUpdaterID = user ao.UserUpdaterID = user
if create && ao.CreatorID == "" { if create {
ao.CreationDate = time.Now() ao.CreationDate = time.Now()
ao.CreatorID = peer ao.CreatorID = peer
ao.UserCreatorID = user ao.UserCreatorID = user
} }
} }
func (ao *AbstractObject) VerifyAuth(callName string, request *tools.APIRequest) bool { func (ao *AbstractObject) VerifyAuth(request *tools.APIRequest) bool {
return (ao.AccessMode == Public && callName == "get") || (request != nil && (request.Admin || (ao.CreatorID == request.PeerID && request.PeerID != ""))) return ao.AccessMode == Public || (request != nil && ao.CreatorID == request.PeerID && request.PeerID != "")
} }
// TODO : check write per auth
func (ao *AbstractObject) GetObjectFilters(search string) *dbs.Filters { func (ao *AbstractObject) GetObjectFilters(search string) *dbs.Filters {
if search == "*" { if search == "*" {
search = "" search = ""
@@ -192,108 +123,50 @@ func (dma *AbstractObject) Serialize(obj DBObject) map[string]interface{} {
return m return m
} }
type AbstractAccessor[T DBObject] struct { type AbstractAccessor struct {
Logger zerolog.Logger // Logger is the logger of the accessor, it's a specilized logger for the accessor Logger zerolog.Logger // Logger is the logger of the accessor, it's a specilized logger for the accessor
Type tools.DataType // Type is the data type of the accessor Type tools.DataType // Type is the data type of the accessor
Request *tools.APIRequest // Caller is the http caller of the accessor (optionnal) only need in a peer connection Request *tools.APIRequest // Caller is the http caller of the accessor (optionnal) only need in a peer connection
ResourceModelAccessor Accessor ResourceModelAccessor Accessor
New func() T
NotImplemented []string
} }
func (r *AbstractAccessor[T]) NewObj() DBObject { func (r *AbstractAccessor) ShouldVerifyAuth() bool {
return r.New()
}
func (r *AbstractAccessor[T]) ShouldVerifyAuth() bool {
return true return true
} }
func (r *AbstractAccessor[T]) GetRequest() *tools.APIRequest { func (r *AbstractAccessor) GetRequest() *tools.APIRequest {
return r.Request return r.Request
} }
func (dma *AbstractAccessor[T]) GetUser() string { func (dma *AbstractAccessor) GetUser() string {
if dma.Request == nil { if dma.Request == nil {
return "" return ""
} }
return dma.Request.Username return dma.Request.Username
} }
func (dma *AbstractAccessor[T]) GetPeerID() string { func (dma *AbstractAccessor) GetPeerID() string {
if dma.Request == nil { if dma.Request == nil {
return "" return ""
} }
return dma.Request.PeerID return dma.Request.PeerID
} }
func (dma *AbstractAccessor[T]) GetGroups() []string { func (dma *AbstractAccessor) GetGroups() []string {
if dma.Request == nil { if dma.Request == nil {
return []string{} return []string{}
} }
return dma.Request.Groups return dma.Request.Groups
} }
func (dma *AbstractAccessor[T]) GetLogger() *zerolog.Logger { func (dma *AbstractAccessor) GetLogger() *zerolog.Logger {
return &dma.Logger return &dma.Logger
} }
func (dma *AbstractAccessor[T]) GetType() tools.DataType { func (dma *AbstractAccessor) GetType() tools.DataType {
return dma.Type return dma.Type
} }
func (dma *AbstractAccessor[T]) GetCaller() *tools.HTTPCaller { func (dma *AbstractAccessor) GetCaller() *tools.HTTPCaller {
if dma.Request == nil { if dma.Request == nil {
return nil return nil
} }
return dma.Request.Caller return dma.Request.Caller
} }
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *AbstractAccessor[T]) DeleteOne(id string) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "DeleteOne") {
return nil, 404, errors.New("not implemented")
}
return GenericDeleteOne(id, a)
}
func (a *AbstractAccessor[T]) UpdateOne(set map[string]interface{}, id string) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "UpdateOne") {
return nil, 404, errors.New("not implemented")
}
// should verify if a source is existing...
return GenericUpdateOne(set, id, a)
}
func (a *AbstractAccessor[T]) StoreOne(data DBObject) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "StoreOne") {
return nil, 404, errors.New("not implemented")
}
return GenericStoreOne(data.(T), a)
}
func (a *AbstractAccessor[T]) CopyOne(data DBObject) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "CopyOne") {
return nil, 404, errors.New("not implemented")
}
return GenericStoreOne(data.(T), a)
}
func (a *AbstractAccessor[T]) LoadOne(id string) (DBObject, int, error) {
return GenericLoadOne(id, a.New(), func(d DBObject) (DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *AbstractAccessor[T]) LoadAll(isDraft bool, 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
}
}
-60
View File
@@ -1,60 +0,0 @@
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:
}
}
}
Executable → Regular
+42 -230
View File
@@ -1,13 +1,10 @@
package utils package utils
import ( import (
"encoding/json"
"errors" "errors"
"os"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/dbs/mongo" "cloud.o-forge.io/core/oc-lib/dbs/mongo"
"github.com/google/uuid"
mgb "go.mongodb.org/mongo-driver/mongo" mgb "go.mongodb.org/mongo-driver/mongo"
) )
@@ -17,14 +14,11 @@ type Owner struct {
} }
func VerifyAccess(a Accessor, id string) error { func VerifyAccess(a Accessor, id string) error {
if a == nil {
return errors.New("no accessor to verify access")
}
data, _, err := a.LoadOne(id) data, _, err := a.LoadOne(id)
if err != nil { if err != nil {
return err return err
} }
if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) { if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
return errors.New("you are not allowed to access :" + a.GetType().String()) return errors.New("you are not allowed to access :" + a.GetType().String())
} }
return nil return nil
@@ -32,13 +26,9 @@ func VerifyAccess(a Accessor, id string) error {
// GenericLoadOne loads one object from the database (generic) // GenericLoadOne loads one object from the database (generic)
func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) { func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
if data.GetID() == "" {
data.GenerateID() data.GenerateID()
}
data.StoreDraftDefault() data.StoreDraftDefault()
data.UpToDate(a.GetUser(), a.GetPeerID(), true) data.UpToDate(a.GetUser(), a.GetPeerID(), true)
data.Unsign()
data.Sign()
f := dbs.Filters{ f := dbs.Filters{
Or: map[string][]dbs.Filter{ Or: map[string][]dbs.Filter{
"abstractresource.abstractobject.name": {{ "abstractresource.abstractobject.name": {{
@@ -51,126 +41,82 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
}}, }},
}, },
} }
if a.ShouldVerifyAuth() && !data.VerifyAuth("store", a.GetRequest()) { if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access : " + a.GetType().String()) return nil, 403, errors.New("you are not allowed to access : " + a.GetType().String())
} }
if cursor, _, _ := a.Search(&f, "", data.IsDrafted(), 0, 10); len(cursor) > 0 { if cursor, _, _ := a.Search(&f, "", data.IsDrafted()); len(cursor) > 0 {
return nil, 409, errors.New(a.GetType().String() + " with name " + data.GetName() + " already exists") return nil, 409, errors.New(a.GetType().String() + " with name " + data.GetName() + " already exists")
} }
err := validate.Struct(data) err := validate.Struct(data)
if err != nil { if err != nil {
return nil, 422, errors.New("error when validating the received struct: " + err.Error()) return nil, 422, err
} }
id, code, err := mongo.MONGOService.StoreOne(data, data.GetID(), a.GetType().String()) id, code, err := mongo.MONGOService.StoreOne(data, data.GetID(), a.GetType().String())
if err != nil { if err != nil {
a.GetLogger().Error().Msg("Could not store " + data.GetName() + " to db. Error: " + err.Error()) a.GetLogger().Error().Msg("Could not store " + data.GetName() + " to db. Error: " + err.Error())
return nil, code, err return nil, code, err
} }
result, rcode, rerr := a.LoadOne(id) return 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) // GenericLoadOne loads one object from the database (generic)
func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) { func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
res, code, err := a.LoadOne(id) res, code, err := a.LoadOne(id)
if err != nil {
return res, code, err
}
if res == nil {
return res, code, errors.New("not found")
}
return GenericDelete(res, a)
}
func GenericDelete(res DBObject, a Accessor) (DBObject, int, error) {
if !res.CanDelete() { if !res.CanDelete() {
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String()) return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
} }
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(res.GetID(), a.GetType().String())
if err != nil { if err != nil {
a.GetLogger().Error().Msg("Could not delete " + res.GetID() + " to db. Error: " + err.Error())
return nil, code, err return nil, code, err
} }
go NotifyChange(a.GetType(), res.GetID(), res, true) if a.ShouldVerifyAuth() && !res.VerifyAuth(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())
if err != nil {
a.GetLogger().Error().Msg("Could not delete " + id + " to db. Error: " + err.Error())
return nil, code, err
}
return res, 200, nil 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 change["is_draft"] == true {
obj.SetDraft(change["is_draft"] == true)
}
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
deepMerge(loaded, change)
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) // GenericLoadOne loads one object from the database (generic)
// json expected in entry is a flatted object no need to respect the inheritance hierarchy // json expected in entry is a flatted object no need to respect the inheritance hierarchy
func GenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, int, error) { func GenericUpdateOne(set DBObject, id string, a Accessor, new DBObject) (DBObject, int, error) {
obj, loaded, c, err := ModelGenericUpdateOne(change, id, a) r, c, err := a.LoadOne(id)
if err != nil { if err != nil {
return nil, c, err return nil, c, err
} }
id, code, err := mongo.MONGOService.UpdateOne(obj.Deserialize(loaded, obj), id, a.GetType().String()) 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())
if err != nil { if err != nil {
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error()) a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
return nil, code, err return nil, code, err
} }
result, rcode, rerr := a.LoadOne(id) return 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, data T, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) { func GenericLoadOne[T DBObject](id string, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
var data T
res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String()) res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String())
if err != nil { if err != nil {
return nil, code, err return nil, code, err
} }
if err = res_mongo.Decode(data); err != nil { res_mongo.Decode(&data)
return nil, 400, err if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
}
if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String()) return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
} }
return f(data) return f(data)
@@ -186,7 +132,7 @@ func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft
return nil, 404, err return nil, 404, err
} }
for _, r := range results { for _, r := range results {
if (a.ShouldVerifyAuth() && !r.VerifyAuth("get", a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) { if (a.ShouldVerifyAuth() && !r.VerifyAuth(a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) {
continue continue
} }
objs = append(objs, f(r)) objs = append(objs, f(r))
@@ -194,27 +140,17 @@ func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft
return objs, 200, nil return objs, 200, nil
} }
func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) { func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor) ([]ShallowDBObject, int, error) {
offset := int64(0) res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType().String())
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) return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
} }
func GenericSearch[T DBObject](filters *dbs.Filters, search string, defaultFilters *dbs.Filters, func GenericSearch[T DBObject](filters *dbs.Filters, search string, defaultFilters *dbs.Filters,
f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) { f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor) ([]ShallowDBObject, int, error) {
if filters == nil && search != "" { if filters == nil && search != "" {
filters = defaultFilters filters = defaultFilters
} }
offset := int64(0) res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType().String())
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) return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
} }
@@ -226,129 +162,5 @@ func GenericRawUpdateOne(set DBObject, id string, a Accessor) (DBObject, int, er
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error()) a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
return nil, code, err return nil, code, err
} }
result, rcode, rerr := a.LoadOne(id) return 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()
}
// deepMerge overlays patch values onto base, preserving base values for keys
// absent from patch, nil patch values, and empty strings when base is non-empty.
// This prevents partial frontend payloads from silently erasing server-managed
// fields (source, env, country, owners, creator_id, creation_date, …).
func deepMerge(base, patch map[string]interface{}) {
for k, pv := range patch {
bv := base[k]
switch pvTyped := pv.(type) {
case map[string]interface{}:
if bvMap, ok := bv.(map[string]interface{}); ok {
deepMerge(bvMap, pvTyped)
} else {
base[k] = pv
}
case []interface{}:
if bvSlice, ok := bv.([]interface{}); ok {
base[k] = mergeSlices(bvSlice, pvTyped)
} else {
base[k] = pv
}
case string:
// Don't overwrite a non-empty base value with an empty string.
if pvTyped != "" {
base[k] = pv
}
default:
if pv != nil {
base[k] = pv
}
}
}
}
// mergeSlices merges two slices element-wise.
// For slices of maps it matches elements by their "id" field when available;
// falls back to positional matching. An empty patch slice leaves base intact.
func mergeSlices(base, patch []interface{}) []interface{} {
if len(patch) == 0 {
return base
}
for _, e := range patch {
if _, ok := e.(map[string]interface{}); !ok {
return patch // non-map elements: replace wholesale
}
}
baseByID := map[string]map[string]interface{}{}
for _, e := range base {
if em, ok := e.(map[string]interface{}); ok {
if id, ok := em["id"].(string); ok && id != "" {
baseByID[id] = em
}
}
}
result := make([]interface{}, 0, len(patch))
for i, pe := range patch {
pm, _ := pe.(map[string]interface{})
if pm == nil {
result = append(result, pe)
continue
}
var baseElem map[string]interface{}
if id, ok := pm["id"].(string); ok && id != "" {
baseElem = baseByID[id]
}
if baseElem == nil && i < len(base) {
baseElem, _ = base[i].(map[string]interface{})
}
if baseElem != nil {
merged := make(map[string]interface{}, len(baseElem))
for k, v := range baseElem {
merged[k] = v
}
deepMerge(merged, pm)
result = append(result, merged)
} else {
result = append(result, pe)
}
}
return result
}
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
} }

Some files were not shown because too many files have changed in this diff Show More