42 Commits

Author SHA1 Message Date
mr 3a66b42c01 test 2026-06-23 09:40:33 +02:00
mr 58e97fbe74 lib 2026-06-22 07:50:01 +02:00
mr 1425a31494 Orga + Consent 2026-06-05 15:56:50 +02:00
mr 6ee169f444 can delete 2026-06-05 09:59:14 +02:00
mr 5be3c0a10a wf 2026-06-04 13:36:55 +02:00
mr d9723e6431 oc lib test 2026-06-04 12:04:04 +02:00
mr c726361deb Get Exploit 2026-06-04 11:31:03 +02:00
mr d19ff1f8b2 CreatorID 2026-06-04 09:00:31 +02:00
mr 69244163b4 workflow bson missing 2026-06-03 15:33:39 +02:00
mr 842364d145 deep merge 2026-06-03 11:23:41 +02:00
mr 9ab374b720 oc nano 2026-06-03 08:03:11 +02:00
mr aa2bca48ef test 2026-06-02 16:43:55 +02:00
mr 322ea38bb4 nil map 2026-06-02 15:57:33 +02:00
mr c1490a7746 proper 2026-06-02 15:49:03 +02:00
mr 49f60d9416 test 2026-06-02 15:34:54 +02:00
mr 548ed84b13 prospect 2026-06-02 15:11:58 +02:00
mr 178cd48314 prospect 2026-06-02 14:45:09 +02:00
mr b31df8cfed adjust allowed instances for type behaviors 2026-06-02 14:16:32 +02:00
mr a0a53f0477 Panic recovered FiltersFromFlatMap 2026-06-02 14:04:33 +02:00
mr dffaa6326f still prospecting 2026-06-02 14:03:30 +02:00
mr 8155f4b17a prospect 2026-06-02 13:47:41 +02:00
mr 51307bb067 trace debug dynamic 2026-06-02 13:33:47 +02:00
mr 6ac788a8ff test 2026-06-02 12:48:33 +02:00
mr a7d0c1208b full filter interpretation 2026-06-02 11:35:19 +02:00
mr 3924fca289 Kick name malformed 2026-06-02 10:50:42 +02:00
mr 797df972ac plantuml duplication behavior 2026-06-02 08:42:20 +02:00
mr 71ae0d2cfc inout change vars regime 2026-06-01 16:45:05 +02:00
mr 5806bdd3d2 Correct link 2026-06-01 15:01:45 +02:00
mr 99fbe82a51 Update Auto Outputs on sourced. 2026-06-01 08:45:50 +02:00
mr 7d8bec9a78 Container can be sourced 2026-06-01 08:26:48 +02:00
mr afd8a2d97c conditionnal is_draft 2026-05-29 14:31:44 +02:00
mr 82a4708f46 Is draft 2026-05-29 14:12:40 +02:00
mr a3bca24982 isdraft pb 2026-05-29 13:43:41 +02:00
mr 41706949fd isDraft Update dafuck 2026-05-29 12:51:04 +02:00
mr b1429596bb not proper enum compararison 2026-05-29 10:38:45 +02:00
mr ce110ee634 inspect comparision 2026-05-29 10:22:07 +02:00
mr 7e5b69b1d2 Live resource failed 2026-05-29 09:12:52 +02:00
mr 26948da3c1 relation peer mismatch 2026-05-28 16:29:36 +02:00
mr 4e1b1164cc relation mismatched 2026-05-28 16:28:51 +02:00
mr 73b844f664 pass to known 2026-05-28 15:37:15 +02:00
mr cef23b5f30 Payment Flow + Access Flow Change 2026-05-27 15:50:23 +02:00
mr e6a9558cbf peerID 2026-05-26 15:04:57 +02:00
62 changed files with 3972 additions and 556 deletions
+53 -14
View File
@@ -3,6 +3,7 @@ package dbs
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"runtime/debug" "runtime/debug"
"strings" "strings"
@@ -160,11 +161,16 @@ type Filter struct {
// Keys inside "and"/"or" are json tag names; the function resolves each to its // 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. // full dotted BSON path using the target struct. Unknown keys are kept as-is.
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Filters { 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{ filters := &Filters{
And: make(map[string][]Filter), And: make(map[string][]Filter),
Or: make(map[string][]Filter), Or: make(map[string][]Filter),
} }
paths := jsonToBsonPaths(reflect.TypeOf(target), "") paths := jsonToBsonPaths(reflect.TypeOf(target), "", "")
resolve := func(jsonKey string) string { resolve := func(jsonKey string) string {
if p, ok := paths[jsonKey]; ok { if p, ok := paths[jsonKey]; ok {
return p return p
@@ -179,11 +185,12 @@ func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Fil
} }
for jsonKey, val := range m { for jsonKey, val := range m {
bsonKey := resolve(jsonKey) bsonKey := resolve(jsonKey)
items, ok := val.([]interface{}) //items, ok := val.([]interface{})
fmt.Println(jsonKey, val, bsonKey)
if !ok { if !ok {
continue continue
} }
for _, item := range items { for _, item := range val.([]interface{}) {
entry, ok := item.(map[string]interface{}) entry, ok := item.(map[string]interface{})
if !ok { if !ok {
continue continue
@@ -214,11 +221,22 @@ func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Fil
// //
// Anonymous embedded fields without any tag follow the BSON convention of this // 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 // codebase: they are stored as a nested sub-document whose key is the lowercased
// struct type name (e.g. utils.AbstractObject → "abstractobject"). // struct type name (e.g. utils.AbstractObject → "abstractobject"). Their JSON
func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string { // 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 { for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
t = t.Elem() t = t.Elem()
} }
if t.Kind() == reflect.Map {
t = t.Elem()
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
}
result := make(map[string]string) result := make(map[string]string)
if t.Kind() != reflect.Struct { if t.Kind() != reflect.Struct {
return result return result
@@ -232,17 +250,21 @@ func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string {
bsonName := strings.Split(bsonTag, ",")[0] bsonName := strings.Split(bsonTag, ",")[0]
// Anonymous embedded struct with no tags: use lowercase type name as BSON prefix. // 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 == "" { if field.Anonymous && jsonName == "" && bsonName == "" {
ft := field.Type ft := field.Type
for ft.Kind() == reflect.Ptr { for ft.Kind() == reflect.Ptr {
ft = ft.Elem() ft = ft.Elem()
} }
if ft.Kind() == reflect.Struct { if ft.Kind() == reflect.Struct {
embedPrefix := strings.ToLower(ft.Name()) embedBsonPrefix := strings.ToLower(ft.Name())
if prefix != "" { re := regexp.MustCompile(`\[[^\]]*\]`)
embedPrefix = prefix + "." + embedPrefix embedBsonPrefix = re.ReplaceAllString(embedBsonPrefix, "")
embedBsonPrefix = strings.ReplaceAll(embedBsonPrefix, "*", "")
if bsonPrefix != "" {
embedBsonPrefix = bsonPrefix + "." + embedBsonPrefix
} }
for k, v := range jsonToBsonPaths(ft, embedPrefix) { for k, v := range jsonToBsonPaths(ft, embedBsonPrefix, jsonPrefix) {
if _, exists := result[k]; !exists { if _, exists := result[k]; !exists {
result[k] = v result[k] = v
} }
@@ -258,19 +280,36 @@ func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string {
bsonName = jsonName bsonName = jsonName
} }
fullPath := bsonName fullBsonPath := bsonName
if prefix != "" { if bsonPrefix != "" {
fullPath = prefix + "." + bsonName fullBsonPath = bsonPrefix + "." + bsonName
}
fullJsonPath := jsonName
if jsonPrefix != "" {
fullJsonPath = jsonPrefix + "." + jsonName
} }
result[jsonName] = fullPath 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 ft := field.Type
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice { for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
ft = ft.Elem() ft = ft.Elem()
} }
if ft.Kind() == reflect.Map {
ft = ft.Elem()
for ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
}
if ft.Kind() == reflect.Struct { if ft.Kind() == reflect.Struct {
for k, v := range jsonToBsonPaths(ft, fullPath) { for k, v := range jsonToBsonPaths(ft, fullBsonPath, fullJsonPath) {
if _, exists := result[k]; !exists { if _, exists := result[k]; !exists {
result[k] = v result[k] = v
} }
+46
View File
@@ -17,6 +17,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/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"
@@ -67,6 +72,12 @@ const (
ALLOWED_IMAGE = tools.ALLOWED_IMAGE ALLOWED_IMAGE = tools.ALLOWED_IMAGE
SERVICE_RESOURCE = tools.SERVICE_RESOURCE SERVICE_RESOURCE = tools.SERVICE_RESOURCE
LIVE_SERVICE = tools.LIVE_SERVICE 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 { func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
@@ -727,6 +738,41 @@ func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
return nil 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 // ------------- Loading resources ----------GetAccessor
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) { func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
-254
View File
@@ -1,254 +0,0 @@
package bill
import (
"encoding/json"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/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"
)
/*
* Booking is a struct that represents a booking
*/
type Bill struct {
utils.AbstractObject
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
}
func (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
}
func GenerateBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
// hhmmm : should get... the loop.
return &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: false,
},
OrderID: order.UUID,
Status: enum.PENDING,
// SubOrders: peerOrders,
}, nil
}
func DraftFirstBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
peers := map[string][]*PeerItemOrder{}
for _, p := range order.Purchases {
// TODO : if once
if _, ok := peers[p.DestPeerID]; !ok {
peers[p.DestPeerID] = []*PeerItemOrder{}
}
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
ResourceType: p.ResourceType,
Purchase: p,
Item: p.PricedItem,
Quantity: 1,
})
}
for _, b := range order.Bookings {
// TODO : if once
isPurchased := false
for _, p := range order.Purchases {
if p.ResourceID == b.ResourceID {
isPurchased = true
break
}
}
if isPurchased {
continue
}
if _, ok := peers[b.DestPeerID]; !ok {
peers[b.DestPeerID] = []*PeerItemOrder{}
}
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
ResourceType: b.ResourceType,
Quantity: 1,
Item: b.PricedItem,
})
}
peerOrders := map[string]*PeerOrder{}
for peerID, items := range peers {
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
if err != nil {
return nil, err
}
peerOrders[peerID] = &PeerOrder{
PeerID: peerID,
BillingAddress: pr.(*peer.Peer).WalletAddress,
Items: items,
}
}
bill := &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: true,
},
OrderID: order.UUID,
Status: enum.PENDING,
SubOrders: peerOrders,
}
return bill.SumUpBill(request)
}
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (r *Bill) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.Status != set.(*Bill).Status {
return true, &Bill{Status: set.(*Bill).Status} // only state can be updated
}
return r.IsDraft, set
}
func (r *Bill) CanDelete() bool {
return r.IsDraft // only draft order can be deleted
}
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return d, err
}
d.Total += b.Total
}
return d, nil
}
type PeerOrder struct {
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
func 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 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 {
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(),
})
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPriceHT(request) // missing something
if err != nil {
return err
}
d.Total += tot
}
return nil
}
type PeerItemOrder struct {
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"`
}
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
/////////// Temporary in order to allow GenerateOrder to complete while billing is still WIP
if d.Purchase == nil {
return 0, nil
}
///////////
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
}
}
}
p, err := priced.GetPriceHT()
if err != nil {
return 0, err
}
return p * float64(d.Quantity), nil
}
// WTF HOW TO SELECT THE RIGHT PRICE ???
// SHOULD SET A BUYING STATUS WHEN PAYMENT IS VALIDATED
+421
View File
@@ -0,0 +1,421 @@
package billing
import (
"encoding/json"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
type Bill struct {
utils.AbstractObject
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
}
func (ri *Bill) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "order":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.ORDER)
}
}
return ext
}
// IsFullySettled retourne vrai quand chaque ligne de chaque peer-order est réglée.
func (b *Bill) IsFullySettled() bool {
for _, po := range b.SubOrders {
for _, item := range po.Items {
if !item.Settled {
return false
}
}
}
return true
}
// SettledTotal retourne le montant total des lignes déjà réglées.
func (b *Bill) SettledTotal() float64 {
total := 0.0
for _, po := range b.SubOrders {
for _, item := range po.Items {
if item.Settled {
total += item.UnitPriceHT * float64(item.Quantity)
}
}
}
return total
}
// MarkItemSettled marque une ligne comme réglée d'après son itemID
// et propage le statut PAID sur le PeerOrder si toutes ses lignes sont réglées.
func (b *Bill) MarkItemSettled(itemID string) bool {
now := time.Now().UTC()
for _, po := range b.SubOrders {
for _, item := range po.Items {
if item.ItemID == itemID {
item.Settled = true
item.SettledAt = &now
// propage le statut PAID si toutes les lignes du peer sont réglées
if po.IsFullySettled() {
po.Status = enum.PAID
}
return true
}
}
}
return false
}
func GenerateBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
return &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: false,
},
OrderID: ord.UUID,
Status: enum.PENDING,
}, nil
}
// DraftFirstBill crée le premier brouillon de facture pour un order.
// Règle :
// - Calcul total indépendant du mode de paiement (photo du coût réel).
// - Les purchases sont toujours BILL_ONCE / PAY_ONCE → réglées immédiatement.
// - Les bookings avec BillingStrategy != BILL_ONCE génèrent des Subscription.
// - Chaque ligne reçoit un ItemID unique pour le suivi de règlement.
func DraftFirstBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
peers := map[string][]*PeerItemOrder{}
// Purchases : facturation immédiate, pas de subscription
for _, p := range ord.Purchases {
if _, ok := peers[p.DestPeerID]; !ok {
peers[p.DestPeerID] = []*PeerItemOrder{}
}
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
ItemID: uuid.New().String(),
ResourceType: p.ResourceType,
Purchase: p,
Item: p.PricedItem,
Quantity: 1,
BillingStrategy: pricing.BILL_ONCE,
PaymentType: pricing.PAY_ONCE,
})
}
// Bookings : exclure les ressources déjà achetées (purchase_resource existant)
purchasedIDs := map[string]bool{}
for _, p := range ord.Purchases {
purchasedIDs[p.ResourceID] = true
}
for _, b := range ord.Bookings {
if purchasedIDs[b.ResourceID] {
continue
}
if _, ok := peers[b.DestPeerID]; !ok {
peers[b.DestPeerID] = []*PeerItemOrder{}
}
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
ItemID: uuid.New().String(),
ResourceType: b.ResourceType,
Quantity: 1,
Item: b.PricedItem,
BillingStrategy: b.BillingStrategy,
PaymentType: b.PaymentType,
})
}
// Résolution des adresses de facturation peer
peerOrders := map[string]*PeerOrder{}
for peerID, items := range peers {
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
if err != nil {
return nil, err
}
peerOrders[peerID] = &PeerOrder{
PeerID: peerID,
BillingAddress: pr.(*peer.Peer).WalletAddress,
Items: items,
}
}
bill := &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: true,
},
OrderID: ord.UUID,
Status: enum.PENDING,
SubOrders: peerOrders,
}
// 1. Calcul des totaux (indépendant du mode de paiement)
if _, err := bill.SumUpBill(request); err != nil {
return bill, err
}
// 2. Création des subscriptions pour les lignes récurrentes
subIDs, err := createRecurringSubscriptions(bill, request)
if err != nil {
return bill, err
}
// 3. Liaison des subscription IDs à l'order pour traçabilité
if len(subIDs) > 0 {
ord.SubscriptionIDs = append(ord.SubscriptionIDs, subIDs...)
}
// 4. Persistance du brouillon de facture (les UnitPriceHT et SubscriptionID sont déjà set)
stored, _, err := NewAccessor(request).StoreOne(bill)
if err != nil {
return bill, err
}
return stored.(*Bill), nil
}
// createRecurringSubscriptions crée les Subscription pour chaque groupe
// (peer × BillingStrategy) dont la stratégie est récurrente.
// Modifie les PeerItemOrder en place (SubscriptionID).
// Retourne les IDs de subscriptions créées.
func createRecurringSubscriptions(b *Bill, request *tools.APIRequest) ([]string, error) {
subIDs := []string{}
for peerID, po := range b.SubOrders {
// Groupe les items récurrents par BillingStrategy
byStrategy := map[pricing.BillingStrategy][]*PeerItemOrder{}
for _, item := range po.Items {
if item.BillingStrategy == pricing.BILL_ONCE {
continue
}
byStrategy[item.BillingStrategy] = append(byStrategy[item.BillingStrategy], item)
}
for strategy, items := range byStrategy {
subItems := make([]*subscription.SubscriptionItem, 0, len(items))
totalAmount := 0.0
for _, item := range items {
subItems = append(subItems, &subscription.SubscriptionItem{
ResourceType: item.ResourceType,
Quantity: item.Quantity,
UnitPrice: item.UnitPriceHT,
})
totalAmount += item.UnitPriceHT * float64(item.Quantity)
}
var sub *subscription.Subscription
switch strategy {
case pricing.BILL_PER_YEAR:
sub = subscription.NewYearlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
case pricing.BILL_PER_WEEK:
sub = subscription.NewWeeklySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
default: // BILL_PER_MONTH et tout autre cas récurrent
sub = subscription.NewMonthlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
}
sub.IsDraft = true
res, _, err := subscription.NewAccessor(request).StoreOne(sub)
if err != nil {
return subIDs, err
}
storedSub := res.(*subscription.Subscription)
subIDs = append(subIDs, storedSub.GetID())
// Lie le SubscriptionID à chaque ligne concernée
for _, item := range items {
item.SubscriptionID = storedSub.GetID()
}
}
}
return subIDs, nil
}
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (r *Bill) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.Status != set.(*Bill).Status {
return true, &Bill{Status: set.(*Bill).Status}
}
return r.IsDraft, set
}
func (r *Bill) CanDelete() bool {
return r.IsDraft
}
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return d, err
}
d.Total += b.Total
}
return d, nil
}
// ---------------------------------------------------------------------------
// PeerOrder
// ---------------------------------------------------------------------------
type PeerOrder struct {
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
// IsFullySettled retourne vrai si toutes les lignes de ce peer sont réglées.
func (po *PeerOrder) IsFullySettled() bool {
for _, item := range po.Items {
if !item.Settled {
return false
}
}
return true
}
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
switch dt {
case tools.PROCESSING_RESOURCE:
return &resources.PricedProcessingResource{}
case tools.STORAGE_RESOURCE:
return &resources.PricedStorageResource{}
case tools.DATA_RESOURCE:
return &resources.PricedDataResource{}
case tools.COMPUTE_RESOURCE:
return &resources.PricedComputeResource{}
case tools.WORKFLOW_RESOURCE:
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
}
return nil
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING
go func() {
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER — UPDATE STATUS ON RETURN
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID {
now := time.Now().UTC()
for _, b := range d.Items {
priced := PricedByType(b.ResourceType)
bb, _ := json.Marshal(b.Item)
json.Unmarshal(bb, priced)
if !priced.IsPurchasable() {
continue
}
accessor := purchase_resource.NewAccessor(request)
accessor.StoreOne(&purchase_resource.PurchaseResource{
ResourceID: priced.GetID(),
ResourceType: priced.GetType(),
EndDate: priced.GetLocationEnd(),
})
// Marque la ligne comme réglée
b.Settled = true
b.SettledAt = &now
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPriceHT(request)
if err != nil {
return err
}
d.Total += tot
}
return nil
}
// ---------------------------------------------------------------------------
// PeerItemOrder
// ---------------------------------------------------------------------------
// PeerItemOrder est une ligne de facture pour un peer donné.
type PeerItemOrder struct {
// ItemID identifie de manière unique cette ligne pour le suivi de règlement.
ItemID string `json:"item_id,omitempty" bson:"item_id,omitempty"`
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
// UnitPriceHT est le prix unitaire HT calculé par SumUpBill/GetPriceHT.
// Utilisé pour la création des subscriptions sans recalcul.
UnitPriceHT float64 `json:"unit_price_ht,omitempty" bson:"unit_price_ht,omitempty"`
// SubscriptionID référence la Subscription créée pour les lignes récurrentes.
// Vide pour les lignes BILL_ONCE.
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
// Settled indique si cette ligne a été réglée (paiement confirmé).
Settled bool `json:"settled" bson:"settled"`
SettledAt *time.Time `json:"settled_at,omitempty" bson:"settled_at,omitempty"`
}
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
if d.Purchase == nil {
return 0, nil
}
priced := PricedByType(d.ResourceType)
b, _ := json.Marshal(d.Item)
err := json.Unmarshal(b, priced)
if err != nil {
return 0, err
}
accessor := purchase_resource.NewAccessor(request)
search, code, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
},
}, "", d.Purchase.IsDraft, 0, 10000)
if code == 200 && len(search) > 0 {
for _, s := range search {
if s.(*purchase_resource.PurchaseResource).EndDate == nil ||
time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
return 0, nil
}
}
}
unitPrice, err := priced.GetPriceHT()
if err != nil {
return 0, err
}
d.UnitPriceHT = unitPrice // cache pour createRecurringSubscriptions
return unitPrice * float64(d.Quantity), nil
}
@@ -1,4 +1,4 @@
package bill package billing
import ( import (
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
@@ -7,14 +7,13 @@ import (
) )
type billMongoAccessor struct { type billMongoAccessor struct {
utils.AbstractAccessor[*Bill] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Bill]
} }
// New creates a new instance of the billMongoAccessor
func NewAccessor(request *tools.APIRequest) *billMongoAccessor { func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
return &billMongoAccessor{ return &billMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Bill]{ AbstractAccessor: utils.AbstractAccessor[*Bill]{
Logger: logs.CreateLogger(tools.BILL.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.BILL.String()),
Request: request, Request: request,
Type: tools.BILL, Type: tools.BILL,
New: func() *Bill { return &Bill{} }, New: func() *Bill { return &Bill{} },
+146
View File
@@ -0,0 +1,146 @@
package discount
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type DiscountType int
const (
PERCENTAGE DiscountType = iota // réduction en pourcentage
FIXED_AMOUNT // réduction montant fixe
)
func (d DiscountType) String() string {
return [...]string{"percentage", "fixed_amount"}[d]
}
func DiscountTypeList() []DiscountType {
return []DiscountType{PERCENTAGE, FIXED_AMOUNT}
}
type DiscountScope int
const (
SCOPE_ALL DiscountScope = iota // applicable à tout
SCOPE_RESOURCE_TYPE // applicable à un type de ressource
SCOPE_RESOURCE // applicable à une ressource spécifique
SCOPE_SUBSCRIPTION // applicable aux souscriptions
)
func (d DiscountScope) String() string {
return [...]string{"all", "resource_type", "resource", "subscription"}[d]
}
// Discount représente une réduction applicable sur les ressources ou abonnements.
type Discount struct {
utils.AbstractObject
Code string `json:"code,omitempty" bson:"code,omitempty"`
DiscountType DiscountType `json:"discount_type" bson:"discount_type"`
Scope DiscountScope `json:"scope" bson:"scope"`
Value float64 `json:"value" bson:"value"` // pourcentage (0-100) ou montant fixe
Currency string `json:"currency,omitempty" bson:"currency,omitempty"` // pour FIXED_AMOUNT
ResourceTypes []tools.DataType `json:"resource_types,omitempty" bson:"resource_types,omitempty"` // si SCOPE_RESOURCE_TYPE
ResourceIDs []string `json:"resource_ids,omitempty" bson:"resource_ids,omitempty"` // si SCOPE_RESOURCE
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
MaxUsage int `json:"max_usage,omitempty" bson:"max_usage,omitempty"` // 0 = illimité
CurrentUsage int `json:"current_usage" bson:"current_usage"`
MinAmount float64 `json:"min_amount,omitempty" bson:"min_amount,omitempty"` // montant minimum du bill pour appliquer
Active bool `json:"active" bson:"active" default:"true"`
}
// IsValid vérifie si la réduction est applicable au moment présent.
func (d *Discount) IsValid(billAmount float64) bool {
now := time.Now().UTC()
if !d.Active {
return false
}
if d.MaxUsage > 0 && d.CurrentUsage >= d.MaxUsage {
return false
}
if d.ValidFrom != nil && now.Before(*d.ValidFrom) {
return false
}
if d.ValidUntil != nil && now.After(*d.ValidUntil) {
return false
}
if d.MinAmount > 0 && billAmount < d.MinAmount {
return false
}
return true
}
// Apply applique la réduction sur un prix HT et retourne le prix réduit.
func (d *Discount) Apply(priceHT float64) float64 {
switch d.DiscountType {
case PERCENTAGE:
return priceHT - (priceHT * d.Value / 100)
case FIXED_AMOUNT:
result := priceHT - d.Value
if result < 0 {
return 0
}
return result
}
return priceHT
}
// DiscountAmount retourne le montant de la réduction sans l'appliquer.
func (d *Discount) DiscountAmount(priceHT float64) float64 {
switch d.DiscountType {
case PERCENTAGE:
return priceHT * d.Value / 100
case FIXED_AMOUNT:
if d.Value > priceHT {
return priceHT
}
return d.Value
}
return 0
}
// AppliesToResource vérifie si cette réduction s'applique à une ressource donnée.
func (d *Discount) AppliesToResource(resourceID string, resourceType tools.DataType) bool {
switch d.Scope {
case SCOPE_ALL:
return true
case SCOPE_RESOURCE:
for _, id := range d.ResourceIDs {
if id == resourceID {
return true
}
}
case SCOPE_RESOURCE_TYPE:
for _, t := range d.ResourceTypes {
if t == resourceType {
return true
}
}
}
return false
}
// IncrementUsage incrémente le compteur d'utilisation.
func (d *Discount) IncrementUsage() {
d.CurrentUsage++
}
func (d *Discount) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (d *Discount) StoreDraftDefault() {
d.IsDraft = true
}
func (d *Discount) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
return d.IsDraft, set
}
func (d *Discount) CanDelete() bool {
return d.IsDraft || d.CurrentUsage == 0
}
@@ -0,0 +1,22 @@
package discount
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type discountMongoAccessor struct {
utils.AbstractAccessor[*Discount]
}
func NewAccessor(request *tools.APIRequest) *discountMongoAccessor {
return &discountMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Discount]{
Logger: logs.CreateLogger(tools.DISCOUNT.String()),
Request: request,
Type: tools.DISCOUNT,
New: func() *Discount { return &Discount{} },
},
}
}
+151
View File
@@ -0,0 +1,151 @@
package payment
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type PaymentStatus int
const (
PAYMENT_PENDING PaymentStatus = iota
PAYMENT_PROCESSING // en cours de traitement blockchain/réseau
PAYMENT_COMPLETED // confirmé
PAYMENT_FAILED // échoué
PAYMENT_CANCELLED // annulé avant exécution
PAYMENT_REFUNDED // remboursé
)
func (s PaymentStatus) String() string {
return [...]string{"pending", "processing", "completed", "failed", "cancelled", "refunded"}[s]
}
func PaymentStatusList() []PaymentStatus {
return []PaymentStatus{PAYMENT_PENDING, PAYMENT_PROCESSING, PAYMENT_COMPLETED, PAYMENT_FAILED, PAYMENT_CANCELLED, PAYMENT_REFUNDED}
}
type PaymentMethod int
const (
METHOD_BLOCKCHAIN PaymentMethod = iota
METHOD_CREDIT_CARD
METHOD_BANK_TRANSFER
METHOD_CRYPTO
METHOD_INTERNAL_CREDIT // crédit interne à la plateforme
)
func (m PaymentMethod) String() string {
return [...]string{"blockchain", "credit_card", "bank_transfer", "crypto", "internal_credit"}[m]
}
func PaymentMethodList() []PaymentMethod {
return []PaymentMethod{METHOD_BLOCKCHAIN, METHOD_CREDIT_CARD, METHOD_BANK_TRANSFER, METHOD_CRYPTO, METHOD_INTERNAL_CREDIT}
}
// Payment représente une transaction de paiement — instantanée, mensuelle ou annuelle.
type Payment struct {
utils.AbstractObject
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
PayerPeerID string `json:"payer_peer_id,omitempty" bson:"payer_peer_id,omitempty"`
RecipientPeerID string `json:"recipient_peer_id,omitempty" bson:"recipient_peer_id,omitempty"`
Amount float64 `json:"amount" bson:"amount"`
Currency string `json:"currency" bson:"currency" default:"EUR"`
Status PaymentStatus `json:"status" bson:"status"`
Method PaymentMethod `json:"method" bson:"method"`
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"` // PAY_ONCE, PAY_EVERY_MONTH, PAY_EVERY_YEAR
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
WalletFrom string `json:"wallet_from,omitempty" bson:"wallet_from,omitempty"`
WalletTo string `json:"wallet_to,omitempty" bson:"wallet_to,omitempty"`
ScheduledAt *time.Time `json:"scheduled_at,omitempty" bson:"scheduled_at,omitempty"`
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
FailureReason string `json:"failure_reason,omitempty" bson:"failure_reason,omitempty"`
Metadata map[string]string `json:"metadata,omitempty" bson:"metadata,omitempty"`
}
// NewInstantPayment crée un paiement immédiat (PAY_ONCE).
func NewInstantPayment(billID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod) *Payment {
return &Payment{
BillID: billID,
PayerPeerID: payerPeerID,
RecipientPeerID: recipientPeerID,
Amount: amount,
Currency: currency,
Status: PAYMENT_PENDING,
Method: method,
PaymentType: pricing.PAY_ONCE,
}
}
// NewScheduledPayment crée un paiement programmé (mensuel ou annuel).
func NewScheduledPayment(subscriptionID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod, paymentType pricing.PaymentType, scheduledAt time.Time) *Payment {
return &Payment{
SubscriptionID: subscriptionID,
PayerPeerID: payerPeerID,
RecipientPeerID: recipientPeerID,
Amount: amount,
Currency: currency,
Status: PAYMENT_PENDING,
Method: method,
PaymentType: paymentType,
ScheduledAt: &scheduledAt,
}
}
// Complete marque le paiement comme confirmé.
func (p *Payment) Complete(transactionID string) {
now := time.Now().UTC()
p.Status = PAYMENT_COMPLETED
p.TransactionID = transactionID
p.ProcessedAt = &now
}
// Fail marque le paiement comme échoué.
func (p *Payment) Fail(reason string) {
now := time.Now().UTC()
p.Status = PAYMENT_FAILED
p.FailureReason = reason
p.ProcessedAt = &now
}
// Cancel annule le paiement s'il est encore en attente.
func (p *Payment) Cancel() bool {
if p.Status != PAYMENT_PENDING {
return false
}
p.Status = PAYMENT_CANCELLED
return true
}
// IsRefundable indique si le paiement peut faire l'objet d'un remboursement.
func (p *Payment) IsRefundable() bool {
return p.Status == PAYMENT_COMPLETED
}
func (p *Payment) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (p *Payment) StoreDraftDefault() {
p.IsDraft = true
}
func (p *Payment) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Payment)
if !p.IsDraft && p.Status != incoming.Status {
return true, &Payment{
Status: incoming.Status,
TransactionID: incoming.TransactionID,
FailureReason: incoming.FailureReason,
}
}
return p.IsDraft, set
}
func (p *Payment) CanDelete() bool {
return p.IsDraft || p.Status == PAYMENT_PENDING
}
@@ -0,0 +1,22 @@
package payment
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type paymentMongoAccessor struct {
utils.AbstractAccessor[*Payment]
}
func NewAccessor(request *tools.APIRequest) *paymentMongoAccessor {
return &paymentMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Payment]{
Logger: logs.CreateLogger(tools.PAYMENT.String()),
Request: request,
Type: tools.PAYMENT,
New: func() *Payment { return &Payment{} },
},
}
}
@@ -0,0 +1,82 @@
package payment
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
)
type ScheduleStatus int
const (
SCHEDULE_ACTIVE ScheduleStatus = iota
SCHEDULE_PAUSED // mis en pause manuellement
SCHEDULE_CANCELLED // résilié
SCHEDULE_COMPLETED // terminé normalement (abonnement expiré)
SCHEDULE_FAILED // trop d'échecs consécutifs
)
func (s ScheduleStatus) String() string {
return [...]string{"active", "paused", "cancelled", "completed", "failed"}[s]
}
// PaymentSchedule pilote la récurrence des paiements d'un abonnement.
type PaymentSchedule struct {
SubscriptionID string `json:"subscription_id" bson:"subscription_id"`
Frequency pricing.PaymentType `json:"frequency" bson:"frequency"` // PAY_EVERY_WEEK / PAY_EVERY_MONTH / PAY_EVERY_YEAR
Amount float64 `json:"amount" bson:"amount"`
Currency string `json:"currency" bson:"currency"`
Status ScheduleStatus `json:"status" bson:"status"`
NextPaymentDate time.Time `json:"next_payment_date" bson:"next_payment_date"`
LastExecutedAt *time.Time `json:"last_executed_at,omitempty" bson:"last_executed_at,omitempty"`
FailureCount int `json:"failure_count" bson:"failure_count"`
MaxRetries int `json:"max_retries" bson:"max_retries" default:"3"`
}
// nextDate calcule la prochaine date selon la fréquence.
func (ps *PaymentSchedule) nextDate() time.Time {
switch ps.Frequency {
case pricing.PAY_EVERY_WEEK:
return ps.NextPaymentDate.AddDate(0, 0, 7)
case pricing.PAY_EVERY_MONTH:
return ps.NextPaymentDate.AddDate(0, 1, 0)
case pricing.PAY_EVERY_YEAR:
return ps.NextPaymentDate.AddDate(1, 0, 0)
}
return ps.NextPaymentDate
}
// Advance enregistre l'exécution réussie et avance à la prochaine échéance.
func (ps *PaymentSchedule) Advance() {
now := time.Now().UTC()
ps.LastExecutedAt = &now
ps.FailureCount = 0
ps.NextPaymentDate = ps.nextDate()
}
// RecordFailure incrémente le compteur d'échecs et désactive après MaxRetries.
func (ps *PaymentSchedule) RecordFailure() {
ps.FailureCount++
if ps.MaxRetries > 0 && ps.FailureCount >= ps.MaxRetries {
ps.Status = SCHEDULE_FAILED
}
}
// IsDue retourne vrai si le paiement est dû maintenant.
func (ps *PaymentSchedule) IsDue() bool {
return ps.Status == SCHEDULE_ACTIVE && !time.Now().UTC().Before(ps.NextPaymentDate)
}
// Pause suspend temporairement le calendrier.
func (ps *PaymentSchedule) Pause() {
if ps.Status == SCHEDULE_ACTIVE {
ps.Status = SCHEDULE_PAUSED
}
}
// Resume réactive un calendrier mis en pause.
func (ps *PaymentSchedule) Resume() {
if ps.Status == SCHEDULE_PAUSED {
ps.Status = SCHEDULE_ACTIVE
}
}
+136
View File
@@ -0,0 +1,136 @@
package refund
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type RefundStatus int
const (
REFUND_PENDING RefundStatus = iota
REFUND_APPROVED // approuvé, en attente de traitement
REFUND_REJECTED // rejeté
REFUND_PROCESSING // en cours de virement/blockchain
REFUND_COMPLETED // remboursé
REFUND_CANCELLED // annulé avant approbation
)
func (s RefundStatus) String() string {
return [...]string{"pending", "approved", "rejected", "processing", "completed", "cancelled"}[s]
}
func RefundStatusList() []RefundStatus {
return []RefundStatus{REFUND_PENDING, REFUND_APPROVED, REFUND_REJECTED, REFUND_PROCESSING, REFUND_COMPLETED, REFUND_CANCELLED}
}
// Refund représente une demande de remboursement sur un paiement validé.
type Refund struct {
utils.AbstractObject
PaymentID string `json:"payment_id" bson:"payment_id" validate:"required"`
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
RefundType pricing.RefundType `json:"refund_type" bson:"refund_type"`
Amount float64 `json:"amount" bson:"amount" validate:"required"`
Currency string `json:"currency" bson:"currency" default:"EUR"`
Reason string `json:"reason,omitempty" bson:"reason,omitempty"`
Status RefundStatus `json:"status" bson:"status"`
RequestedAt time.Time `json:"requested_at" bson:"requested_at"`
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
ProcessedByID string `json:"processed_by_id,omitempty" bson:"processed_by_id,omitempty"`
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
Notes string `json:"notes,omitempty" bson:"notes,omitempty"`
// ratio appliqué sur le montant original (0-100). 0 = non renseigné (remboursement total).
RefundRatio float64 `json:"refund_ratio,omitempty" bson:"refund_ratio,omitempty"`
}
// NewRefund crée une demande de remboursement total.
func NewRefund(paymentID, billID string, amount float64, currency string, refundType pricing.RefundType, reason string) *Refund {
return &Refund{
PaymentID: paymentID,
BillID: billID,
Amount: amount,
Currency: currency,
RefundType: refundType,
Reason: reason,
Status: REFUND_PENDING,
RequestedAt: time.Now().UTC(),
}
}
// NewPartialRefund crée une demande de remboursement partiel selon un ratio pourcentage.
func NewPartialRefund(paymentID, billID string, originalAmount, ratioPercent float64, currency string, refundType pricing.RefundType, reason string) *Refund {
amount := originalAmount * ratioPercent / 100
r := NewRefund(paymentID, billID, amount, currency, refundType, reason)
r.RefundRatio = ratioPercent
return r
}
// Approve approuve la demande de remboursement.
func (r *Refund) Approve(processedByID string) {
r.Status = REFUND_APPROVED
r.ProcessedByID = processedByID
}
// Reject rejette la demande de remboursement.
func (r *Refund) Reject(processedByID, notes string) {
now := time.Now().UTC()
r.Status = REFUND_REJECTED
r.ProcessedByID = processedByID
r.ProcessedAt = &now
r.Notes = notes
}
// Process passe le remboursement en cours de traitement.
func (r *Refund) Process() bool {
if r.Status != REFUND_APPROVED {
return false
}
r.Status = REFUND_PROCESSING
return true
}
// Complete finalise le remboursement avec l'identifiant de transaction.
func (r *Refund) Complete(transactionID string) {
now := time.Now().UTC()
r.Status = REFUND_COMPLETED
r.TransactionID = transactionID
r.ProcessedAt = &now
}
// Cancel annule la demande si elle est encore en attente.
func (r *Refund) Cancel() bool {
if r.Status != REFUND_PENDING {
return false
}
r.Status = REFUND_CANCELLED
return true
}
func (r *Refund) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (r *Refund) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Refund) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Refund)
if !r.IsDraft && r.Status != incoming.Status {
return true, &Refund{
Status: incoming.Status,
TransactionID: incoming.TransactionID,
Notes: incoming.Notes,
ProcessedByID: incoming.ProcessedByID,
}
}
return r.IsDraft, set
}
func (r *Refund) CanDelete() bool {
return r.IsDraft || r.Status == REFUND_PENDING
}
@@ -0,0 +1,22 @@
package refund
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type refundMongoAccessor struct {
utils.AbstractAccessor[*Refund]
}
func NewAccessor(request *tools.APIRequest) *refundMongoAccessor {
return &refundMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Refund]{
Logger: logs.CreateLogger(tools.REFUND.String()),
Request: request,
Type: tools.REFUND,
New: func() *Refund { return &Refund{} },
},
}
}
+194
View File
@@ -0,0 +1,194 @@
package subscription
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type SubscriptionStatus int
const (
SUBSCRIPTION_PENDING SubscriptionStatus = iota // en attente de premier paiement
SUBSCRIPTION_TRIAL // période d'essai
SUBSCRIPTION_ACTIVE // actif
SUBSCRIPTION_PAUSED // suspendu temporairement
SUBSCRIPTION_CANCELLED // résilié par l'utilisateur
SUBSCRIPTION_EXPIRED // date de fin dépassée
)
func (s SubscriptionStatus) String() string {
return [...]string{"pending", "trial", "active", "paused", "cancelled", "expired"}[s]
}
func SubscriptionStatusList() []SubscriptionStatus {
return []SubscriptionStatus{SUBSCRIPTION_PENDING, SUBSCRIPTION_TRIAL, SUBSCRIPTION_ACTIVE, SUBSCRIPTION_PAUSED, SUBSCRIPTION_CANCELLED, SUBSCRIPTION_EXPIRED}
}
// SubscriptionItem représente un élément d'un abonnement (ressource louée).
type SubscriptionItem struct {
ResourceID string `json:"resource_id" bson:"resource_id"`
ResourceType tools.DataType `json:"resource_type" bson:"resource_type"`
Quantity int `json:"quantity" bson:"quantity"`
UnitPrice float64 `json:"unit_price" bson:"unit_price"`
}
// Subscription représente un abonnement mensuel ou annuel à des ressources.
type Subscription struct {
utils.AbstractObject
SubscriberPeerID string `json:"subscriber_peer_id" bson:"subscriber_peer_id" validate:"required"`
ProviderPeerID string `json:"provider_peer_id,omitempty" bson:"provider_peer_id,omitempty"`
Status SubscriptionStatus `json:"status" bson:"status"`
PlanType pricing.PaymentType `json:"plan_type" bson:"plan_type"` // PAY_EVERY_MONTH ou PAY_EVERY_YEAR
Items []*SubscriptionItem `json:"items,omitempty" bson:"items,omitempty"`
Amount float64 `json:"amount" bson:"amount"`
Currency string `json:"currency" bson:"currency" default:"EUR"`
StartDate time.Time `json:"start_date" bson:"start_date"`
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"`
NextBillingDate time.Time `json:"next_billing_date" bson:"next_billing_date"`
AutoRenew bool `json:"auto_renew" bson:"auto_renew" default:"true"`
TrialEndDate *time.Time `json:"trial_end_date,omitempty" bson:"trial_end_date,omitempty"`
DiscountIDs []string `json:"discount_ids,omitempty" bson:"discount_ids,omitempty"`
CancelledAt *time.Time `json:"cancelled_at,omitempty" bson:"cancelled_at,omitempty"`
CancelReason string `json:"cancel_reason,omitempty" bson:"cancel_reason,omitempty"`
}
// newSubscription est le constructeur interne commun.
func newSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, plan pricing.PaymentType, nextBilling time.Time) *Subscription {
now := time.Now().UTC()
return &Subscription{
SubscriberPeerID: subscriberPeerID,
ProviderPeerID: providerPeerID,
Status: SUBSCRIPTION_PENDING,
PlanType: plan,
Items: items,
Amount: amount,
Currency: currency,
StartDate: now,
NextBillingDate: nextBilling,
AutoRenew: true,
}
}
// NewWeeklySubscription crée un abonnement hebdomadaire.
func NewWeeklySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
now := time.Now().UTC()
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_WEEK, now.AddDate(0, 0, 7))
}
// NewMonthlySubscription crée un abonnement mensuel.
func NewMonthlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
now := time.Now().UTC()
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_MONTH, now.AddDate(0, 1, 0))
}
// NewYearlySubscription crée un abonnement annuel.
func NewYearlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
now := time.Now().UTC()
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_YEAR, now.AddDate(1, 0, 0))
}
// NewTrialSubscription crée un abonnement mensuel avec période d'essai.
func NewTrialSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, trialDays int) *Subscription {
now := time.Now().UTC()
trialEnd := now.AddDate(0, 0, trialDays)
s := NewMonthlySubscription(subscriberPeerID, providerPeerID, items, amount, currency)
s.Status = SUBSCRIPTION_TRIAL
s.TrialEndDate = &trialEnd
s.NextBillingDate = trialEnd
return s
}
// Activate passe l'abonnement au statut actif (après premier paiement).
func (s *Subscription) Activate() {
s.Status = SUBSCRIPTION_ACTIVE
}
// Pause suspend l'abonnement.
func (s *Subscription) Pause() {
if s.Status == SUBSCRIPTION_ACTIVE {
s.Status = SUBSCRIPTION_PAUSED
}
}
// Resume réactive un abonnement suspendu.
func (s *Subscription) Resume() {
if s.Status == SUBSCRIPTION_PAUSED {
s.Status = SUBSCRIPTION_ACTIVE
}
}
// Cancel résilie l'abonnement.
func (s *Subscription) Cancel(reason string) {
now := time.Now().UTC()
s.Status = SUBSCRIPTION_CANCELLED
s.CancelledAt = &now
s.CancelReason = reason
s.AutoRenew = false
}
// Renew avance la prochaine date de facturation d'une période.
func (s *Subscription) Renew() {
switch s.PlanType {
case pricing.PAY_EVERY_WEEK:
s.NextBillingDate = s.NextBillingDate.AddDate(0, 0, 7)
case pricing.PAY_EVERY_MONTH:
s.NextBillingDate = s.NextBillingDate.AddDate(0, 1, 0)
case pricing.PAY_EVERY_YEAR:
s.NextBillingDate = s.NextBillingDate.AddDate(1, 0, 0)
}
}
// IsExpired vérifie si l'abonnement a dépassé sa date de fin.
func (s *Subscription) IsExpired() bool {
if s.EndDate == nil {
return false
}
return time.Now().UTC().After(*s.EndDate)
}
// IsBillingDue vérifie si la prochaine échéance est atteinte.
func (s *Subscription) IsBillingDue() bool {
return s.Status == SUBSCRIPTION_ACTIVE && !time.Now().UTC().Before(s.NextBillingDate)
}
// IsInTrial vérifie si l'abonnement est en période d'essai.
func (s *Subscription) IsInTrial() bool {
return s.Status == SUBSCRIPTION_TRIAL && s.TrialEndDate != nil && time.Now().UTC().Before(*s.TrialEndDate)
}
// ComputeAmount recalcule le montant total depuis les items.
func (s *Subscription) ComputeAmount() float64 {
total := 0.0
for _, item := range s.Items {
total += item.UnitPrice * float64(item.Quantity)
}
s.Amount = total
return total
}
func (s *Subscription) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (s *Subscription) StoreDraftDefault() {
s.IsDraft = true
}
func (s *Subscription) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Subscription)
if !s.IsDraft && s.Status != incoming.Status {
return true, &Subscription{
Status: incoming.Status,
AutoRenew: incoming.AutoRenew,
CancelReason: incoming.CancelReason,
}
}
return s.IsDraft, set
}
func (s *Subscription) CanDelete() bool {
return s.IsDraft || s.Status == SUBSCRIPTION_CANCELLED
}
@@ -0,0 +1,22 @@
package subscription
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type subscriptionMongoAccessor struct {
utils.AbstractAccessor[*Subscription]
}
func NewAccessor(request *tools.APIRequest) *subscriptionMongoAccessor {
return &subscriptionMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Subscription]{
Logger: logs.CreateLogger(tools.SUBSCRIPTION.String()),
Request: request,
Type: tools.SUBSCRIPTION,
New: func() *Subscription { return &Subscription{} },
},
}
}
@@ -1,9 +1,9 @@
package bill_test package billing_test
import ( import (
"testing" "testing"
"cloud.o-forge.io/core/oc-lib/models/bill" "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/common/enum"
"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/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
@@ -12,71 +12,67 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// ---- Bill model ----
func TestBill_StoreDraftDefault(t *testing.T) { func TestBill_StoreDraftDefault(t *testing.T) {
b := &bill.Bill{} b := &billing.Bill{}
b.StoreDraftDefault() b.StoreDraftDefault()
assert.True(t, b.IsDraft) assert.True(t, b.IsDraft)
} }
func TestBill_CanDelete_Draft(t *testing.T) { func TestBill_CanDelete_Draft(t *testing.T) {
b := &bill.Bill{} b := &billing.Bill{}
b.IsDraft = true b.IsDraft = true
assert.True(t, b.CanDelete()) assert.True(t, b.CanDelete())
} }
func TestBill_CanDelete_NonDraft(t *testing.T) { func TestBill_CanDelete_NonDraft(t *testing.T) {
b := &bill.Bill{} b := &billing.Bill{}
b.IsDraft = false b.IsDraft = false
assert.False(t, b.CanDelete()) assert.False(t, b.CanDelete())
} }
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) { func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING} b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = false b.IsDraft = false
set := &bill.Bill{Status: enum.PAID} set := &billing.Bill{Status: enum.PAID}
ok, returned := b.CanUpdate(set) ok, returned := b.CanUpdate(set)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, enum.PAID, returned.(*bill.Bill).Status) assert.Equal(t, enum.PAID, returned.(*billing.Bill).Status)
} }
func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) { func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING} b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = false b.IsDraft = false
set := &bill.Bill{Status: enum.PENDING} set := &billing.Bill{Status: enum.PENDING}
ok, _ := b.CanUpdate(set) ok, _ := b.CanUpdate(set)
assert.False(t, ok) assert.False(t, ok)
} }
func TestBill_CanUpdate_Draft(t *testing.T) { func TestBill_CanUpdate_Draft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING} b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = true b.IsDraft = true
set := &bill.Bill{Status: enum.PAID} set := &billing.Bill{Status: enum.PAID}
ok, _ := b.CanUpdate(set) ok, _ := b.CanUpdate(set)
assert.True(t, ok) assert.True(t, ok)
} }
func TestBill_GetAccessor(t *testing.T) { func TestBill_GetAccessor(t *testing.T) {
b := &bill.Bill{} b := &billing.Bill{}
acc := b.GetAccessor(&tools.APIRequest{}) acc := b.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc) assert.NotNil(t, acc)
} }
func TestBill_GetAccessor_NilRequest(t *testing.T) { func TestBill_GetAccessor_NilRequest(t *testing.T) {
b := &bill.Bill{} b := &billing.Bill{}
acc := b.GetAccessor(nil) acc := b.GetAccessor(nil)
assert.NotNil(t, acc) assert.NotNil(t, acc)
} }
// ---- GenerateBill ----
func TestGenerateBill_Basic(t *testing.T) { func TestGenerateBill_Basic(t *testing.T) {
o := &order.Order{ o := &order.Order{
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"}, AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
} }
req := &tools.APIRequest{PeerID: "peer-abc"} req := &tools.APIRequest{PeerID: "peer-abc"}
b, err := bill.GenerateBill(o, req) b, err := billing.GenerateBill(o, req)
require.NoError(t, err) require.NoError(t, err)
assert.NotNil(t, b) assert.NotNil(t, b)
assert.Equal(t, "order-uuid-1", b.OrderID) assert.Equal(t, "order-uuid-1", b.OrderID)
@@ -85,10 +81,8 @@ func TestGenerateBill_Basic(t *testing.T) {
assert.Contains(t, b.Name, "peer-abc") assert.Contains(t, b.Name, "peer-abc")
} }
// ---- SumUpBill ----
func TestBill_SumUpBill_NoSubOrders(t *testing.T) { func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
b := &bill.Bill{Total: 0} b := &billing.Bill{Total: 0}
result, err := b.SumUpBill(nil) result, err := b.SumUpBill(nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0.0, result.Total) assert.Equal(t, 0.0, result.Total)
+13 -2
View File
@@ -5,6 +5,7 @@ import (
"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/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"
) )
@@ -15,7 +16,7 @@ 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:"priced_item,omitempty"` 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 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"` ResumeMetrics map[string]map[string]models.MetricResume `json:"resume_metrics,omitempty" bson:"resume_metrics,omitempty"`
@@ -48,6 +49,16 @@ type Booking struct {
// OriginRef carries the registry reference of a peerless resource // OriginRef carries the registry reference of a peerless resource
// (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it. // (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it.
OriginRef string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` 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 { func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
@@ -135,5 +146,5 @@ func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
} }
func (r *Booking) CanDelete() bool { func (r *Booking) CanDelete() bool {
return r.IsDraft // only draft bookings can be deleted return true // only draft bookings can be deleted
} }
+8 -18
View File
@@ -1,23 +1,13 @@
package booking package booking
type BookingMode int 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 ( const (
PLANNED BookingMode = iota // predictible PLANNED = enum.PLANNED
PREEMPTED // can be both predictible or unpredictible, first one asking for a quick exec, second on event, but we pay to preempt in any case. PREEMPTED = enum.PREEMPTED
WHEN_POSSIBLE // unpredictable, two mode of payment can be available on that case: fixed, or per USE WHEN_POSSIBLE = enum.WHEN_POSSIBLE
) )
/*
Ok make a point there:
There is 3 notions about booking & payment :
Booking mode : WHEN is executed
Buying mode : Duration of payment
Pricing Mode : How Many time we pay
We can simplify Buying Mode and Pricing Mode, some Buying Mode implied limited pricing mode
Such as Rules. Just like PERMANENT BUYING can be paid only once.
Booking Mode on WHEN POSSIBLE make an exception, because we can't know when executed.
*/
+6 -6
View File
@@ -15,10 +15,10 @@ import (
// InstanceCapacity holds the maximum available resources of a single resource instance. // InstanceCapacity holds the maximum available resources of a single resource instance.
type InstanceCapacity struct { type InstanceCapacity struct {
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
} }
@@ -54,8 +54,8 @@ type PlannerITF interface {
// any availability check against a blocked resource returns false immediately. // any availability check against a blocked resource returns false immediately.
type Planner struct { type Planner struct {
GeneratedAt time.Time `json:"generated_at"` GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity 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 BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
} }
+16
View File
@@ -0,0 +1,16 @@
package enum
type BookingMode int
const (
PLANNED BookingMode = iota // timing prédictible
PREEMPTED // peut être interrompu, premium payé
WHEN_POSSIBLE // timing imprévisible
)
/*
3 notions distinctes :
- BookingMode : QUAND est exécuté (PLANNED / PREEMPTED / WHEN_POSSIBLE)
- BillingStrategy : fréquence de facturation (ONCE / WEEKLY / MONTHLY / YEARLY)
- PaymentType : mode de paiement par ressource (PAY_ONCE / PAY_EVERY_MONTH / ...)
*/
+10
View File
@@ -1,5 +1,7 @@
package enum package enum
import "fmt"
type InfrastructureType int type InfrastructureType int
const ( const (
@@ -18,3 +20,11 @@ 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,5 +1,7 @@
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
@@ -54,3 +56,11 @@ 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)
}
+67 -5
View File
@@ -1,11 +1,73 @@
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 {
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO PathSource
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 Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
Env map[string]string `json:"env,omitempty" bson:"env,omitempty"` // Env is the container environment variables Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
} }
type Expose struct { type Expose struct {
+6 -6
View File
@@ -7,12 +7,12 @@ type Artifact struct {
} }
type Param struct { type Param struct {
Name string `json:"name" bson:"name" validate:"required"` Name string `json:"name" bson:"name" validate:"required"`
Attr string `json:"attr,omitempty" bson:"attr,omitempty"` Attr string `json:"attr,omitempty" bson:"attr,omitempty"`
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"`
Optionnal bool `json:"optionnal" bson:"optionnal" default:"true"` Required bool `json:"required" bson:"required" default:"true"`
} }
type InOutputs struct { type InOutputs struct {
+2 -2
View File
@@ -3,7 +3,7 @@ package pricing
import ( import (
"time" "time"
"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/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
@@ -16,7 +16,7 @@ type PricedItemITF interface {
IsBooked() bool IsBooked() bool
GetQuantity() int GetQuantity() int
AddQuantity(amount int) AddQuantity(amount int)
GetBookingMode() booking.BookingMode GetBookingMode() enum.BookingMode
GetCreatorID() string GetCreatorID() string
SelectPricing() PricingProfileITF SelectPricing() PricingProfileITF
GetLocationStart() *time.Time GetLocationStart() *time.Time
+6 -6
View File
@@ -111,13 +111,11 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
fromAverageDuration := after.Sub(now).Seconds() fromAverageDuration := after.Sub(now).Seconds()
var tEnd time.Time var tEnd time.Time
if end == nil { fromDateDuration := float64(0)
tEnd = start.Add(5 * time.Minute) if end != nil {
} 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
} }
@@ -126,6 +124,9 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInSecond float64, start time.Time, end *time.Time) (float64, error) { 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 {
@@ -176,7 +177,6 @@ func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration
} }
return p.Price, nil return p.Price, nil
case PERMANENT: case PERMANENT:
if variations != nil { if variations != nil {
price := p.Price price := p.Price
+4 -1
View File
@@ -1,6 +1,8 @@
package live package live
import ( import (
"fmt"
"cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models" "cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/utils"
@@ -36,7 +38,8 @@ type LiveDatacenter struct {
} }
func (r *LiveDatacenter) IsCompatible(service map[string]interface{}) bool { func (r *LiveDatacenter) IsCompatible(service map[string]interface{}) bool {
return service["infrastructure"] == r.Infrastructure && service["architecture"] == r.Architecture 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 { func (d *LiveDatacenter) GetAccessor(request *tools.APIRequest) utils.Accessor {
+10 -6
View File
@@ -1,6 +1,8 @@
package live package live
import ( import (
"fmt"
"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"
@@ -26,11 +28,11 @@ func (p ServiceProtocol) String() string {
// rather than trusted from the ServiceResource, which may be stale. // rather than trusted from the ServiceResource, which may be stale.
type LiveService struct { type LiveService struct {
AbstractLive AbstractLive
MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"` MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"`
Protocol ServiceProtocol `json:"protocol" bson:"protocol" default:"0"` Protocol ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"` EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,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 Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
} }
func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor {
@@ -38,5 +40,7 @@ func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor {
} }
func (r *LiveService) IsCompatible(service map[string]interface{}) bool { func (r *LiveService) IsCompatible(service map[string]interface{}) bool {
return service["infrastructure"] == r.Infrastructure fmt.Println("COMPARE <", service["infrastructure"], "> <", r.Infrastructure, ">")
return r.Infrastructure.Compare(service["infrastructure"])
} }
+5 -1
View File
@@ -1,6 +1,8 @@
package live package live
import ( import (
"fmt"
"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"
@@ -26,7 +28,9 @@ type LiveStorage struct {
} }
func (r *LiveStorage) IsCompatible(service map[string]interface{}) bool { func (r *LiveStorage) IsCompatible(service map[string]interface{}) bool {
return service["storage_type"] == r.StorageType 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 { func (d *LiveStorage) GetAccessor(request *tools.APIRequest) utils.Accessor {
+12 -2
View File
@@ -3,10 +3,15 @@ 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/allowed_image"
"cloud.o-forge.io/core/oc-lib/models/bill" "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/execution_verification"
"cloud.o-forge.io/core/oc-lib/models/live" "cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/order" "cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/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/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
@@ -47,9 +52,14 @@ var ModelsCatalog = map[string]func() utils.DBObject{
tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} }, tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} },
tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} }, tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
tools.LIVE_SERVICE.String(): func() utils.DBObject { return &live.LiveService{} }, tools.LIVE_SERVICE.String(): func() utils.DBObject { return &live.LiveService{} },
tools.BILL.String(): func() utils.DBObject { return &bill.Bill{} }, 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.EXECUTION_VERIFICATION.String(): func() utils.DBObject { return &execution_verification.ExecutionVerification{} },
tools.ALLOWED_IMAGE.String(): func() utils.DBObject { return &allowed_image.AllowedImage{} }, 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
+5
View File
@@ -22,7 +22,12 @@ type Order struct {
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"` Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"`
Bookings []*booking.Booking `json:"bookings" bson:"bookings"` Bookings []*booking.Booking `json:"bookings" bson:"bookings"`
// Billing groupe les bookings par fréquence de facturation, peuplé par GenerateOrder.
Billing map[pricing.BillingStrategy][]*booking.Booking `json:"billing" bson:"billing"` 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() {
+11
View File
@@ -0,0 +1,11 @@
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"`
}
+37 -1
View File
@@ -5,6 +5,7 @@ import (
"strings" "strings"
"time" "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" "github.com/biter777/countries"
@@ -30,12 +31,23 @@ const (
NANO NANO
PENDING_NANO PENDING_NANO
PENDING_MASTER PENDING_MASTER
ORGANIZATION_MASTER
ORGANIZATION_MEMBER
ORGANIZATION_PARTNER
ORGANIZATION_MASTER_PENDING
ORGANIZATION_MEMBER_PENDING
) )
var path = []string{"unknown", "self", "partner", "blacklist", "partner", "pending_partner", "master", "nano", "pending_nano", "pending_master"} var path = []string{
"known", "self", "partner", "blacklist", "pending_partner",
"master", "nano", "pending_nano", "pending_master",
"organization_master", "organization_member", "organization_partner",
"organization_master_pending", "organization_member_pending",
}
func GetRelationPath(str string) int { func GetRelationPath(str string) int {
for i, p := range path { for i, p := range path {
fmt.Println("GetRelationPath", i, p)
if str == p { if str == p {
return i return i
} }
@@ -115,6 +127,25 @@ type Peer struct {
BlacklistReason string `json:"blacklist_reason,omitempty" bson:"blacklist_reason,omitempty"` BlacklistReason string `json:"blacklist_reason,omitempty" bson:"blacklist_reason,omitempty"`
BehaviorWarnings []BehaviorWarning `json:"behavior_warnings,omitempty" bson:"behavior_warnings,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:"-"). // Volatile connectivity state — never persisted to DB (bson:"-").
// Set in-memory by oc-peer when it receives a PEER_OBSERVE_RESPONSE_EVENT. // 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). // Considered offline when LastHeartbeat is older than 60 s (30 s interval + 30 s grace).
@@ -131,6 +162,11 @@ func (ri *Peer) Extend(typ ...string) map[string][]tools.DataType {
ext[t] = []tools.DataType{} ext[t] = []tools.DataType{}
} }
ext[t] = append(ext[t], tools.PEER) 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 return ext
+30
View File
@@ -0,0 +1,30 @@
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)
}
@@ -0,0 +1,31 @@
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}},
},
}
}
+14
View File
@@ -0,0 +1,14 @@
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
}
+1 -1
View File
@@ -62,7 +62,7 @@ func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInsta
type DataInstance struct { type DataInstance struct {
ResourceInstance[*DataResourcePartnership] ResourceInstance[*DataResourcePartnership]
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
} }
func NewDataInstance(name string, peerID string) ResourceInstanceITF { func NewDataInstance(name string, peerID string) ResourceInstanceITF {
+240 -33
View File
@@ -20,9 +20,9 @@ import (
*/ */
type DynamicResource struct { type DynamicResource struct {
AbstractResource AbstractResource
Type tools.DataType `bson:"type,omitempty" json:"type,omitempty"` Type tools.DataType `bson:"type,omitempty" json:"type,omitempty"`
Filters map[string]interface{} `bson:"filters,omitempty" json:"filters,omitempty"` Filters dbs.Filters `bson:"filters,omitempty" json:"filters,omitempty"`
SortRules map[string]string `bson:"rules,omitempty" json:"rules,omitempty"` SortRules map[string]string `bson:"rules,omitempty" json:"rules,omitempty"`
PeerIds map[int]string `bson:"peer_ids,omitempty" json:"peer_ids,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"` ResourceIds map[int]string `bson:"resource_ids,omitempty" json:"resource_ids,omitempty"`
@@ -37,44 +37,249 @@ type DynamicResource struct {
WatchedDynamicResource []string `bson:"watched_dynamic_resource,omitempty" json:"watched_dynamic_resource,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 { func (d *DynamicResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return nil return nil
} }
func (d *DynamicResource) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF { func (d *DynamicResource) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
d.Instances = []ResourceInstanceITF{} if WorkspaceCandidatesProvider != nil {
for k, v := range map[tools.DataType]ResourceInterface{ candidates := WorkspaceCandidatesProvider(d.Type, request)
tools.COMPUTE_RESOURCE: &ComputeResource{}, return d.SetAllowedInstancesFromSet(candidates, request, instance_id...)
tools.DATA_RESOURCE: &DataResource{},
tools.STORAGE_RESOURCE: &StorageResource{},
tools.PROCESSING_RESOURCE: &ProcessingResource{},
tools.WORKFLOW_RESOURCE: &WorkflowResource{}} {
if d.Type != k {
continue
}
access := NewAccessor[*DynamicResource](k, request)
a, _, _ := access.Search(dbs.FiltersFromFlatMap(d.Filters, v), "", false, 0, 100000)
d.PeerIds = map[int]string{}
d.ResourceIds = map[int]string{}
for _, res := range a {
for _, i := range res.(ResourceInterface).SetAllowedInstances(request, instance_id...) {
d.PeerIds[len(d.Instances)] = res.GetCreatorID()
d.ResourceIds[len(d.Instances)] = res.GetID()
d.Instances = append(d.Instances, i)
}
}
break
} }
sorted := make([]ResourceInstanceITF, len(d.Instances)) d.Instances = []ResourceInstanceITF{}
copy(sorted, d.Instances) d.sortAndResetInstances()
slices.SortStableFunc(sorted, func(a, b ResourceInstanceITF) int {
d.SortRules["partnerships"] = "%v not contains 2"
return d.compareByRules(a, b, d.SortRules)
})
d.WatchedDynamicResource = []string{}
return d.Instances 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) { func (d *DynamicResource) AddInstances(instance ResourceInstanceITF) {
d.Instances = append(d.Instances, instance) d.Instances = append(d.Instances, instance)
} }
@@ -91,13 +296,15 @@ func (d *DynamicResource) GetSelectedInstance(index *int) ResourceInstanceITF {
d.SelectedIndex = i d.SelectedIndex = i
for i := range inst.GetPartnerships() { 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 { if inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy) != nil {
d.SelectedPartnershipIndex = &i d.SelectedPartnershipIndex = &i
break break
} }
} }
if d.SelectedPartnershipIndex == nil { if d.SelectedPartnershipIndex == nil {
continue i := 0
d.SelectedPartnershipIndex = &i
} }
return inst return inst
} }
@@ -0,0 +1,223 @@
package resources
// exploitation_authorization.go — Autorisation d'Exploitation (AE)
//
// AEs are embedded inside AbstractResource (field ExploitationAuthorizations).
// They are NOT a separate MongoDB collection — each resource document carries
// its own AEs, just like it carries its Instances.
//
// # Visibility filtering
//
// When a resource is returned to a consumer peer the AE list is filtered:
// - AllowedPeerIDs empty → public AE, visible to all peers.
// - AllowedPeerIDs non-empty, contains requester → visible to that peer.
// - AllowedPeerIDs non-empty, doesn't contain requester → stripped from response.
//
// The resource owner always sees all of their own AEs unfiltered.
//
// # Enforcement
//
// oc-schedulerd's validateWorkflowIntegrity calls CheckWorkflowAE (defined in
// its own package to avoid circular imports) before launching any execution.
// Violations emit PEER_BEHAVIOR_EVENT(BehaviorFraud) against the consumer peer
// and cause the execution to be rejected.
import (
"encoding/json"
"fmt"
"slices"
"time"
"cloud.o-forge.io/core/oc-lib/tools"
)
// CouplingConstraint defines which resources must or must not co-exist in the
// same workflow when the protected resource is included.
type CouplingConstraint struct {
// RequiredResourceIDs — ALL of these resource UUIDs must appear in the
// workflow alongside the protected resource.
RequiredResourceIDs []string `json:"required_resource_ids,omitempty" bson:"required_resource_ids,omitempty"`
// ForbiddenResourceIDs — NONE of these resource UUIDs may appear in the
// workflow alongside the protected resource.
ForbiddenResourceIDs []string `json:"forbidden_resource_ids,omitempty" bson:"forbidden_resource_ids,omitempty"`
}
// ExploitationAuthorization (AE) is embedded in a resource and restricts how
// the resource may be used by other consumer peers.
//
// It is stored as part of the resource document (bson embedded), not as a
// separate collection. Create/update it by PATCHing the parent resource.
type ExploitationAuthorization struct {
// ID is a client-assigned UUID so individual AEs can be referenced.
ID string `json:"id" bson:"id"`
// Name is a human-readable label shown in the catalog detail view.
Name string `json:"name,omitempty" bson:"name,omitempty"`
// AllowedPeerIDs restricts which consumer peers may use the resource.
// An empty list means any peer is allowed.
AllowedPeerIDs []string `json:"allowed_peer_ids,omitempty" bson:"allowed_peer_ids,omitempty"`
// AllowedWorkflowIDs restricts which workflow IDs may include the resource.
// An empty list means any workflow is allowed.
AllowedWorkflowIDs []string `json:"allowed_workflow_ids,omitempty" bson:"allowed_workflow_ids,omitempty"`
// Coupling describes positive (required) and negative (forbidden) coupling.
// Nil means no coupling constraint.
Coupling *CouplingConstraint `json:"coupling,omitempty" bson:"coupling,omitempty"`
// ValidFrom / ValidUntil define the active window.
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
// IsRevoked allows instant revocation without deleting the AE from the resource.
IsRevoked bool `json:"is_revoked" bson:"is_revoked"`
}
// IsVisibleTo returns true when this AE should be included in the response to
// peerID. The resource owner (creatorID) always sees all AEs.
func (ae *ExploitationAuthorization) IsVisibleTo(peerID, creatorID string) bool {
if peerID == creatorID {
return true // owner sees everything
}
return len(ae.AllowedPeerIDs) == 0 || slices.Contains(ae.AllowedPeerIDs, peerID)
}
// CheckAE evaluates this AE against the execution context and returns any
// violations found. workflowResourceIDs is the set of all resource UUIDs in
// the workflow; resourceID is the UUID of the resource this AE belongs to.
func (ae *ExploitationAuthorization) CheckAE(
resourceID, workflowID, consumerPeerID string,
workflowResourceIDs map[string]struct{},
now time.Time,
) []AEViolation {
var vs []AEViolation
add := func(t AEViolationType, msg string) {
vs = append(vs, AEViolation{AEID: ae.ID, ResourceID: resourceID, Type: t, Message: msg})
}
if ae.IsRevoked {
add(AEViolationRevoked, fmt.Sprintf("AE %s for resource %s is revoked", ae.ID, resourceID))
return vs
}
if ae.ValidUntil != nil && now.After(*ae.ValidUntil) {
add(AEViolationExpired, fmt.Sprintf("AE %s for resource %s expired at %s",
ae.ID, resourceID, ae.ValidUntil.Format(time.RFC3339)))
return vs
}
if ae.ValidFrom != nil && now.Before(*ae.ValidFrom) {
add(AEViolationNotYetValid, fmt.Sprintf("AE %s for resource %s not valid until %s",
ae.ID, resourceID, ae.ValidFrom.Format(time.RFC3339)))
return vs
}
if consumerPeerID != "" && len(ae.AllowedPeerIDs) > 0 {
if !slices.Contains(ae.AllowedPeerIDs, consumerPeerID) {
add(AEViolationPeerNotAllowed, fmt.Sprintf(
"peer %s not allowed to use resource %s (AE %s)", consumerPeerID, resourceID, ae.ID))
}
}
if workflowID != "" && len(ae.AllowedWorkflowIDs) > 0 {
if !slices.Contains(ae.AllowedWorkflowIDs, workflowID) {
add(AEViolationWorkflowNotAllow, fmt.Sprintf(
"workflow %s not in allowed-workflow list for resource %s (AE %s)", workflowID, resourceID, ae.ID))
}
}
if ae.Coupling != nil {
for _, req := range ae.Coupling.RequiredResourceIDs {
if _, ok := workflowResourceIDs[req]; !ok {
add(AEViolationCouplingRequired, fmt.Sprintf(
"resource %s requires %s to be present (AE %s)", resourceID, req, ae.ID))
}
}
for _, forb := range ae.Coupling.ForbiddenResourceIDs {
if _, ok := workflowResourceIDs[forb]; ok {
add(AEViolationCouplingForbid, fmt.Sprintf(
"resource %s forbids co-use with %s (AE %s)", resourceID, forb, ae.ID))
}
}
}
return vs
}
// ── Violation types ───────────────────────────────────────────────────────────
type AEViolationType string
const (
AEViolationRevoked AEViolationType = "ae_revoked"
AEViolationExpired AEViolationType = "ae_expired"
AEViolationNotYetValid AEViolationType = "ae_not_yet_valid"
AEViolationPeerNotAllowed AEViolationType = "ae_peer_not_allowed"
AEViolationWorkflowNotAllow AEViolationType = "ae_workflow_not_allowed"
AEViolationCouplingRequired AEViolationType = "ae_coupling_required"
AEViolationCouplingForbid AEViolationType = "ae_coupling_forbidden"
)
// AEViolation describes a single constraint that was not satisfied.
type AEViolation struct {
AEID string
ResourceID string
Type AEViolationType
Message string
}
// ── NATS emit helper (uses tools only — no oclib circular import) ─────────────
// EmitAEBehaviorReport emits a PEER_BEHAVIOR_EVENT(BehaviorFraud) for each
// unique AE violation. Call this before rejecting the execution.
func EmitAEBehaviorReport(consumerPeerID string, violations []AEViolation) {
if consumerPeerID == "" || len(violations) == 0 {
return
}
seen := map[string]struct{}{}
for _, v := range violations {
key := v.AEID + ":" + v.ResourceID
if _, dup := seen[key]; dup {
continue
}
seen[key] = struct{}{}
report := tools.PeerBehaviorReport{
ReporterApp: "oc-scheduler",
TargetPeerID: consumerPeerID,
Severity: tools.BehaviorFraud,
Reason: fmt.Sprintf("AE violation (%s): %s", v.Type, v.Message),
Evidence: v.AEID,
At: time.Now().UTC(),
}
if b, err := json.Marshal(report); err == nil {
tools.NewNATSCaller().SetNATSPub(tools.PEER_BEHAVIOR_EVENT, tools.NATSResponse{
FromApp: "oc-scheduler",
Method: int(tools.PEER_BEHAVIOR_EVENT),
Payload: b,
})
}
}
}
// OriginType qualifies where a resource instance comes from.
type OriginType int
const (
// OriginPeer: instance offered by a known network peer (default).
OriginPeer OriginType = iota
// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
// No peer confirmation is needed; access is unrestricted.
OriginPublic
// OriginSelf: self-hosted instance with no third-party peer.
OriginSelf
)
// OriginMeta carries provenance information for a resource instance.
type OriginMeta struct {
Type OriginType `json:"origin_type" bson:"origin_type"`
Ref string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
Verified bool `json:"origin_verified" bson:"origin_verified"` // manually vetted by an OC admin
}
// IsPeerless MUST NOT be used for authorization decisions.
// Use ResourceInstance.IsPeerless() instead, which derives the property
// from structural invariants rather than this self-declared field.
//
// This method is kept only for display/logging purposes.
func (o OriginMeta) DeclaredPeerless() bool {
return o.Type != OriginPeer
}
+2
View File
@@ -29,6 +29,8 @@ type ResourceInterface interface {
GetEnv() []models.Param GetEnv() []models.Param
GetInputs() []models.Param GetInputs() []models.Param
GetOutputs() []models.Param GetOutputs() []models.Param
GetExploitationAuthorizations() []ExploitationAuthorization
GetConsents() []Consent
} }
type ResourceInstanceITF interface { type ResourceInstanceITF interface {
+125 -1
View File
@@ -15,7 +15,8 @@ type ResourceSet struct {
Services []string `bson:"services,omitempty" json:"services,omitempty"` Services []string `bson:"services,omitempty" json:"services,omitempty"`
Dynamics []string `bson:"dynamics,omitempty" json:"dynamics,omitempty"` Dynamics []string `bson:"dynamics,omitempty" json:"dynamics,omitempty"`
// DynamicResources are stored inline — no DB collection, resolved at runtime via SetAllowedInstances. // 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"` 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"`
@@ -26,6 +27,129 @@ type ResourceSet struct {
ServiceResources []*ServiceResource `bson:"-" json:"service_resources,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() {
r.DataResources = nil r.DataResources = nil
r.StorageResources = nil r.StorageResources = nil
+7 -1
View File
@@ -38,7 +38,13 @@ func (d *NativeTool) ClearEnv() utils.DBObject {
} }
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF { func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */ // 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{} return []ResourceInstanceITF{}
} }
-31
View File
@@ -1,31 +0,0 @@
package resources
// 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"
License string `json:"origin_license,omitempty" bson:"origin_license,omitempty"` // SPDX identifier or free-form
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
}
+9 -9
View File
@@ -31,23 +31,23 @@ type ProcessingResource struct {
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"` Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"`
OpenSource bool `json:"open_source" bson:"open_source" default:"false"` OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
License string `json:"license,omitempty" bson:"license,omitempty"` // IsService marks a long-running processing that acts as a persistent service.
Maturity string `json:"maturity,omitempty" bson:"maturity,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"`
// 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 *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"` SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"` ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
} }
func NewProcessingInstance(name string, peerID string) ResourceInstanceITF { func NewProcessingInstance(name string, peerID string) ResourceInstanceITF {
+63 -2
View File
@@ -3,6 +3,7 @@ package resources
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"slices" "slices"
"time" "time"
@@ -39,6 +40,20 @@ type AbstractResource struct {
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"` Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"` Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,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 (ri *AbstractResource) Extend(typ ...string) map[string][]tools.DataType {
@@ -83,6 +98,33 @@ 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 { func (ri *AbstractResource) ClearEnv() utils.DBObject {
ri.Env = []models.Param{} ri.Env = []models.Param{}
ri.Inputs = []models.Param{} ri.Inputs = []models.Param{}
@@ -112,7 +154,8 @@ func (r *AbstractResource) StoreDraftDefault() {
} }
func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) { func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
return r.IsDraft, set fmt.Println("IsDrafted", r.IsDraft, set.IsDrafted())
return r.IsDraft || set.IsDrafted(), set
} }
type AbstractInstanciatedResource[T ResourceInstanceITF] struct { type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
@@ -201,12 +244,15 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF { func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) { if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...) abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
// Filter AEs: only return AEs visible to the requesting peer.
if request != nil {
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
} }
inst := []ResourceInstanceITF{} inst := []ResourceInstanceITF{}
for _, i := range abs.Instances { for _, i := range abs.Instances {
inst = append(inst, i) inst = append(inst, i)
} }
return inst return inst
} }
@@ -528,3 +574,18 @@ func ToResource(
} }
return nil, errors.New("can't found any data resources matching") 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
}
+201 -2
View File
@@ -1,12 +1,15 @@
package resources package resources
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "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/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"
@@ -16,6 +19,48 @@ type ResourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
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) *ResourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{ if !slices.Contains([]tools.DataType{
@@ -67,6 +112,40 @@ func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, er
return data, code, err return data, code, err
} }
var workspaceResourceTypes = []tools.DataType{
tools.COMPUTE_RESOURCE,
tools.DATA_RESOURCE,
tools.PROCESSING_RESOURCE,
tools.STORAGE_RESOURCE,
tools.WORKFLOW_RESOURCE,
tools.SERVICE_RESOURCE,
}
func emitResourceNATS(method tools.NATSMethod, dt tools.DataType, payload []byte) {
if !slices.Contains(workspaceResourceTypes, dt) {
return
}
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) {
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) { func (dca *ResourceMongoAccessor[T]) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE { if dca.GetType() == tools.COMPUTE_RESOURCE {
delete(set, "architecture") delete(set, "architecture")
@@ -76,6 +155,14 @@ func (dca *ResourceMongoAccessor[T]) UpdateOne(set map[string]interface{}, id st
} else if dca.GetType() == tools.STORAGE_RESOURCE { } else if dca.GetType() == tools.STORAGE_RESOURCE {
delete(set, "storage_type") 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) return utils.GenericUpdateOne(set, id, dca)
} }
@@ -94,6 +181,7 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
} }
a = live.NewAccessor[*live.LiveDatacenter](tools.LIVE_DATACENTER, &tools.APIRequest{Admin: true}) a = live.NewAccessor[*live.LiveDatacenter](tools.LIVE_DATACENTER, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID()) res, _, _ := a.LoadOne(r.Instances[0].GetID())
fmt.Println(res, r.Instances[0].GetID())
if res == nil { if res == nil {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog") return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
} }
@@ -110,7 +198,7 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
a = live.NewAccessor[*live.LiveService](tools.LIVE_SERVICE, &tools.APIRequest{Admin: true}) a = live.NewAccessor[*live.LiveService](tools.LIVE_SERVICE, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID()) res, _, _ := a.LoadOne(r.Instances[0].GetID())
if res == nil { if res == nil {
return nil, 404, errors.New("can't create a non existing service resource not reported onto compute units catalog") 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)) { if !res.(*live.LiveService).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live service target is not compatible") return nil, 404, errors.New("live service target is not compatible")
@@ -125,7 +213,7 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
a = live.NewAccessor[*live.LiveStorage](tools.LIVE_STORAGE, &tools.APIRequest{Admin: true}) a = live.NewAccessor[*live.LiveStorage](tools.LIVE_STORAGE, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID()) res, _, _ := a.LoadOne(r.Instances[0].GetID())
if res == nil { if res == nil {
return nil, 404, errors.New("can't create a non existing storage resource not reported onto compute units catalog") 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)) { if !res.(*live.LiveStorage).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live storage target is not compatible") return nil, 404, errors.New("live storage target is not compatible")
@@ -133,6 +221,7 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
i = res.GetID() i = res.GetID()
idsToUpdate = res.(*live.LiveStorage).ResourcesID idsToUpdate = res.(*live.LiveStorage).ResourcesID
} }
applyAccessSourceOutput(data)
res, code, err := utils.GenericStoreOne(data, dca) res, code, err := utils.GenericStoreOne(data, dca)
if res != nil && i != "" { if res != nil && i != "" {
idsToUpdate = append(idsToUpdate, res.GetID()) idsToUpdate = append(idsToUpdate, res.GetID())
@@ -140,9 +229,119 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
"resources_id": idsToUpdate, "resources_id": idsToUpdate,
}, i) }, i)
} }
if err == nil && res != nil {
b, _ := json.Marshal(res)
go emitResourceNATS(tools.CREATE_RESOURCE, dca.GetType(), b)
}
return res, code, err 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) { func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return dca.StoreOne(data) return dca.StoreOne(data)
} }
+7 -1
View File
@@ -31,7 +31,13 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
} }
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF { func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */ // 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{} return []ResourceInstanceITF{}
} }
+5 -1
View File
@@ -98,6 +98,10 @@ func (r *AbstractObject) DeepCopy() *AbstractObject {
return &obj return &obj
} }
func (r *AbstractObject) SetDraft(draft bool) {
r.IsDraft = draft
}
func (r *AbstractObject) SetName(name string) { func (r *AbstractObject) SetName(name string) {
r.Name = name r.Name = name
} }
@@ -146,7 +150,7 @@ 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.CreatorID == "" {
ao.CreationDate = time.Now() ao.CreationDate = time.Now()
ao.CreatorID = peer ao.CreatorID = peer
ao.UserCreatorID = user ao.UserCreatorID = user
+86 -4
View File
@@ -109,6 +109,9 @@ func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor)
obj := a.NewObj() obj := a.NewObj()
b, _ := json.Marshal(r) b, _ := json.Marshal(r)
json.Unmarshal(b, obj) json.Unmarshal(b, obj)
if change["is_draft"] == true {
obj.SetDraft(change["is_draft"] == true)
}
if !a.GetRequest().Admin { if !a.GetRequest().Admin {
var ok bool var ok bool
ok, r = r.CanUpdate(obj) ok, r = r.CanUpdate(obj)
@@ -125,10 +128,8 @@ func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor)
r.Sign() r.Sign()
} }
loaded := r.Serialize(r) // get the loaded object loaded := r.Serialize(r) // get the loaded object
for k, v := range change { // apply the changes, with a flatten method deepMerge(loaded, change)
loaded[k] = v
}
newObj := a.NewObj() newObj := a.NewObj()
b, err = json.Marshal(loaded) b, err = json.Marshal(loaded)
if err != nil { if err != nil {
@@ -252,6 +253,87 @@ func IsMySelf(peerID string, wfa Accessor) (bool, string) {
return peerID == pp.GetID(), pp.GetID() 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) { func GenerateNodeID() (string, error) {
folderStatic := "/var/lib/opencloud-node" folderStatic := "/var/lib/opencloud-node"
if _, err := os.Stat(folderStatic); err == nil { if _, err := os.Stat(folderStatic); err == nil {
+1
View File
@@ -26,6 +26,7 @@ type DBObject interface {
GetID() string GetID() string
GetName() string GetName() string
SetName(name string) SetName(name string)
SetDraft(draft bool)
IsDrafted() bool IsDrafted() bool
CanDelete() bool CanDelete() bool
StoreDraftDefault() StoreDraftDefault()
+44
View File
@@ -145,6 +145,50 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
return max, nil return max, nil
} }
// DataStorageLink represents a resolved Data→Storage pair found in the graph.
type DataStorageLink struct {
DataItemID string
StorageItemID string
}
// GetDataStorageLinks returns all links that connect a Data item to a Storage item.
// These links are mandatory when the Data instance has a Source configured:
// the workflow builder uses them to know where to download the data before
// any processing step that consumes that storage.
func (g *Graph) GetDataStorageLinks() []DataStorageLink {
var result []DataStorageLink
for _, link := range g.Links {
srcItem, srcOk := g.Items[link.Source.ID]
dstItem, dstOk := g.Items[link.Destination.ID]
if !srcOk || !dstOk {
continue
}
if g.IsData(srcItem) && g.IsStorage(dstItem) {
result = append(result, DataStorageLink{
DataItemID: link.Source.ID,
StorageItemID: link.Destination.ID,
})
} else if g.IsStorage(srcItem) && g.IsData(dstItem) {
result = append(result, DataStorageLink{
DataItemID: link.Destination.ID,
StorageItemID: link.Source.ID,
})
}
}
return result
}
// GetLinkedStorageForData returns the storage item IDs linked to a given Data item.
func (g *Graph) GetLinkedStorageForData(dataItemID string) []string {
var storageIDs []string
for _, dsl := range g.GetDataStorageLinks() {
if dsl.DataItemID == dataItemID {
storageIDs = append(storageIDs, dsl.StorageItemID)
}
}
return storageIDs
}
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) { func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
if item, ok := g.Items[id]; ok { if item, ok := g.Items[id]; ok {
if item.NativeTool != nil { if item.NativeTool != nil {
+23 -23
View File
@@ -15,32 +15,32 @@ type GraphItem struct {
} }
func (g *GraphItem) GetResource() (tools.DataType, resources.ResourceInterface) { func (g *GraphItem) GetResource() (tools.DataType, resources.ResourceInterface) {
if g.Data != nil { if g.ItemResource.Data != nil {
return tools.DATA_RESOURCE, g.Data return tools.DATA_RESOURCE, g.ItemResource.Data
} else if g.Compute != nil { } else if g.ItemResource.Compute != nil {
return tools.COMPUTE_RESOURCE, g.Compute return tools.COMPUTE_RESOURCE, g.ItemResource.Compute
} else if g.Workflow != nil { } else if g.ItemResource.Workflow != nil {
return tools.WORKFLOW_RESOURCE, g.Workflow return tools.WORKFLOW_RESOURCE, g.ItemResource.Workflow
} else if g.Processing != nil { } else if g.ItemResource.Processing != nil {
return tools.PROCESSING_RESOURCE, g.Processing return tools.PROCESSING_RESOURCE, g.ItemResource.Processing
} else if g.Storage != nil { } else if g.ItemResource.Storage != nil {
return tools.STORAGE_RESOURCE, g.Storage return tools.STORAGE_RESOURCE, g.ItemResource.Storage
} else if g.NativeTool != nil { } else if g.ItemResource.NativeTool != nil {
return tools.NATIVE_TOOL, g.NativeTool return tools.NATIVE_TOOL, g.ItemResource.NativeTool
} else if g.Service != nil { } else if g.ItemResource.Service != nil {
return tools.SERVICE_RESOURCE, g.Service return tools.SERVICE_RESOURCE, g.ItemResource.Service
} else if g.Dynamic != nil { } else if g.ItemResource.Dynamic != nil {
return tools.DYNAMIC_RESOURCE, g.Dynamic return tools.DYNAMIC_RESOURCE, g.ItemResource.Dynamic
} }
return tools.INVALID, nil return tools.INVALID, nil
} }
func (g *GraphItem) Clear() { func (g *GraphItem) Clear() {
g.Data = nil g.ItemResource.Data = nil
g.Compute = nil g.ItemResource.Compute = nil
g.Workflow = nil g.ItemResource.Workflow = nil
g.Processing = nil g.ItemResource.Processing = nil
g.Storage = nil g.ItemResource.Storage = nil
g.Service = nil g.ItemResource.Service = nil
g.Dynamic = nil g.ItemResource.Dynamic = nil
} }
+4 -4
View File
@@ -23,11 +23,11 @@ func (l *GraphLink) IsComputeLink(g Graph) (bool, string) {
if g.Items == nil { if g.Items == nil {
return false, "" return false, ""
} }
if d, ok := g.Items[l.Source.ID]; ok && d.Compute != nil { if d, ok := g.Items[l.Source.ID]; ok && d.ItemResource.Compute != nil {
return true, d.Compute.UUID return true, d.ItemResource.Compute.UUID
} }
if d, ok := g.Items[l.Destination.ID]; ok && d.Compute != nil { if d, ok := g.Items[l.Destination.ID]; ok && d.ItemResource.Compute != nil {
return true, d.Compute.UUID return true, d.ItemResource.Compute.UUID
} }
return false, "" return false, ""
} }
+18 -18
View File
@@ -104,17 +104,17 @@ func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
func plantUMLPrefix(item graph.GraphItem) string { func plantUMLPrefix(item graph.GraphItem) string {
switch { switch {
case item.NativeTool != nil: case item.ItemResource.NativeTool != nil:
return "e" return "e"
case item.Data != nil: case item.ItemResource.Data != nil:
return "d" return "d"
case item.Processing != nil: case item.ItemResource.Processing != nil:
return "p" return "p"
case item.Storage != nil: case item.ItemResource.Storage != nil:
return "s" return "s"
case item.Compute != nil: case item.ItemResource.Compute != nil:
return "c" return "c"
case item.Workflow != nil: case item.ItemResource.Workflow != nil:
return "wf" return "wf"
} }
return "u" return "u"
@@ -123,24 +123,24 @@ func plantUMLPrefix(item graph.GraphItem) string {
// plantUMLItemLine builds the PlantUML declaration line for one graph item. // plantUMLItemLine builds the PlantUML declaration line for one graph item.
func plantUMLItemLine(varName string, item graph.GraphItem) string { func plantUMLItemLine(varName string, item graph.GraphItem) string {
switch { switch {
case item.NativeTool != nil: case item.ItemResource.NativeTool != nil:
// WorkflowEvent has no instance and no configurable attributes. // WorkflowEvent has no instance and no configurable attributes.
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.NativeTool.GetName()) return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.ItemResource.NativeTool.GetName())
case item.Data != nil: case item.ItemResource.Data != nil:
return plantUMLResourceLine("Data", varName, item.Data) return plantUMLResourceLine("Data", varName, item.ItemResource.Data)
case item.Processing != nil: case item.ItemResource.Processing != nil:
return plantUMLResourceLine("Processing", varName, item.Processing) return plantUMLResourceLine("Processing", varName, item.ItemResource.Processing)
case item.Storage != nil: case item.ItemResource.Storage != nil:
return plantUMLResourceLine("Storage", varName, item.Storage) return plantUMLResourceLine("Storage", varName, item.ItemResource.Storage)
case item.Compute != nil: case item.ItemResource.Compute != nil:
return plantUMLResourceLine("ComputeUnit", varName, item.Compute) return plantUMLResourceLine("ComputeUnit", varName, item.ItemResource.Compute)
case item.Workflow != nil: case item.ItemResource.Workflow != nil:
return plantUMLResourceLine("Workflow", varName, item.Workflow) return plantUMLResourceLine("Workflow", varName, item.ItemResource.Workflow)
} }
return "" return ""
} }
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -160,7 +160,7 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
if err == nil && len(resource) > 0 { // if the workspace already exists, update it if err == nil && len(resource) > 0 { // if the workspace already exists, update it
w := &workspace.Workspace{ w := &workspace.Workspace{
Active: active, Active: active,
ResourceSet: resources.ResourceSet{ WorkspaceResourceSet: resources.WorkspaceResourceSet{
Datas: workflow.Datas, Datas: workflow.Datas,
Processings: workflow.Processings, Processings: workflow.Processings,
Storages: workflow.Storages, Storages: workflow.Storages,
@@ -173,7 +173,7 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
a.workspaceAccessor.StoreOne(&workspace.Workspace{ a.workspaceAccessor.StoreOne(&workspace.Workspace{
Active: active, Active: active,
AbstractObject: utils.AbstractObject{Name: workflow.Name + "_workspace"}, AbstractObject: utils.AbstractObject{Name: workflow.Name + "_workspace"},
ResourceSet: resources.ResourceSet{ WorkspaceResourceSet: resources.WorkspaceResourceSet{
Datas: workflow.Datas, Datas: workflow.Datas,
Processings: workflow.Processings, Processings: workflow.Processings,
Storages: workflow.Storages, Storages: workflow.Storages,
+5 -5
View File
@@ -25,7 +25,7 @@ const (
// - State : current lifecycle state of the step // - State : current lifecycle state of the step
// - Deps : itemIDs that must reach StepSuccess before this step can start // - Deps : itemIDs that must reach StepSuccess before this step can start
// - WhenRunning : itemIDs (resources) that become active while this step is running // - WhenRunning : itemIDs (resources) that become active while this step is running
// (e.g. the compute node executing it, the storage it reads/writes) // (e.g. the compute node executing it, the storage it reads/writes)
type ExecutionGraphItem struct { type ExecutionGraphItem struct {
Name string `json:"name" bson:"name"` Name string `json:"name" bson:"name"`
StartDate *time.Time `json:"start_date,omitempty" bson:"start_date,omitempty"` StartDate *time.Time `json:"start_date,omitempty" bson:"start_date,omitempty"`
@@ -69,10 +69,10 @@ func BuildExecutionGraph(g *workflowgraph.Graph) ExecutionGraph {
// Steps (logical nodes that sequence execution): Data, Processing, Workflow, NativeTool. // Steps (logical nodes that sequence execution): Data, Processing, Workflow, NativeTool.
// Resources (infrastructure co-active while a step runs): Compute, Storage. // Resources (infrastructure co-active while a step runs): Compute, Storage.
srcIsStep := srcItem.Data != nil || srcItem.Processing != nil || srcItem.Workflow != nil || srcItem.NativeTool != nil srcIsStep := srcItem.ItemResource.Data != nil || srcItem.ItemResource.Processing != nil || srcItem.ItemResource.Workflow != nil || srcItem.ItemResource.NativeTool != nil
dstIsStep := dstItem.Data != nil || dstItem.Processing != nil || dstItem.Workflow != nil || dstItem.NativeTool != nil dstIsStep := dstItem.ItemResource.Data != nil || dstItem.ItemResource.Processing != nil || dstItem.ItemResource.Workflow != nil || dstItem.ItemResource.NativeTool != nil
srcIsResource := srcItem.Compute != nil || srcItem.Storage != nil srcIsResource := srcItem.ItemResource.Compute != nil || srcItem.ItemResource.Storage != nil
dstIsResource := dstItem.Compute != nil || dstItem.Storage != nil dstIsResource := dstItem.ItemResource.Compute != nil || dstItem.ItemResource.Storage != nil
switch { switch {
case srcIsStep && dstIsStep: case srcIsStep && dstIsStep:
@@ -48,6 +48,10 @@ type WorkflowExecution struct {
BookingsState map[string]BookingState `json:"bookings_state" bson:"bookings_state,omitempty"` // booking_id → reservation+completion status BookingsState map[string]BookingState `json:"bookings_state" bson:"bookings_state,omitempty"` // booking_id → reservation+completion status
PurchasesState map[string]bool `json:"purchases_state" bson:"purchases_state,omitempty"` // purchase_id → confirmed PurchasesState map[string]bool `json:"purchases_state" bson:"purchases_state,omitempty"` // purchase_id → confirmed
// ResourceConsents records which consent strings the user acknowledged per resource
// (resource_id → list of acknowledged ConsentString values) at scheduling time.
ResourceConsents map[string][]string `json:"resource_consents,omitempty" bson:"resource_consents,omitempty"`
// Graph is a lightweight, real-time summary of the workflow execution graph. // Graph is a lightweight, real-time summary of the workflow execution graph.
// Keyed by workflow graph item ID; updated by oc-scheduler on each step-done event. // Keyed by workflow graph item ID; updated by oc-scheduler on each step-done event.
// Consumed by oc-front to render the live execution panel via websocket updates. // Consumed by oc-front to render the live execution panel via websocket updates.
@@ -57,6 +61,12 @@ type WorkflowExecution struct {
SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"` SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
SelectedBuyings workflow.ConfigItem `json:"selected_buyings"` SelectedBuyings workflow.ConfigItem `json:"selected_buyings"`
SelectedStrategies workflow.ConfigItem `json:"selected_strategies"` SelectedStrategies workflow.ConfigItem `json:"selected_strategies"`
SelectedPaymentMode workflow.ConfigItem `json:"selected_payment_mode"`
// SelectedBillingStrategy est la fréquence de facturation globale choisie par l'utilisateur
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
// Propagée depuis WorkflowSchedule.SelectedBillingStrategy par GenerateExecutions().
SelectedBillingStrategy pricing.BillingStrategy `json:"selected_billing_strategy" bson:"selected_billing_strategy"`
// SelectedEmbeddedStorages records which storage capability was activated on // SelectedEmbeddedStorages records which storage capability was activated on
// each compute unit graph node (key = compute graph node ID). // each compute unit graph node (key = compute graph node ID).
@@ -91,7 +101,7 @@ func (r *WorkflowExecution) CanUpdate(set utils.DBObject) (bool, utils.DBObject)
} }
func (r *WorkflowExecution) CanDelete() bool { func (r *WorkflowExecution) CanDelete() bool {
return r.IsDraft // only draft bookings can be deleted return true // only draft bookings can be deleted
} }
func (wfa *WorkflowExecution) Equals(we *WorkflowExecution) bool { func (wfa *WorkflowExecution) Equals(we *WorkflowExecution) bool {
@@ -275,6 +285,16 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
if len(executionsID) > 8 { if len(executionsID) > 8 {
name += " " + executionsID[:8] name += " " + executionsID[:8]
} }
// Résout le mode de paiement spécifique à cette ressource depuis SelectedPaymentMode.
// SelectedPaymentMode est un ConfigItem (map[string]int) dont les clés sont les IDs
// de nœuds du graph ; on tente itemID puis l'ID de la ressource comme fallback.
paymentType := pricing.PAY_ONCE
if v, ok := d.SelectedPaymentMode[itemID]; ok {
paymentType = pricing.PaymentType(v)
} else if v, ok := d.SelectedPaymentMode[priced.GetID()]; ok {
paymentType = pricing.PaymentType(v)
}
bookingItem := &booking.Booking{ bookingItem := &booking.Booking{
AbstractObject: utils.AbstractObject{ AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(), UUID: uuid.New().String(),
@@ -293,6 +313,8 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
ExecutionID: d.GetID(), ExecutionID: d.GetID(),
ExpectedStartDate: start, ExpectedStartDate: start,
ExpectedEndDate: endDate, ExpectedEndDate: endDate,
BillingStrategy: d.SelectedBillingStrategy,
PaymentType: paymentType,
} }
items = append(items, bookingItem) items = append(items, bookingItem)
d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append( d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append(
+176 -6
View File
@@ -1,23 +1,49 @@
package workspace package workspace
import ( import (
"fmt"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area" "cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/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/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"
) )
// trustedRelations holds peer relations that yield TrustMap = true for a resource.
var trustedRelations = map[peer.PeerRelation]bool{
peer.PARTNER: true,
peer.MASTER: true,
peer.NANO: true,
peer.ORGANIZATION_MASTER: true,
peer.ORGANIZATION_MEMBER: true,
peer.ORGANIZATION_PARTNER: true,
}
// Workspace is a struct that represents a workspace // Workspace is a struct that represents a workspace
type Workspace struct { type Workspace 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)
resources.ResourceSet // ResourceSet contains the resources of the workspace (data, compute, processing, storage, workflow) resources.WorkspaceResourceSet // WorkspaceResourceSet persists both IDs and complete resource objects
IsContextual bool `json:"is_contextual" bson:"is_contextual" default:"false"` // IsContextual is a flag that indicates if the workspace is contextual IsContextual bool `json:"is_contextual" bson:"is_contextual" default:"false"`
Active bool `json:"active" bson:"active" default:"false"` // Active is a flag that indicates if the workspace is active Active bool `json:"active" bson:"active" default:"false"`
Shared string `json:"shared,omitempty" bson:"shared,omitempty"` // Shared is the ID of the shared workspace Shared string `json:"shared,omitempty" bson:"shared,omitempty"`
// Notifications accumulates strings for auto-modifications (e.g. resource removed after peer blacklist).
// Cleared by the owner via the notifications update endpoint.
Notifications []string `json:"notifications,omitempty" bson:"notifications,omitempty"`
// TrustMap maps resource ID → trust bool based on the creator peer's relation.
// Not persisted (bson:"-") — recomputed on every load by ComputeTrustAndClean.
TrustMap map[string]bool `json:"trust_map,omitempty" bson:"-"`
// StaleMap maps resource ID → stale bool. Populated at GET time from the
// verify campaign results stored in oc-workspace's stale cache. Not persisted.
StaleMap map[string]bool `json:"stale_map,omitempty" bson:"-"`
} }
func (d *Workspace) GetAccessor(request *tools.APIRequest) utils.Accessor { func (d *Workspace) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request)
} }
func (ao *Workspace) VerifyAuth(callName string, request *tools.APIRequest) bool { func (ao *Workspace) VerifyAuth(callName string, request *tools.APIRequest) bool {
@@ -30,3 +56,147 @@ func (ao *Workspace) VerifyAuth(callName string, request *tools.APIRequest) bool
} }
return ao.AbstractObject.VerifyAuth(callName, request) return ao.AbstractObject.VerifyAuth(callName, request)
} }
// ComputeTrustAndClean populates TrustMap for all resources embedded in this workspace,
// removes resources whose creator peer is blacklisted, and appends a deletion notification
// for each removal. Returns true when at least one resource was removed (caller should persist).
func (w *Workspace) ComputeTrustAndClean() bool {
w.TrustMap = map[string]bool{}
selfPeer, _ := utils.GetMySelf(peer.NewShallowAccessor())
var selfPeerID string
if selfPeer != nil {
if p, ok := selfPeer.(*peer.Peer); ok {
selfPeerID = p.PeerID
}
}
// Cache peer relations to avoid redundant DB lookups per workspace load.
cache := map[string]peer.PeerRelation{}
relation := func(creatorID string) peer.PeerRelation {
if r, ok := cache[creatorID]; ok {
return r
}
if creatorID == selfPeerID {
cache[creatorID] = peer.SELF
return peer.SELF
}
results, _, _ := peer.NewShallowAccessor().Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"peer_id": {{Operator: dbs.EQUAL.String(), Value: creatorID}},
},
}, "", false, 0, 1)
rel := peer.NONE
if len(results) > 0 {
if p, ok := results[0].(*peer.Peer); ok {
rel = p.Relation
}
}
cache[creatorID] = rel
return rel
}
setTrust := func(id, creatorID string, rel peer.PeerRelation) {
w.TrustMap[id] = (creatorID == selfPeerID) || trustedRelations[rel]
}
changed := false
var keptData []*resources.DataResource
for _, r := range w.DataResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptData = append(keptData, r)
}
}
w.DataResources = keptData
var keptCompute []*resources.ComputeResource
for _, r := range w.ComputeResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptCompute = append(keptCompute, r)
}
}
w.ComputeResources = keptCompute
var keptStorage []*resources.StorageResource
for _, r := range w.StorageResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptStorage = append(keptStorage, r)
}
}
w.StorageResources = keptStorage
var keptProcessing []*resources.ProcessingResource
for _, r := range w.ProcessingResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptProcessing = append(keptProcessing, r)
}
}
w.ProcessingResources = keptProcessing
var keptWorkflow []*resources.WorkflowResource
for _, r := range w.WorkflowResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptWorkflow = append(keptWorkflow, r)
}
}
w.WorkflowResources = keptWorkflow
var keptService []*resources.ServiceResource
for _, r := range w.ServiceResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptService = append(keptService, r)
}
}
w.ServiceResources = keptService
var keptDynamic []*resources.DynamicResource
for _, r := range w.DynamicResources {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptDynamic = append(keptDynamic, r)
}
}
w.DynamicResources = keptDynamic
var keptNative []*resources.NativeTool
for _, r := range w.NativeTools {
if rel := relation(r.GetCreatorID()); rel == peer.BLACKLIST {
w.Notifications = append(w.Notifications, fmt.Sprintf("resource %s (%s) removed: creator peer blacklisted", r.GetName(), r.GetID()))
changed = true
} else {
setTrust(r.GetID(), r.GetCreatorID(), rel)
keptNative = append(keptNative, r)
}
}
w.NativeTools = keptNative
return changed
}
+70 -6
View File
@@ -7,10 +7,60 @@ 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/collaborative_area/shallow_collaborative_area" "cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/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/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"
) )
func init() {
resources.WorkspaceCandidatesProvider = func(dt tools.DataType, request *tools.APIRequest) []resources.ResourceInterface {
res, _, _ := NewAccessor(request).Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"user_creator_id": {{Operator: dbs.EQUAL.String(), Value: request.Username}},
"active": {{Operator: dbs.EQUAL.String(), Value: true}},
},
}, "", false, 0, 1)
if len(res) == 0 {
return []resources.ResourceInterface{}
}
ws, ok := res[0].(*Workspace)
if !ok {
return []resources.ResourceInterface{}
}
// ws.Fill was already called by Search — typed slices are populated.
// Return an empty non-nil slice when the workspace exists but has no
// resources of the requested type: caller must not fall back to catalog.
out := []resources.ResourceInterface{}
switch dt {
case tools.COMPUTE_RESOURCE:
for _, c := range ws.ComputeResources {
out = append(out, c)
}
case tools.DATA_RESOURCE:
for _, c := range ws.DataResources {
out = append(out, c)
}
case tools.STORAGE_RESOURCE:
for _, c := range ws.StorageResources {
out = append(out, c)
}
case tools.PROCESSING_RESOURCE:
for _, c := range ws.ProcessingResources {
out = append(out, c)
}
case tools.WORKFLOW_RESOURCE:
for _, c := range ws.WorkflowResources {
out = append(out, c)
}
case tools.SERVICE_RESOURCE:
for _, c := range ws.ServiceResources {
out = append(out, c)
}
}
return out
}
}
// Workspace is a struct that represents a workspace // Workspace is a struct that represents a workspace
type workspaceMongoAccessor struct { type workspaceMongoAccessor struct {
utils.AbstractAccessor[*Workspace] // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor[*Workspace] // AbstractAccessor contains the basic fields of an accessor (model, caller)
@@ -88,25 +138,39 @@ func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject,
func (a *workspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *workspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
d.(*Workspace).Fill(a.GetRequest()) w := d.(*Workspace)
return d, 200, nil w.Fill(a.GetRequest())
a.applyTrustAndClean(w)
return w, 200, nil
}, a) }, a)
} }
func (a *workspaceMongoAccessor) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) { func (a *workspaceMongoAccessor) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Workspace](func(d utils.DBObject) utils.ShallowDBObject { return utils.GenericLoadAll[*Workspace](func(d utils.DBObject) utils.ShallowDBObject {
d.(*Workspace).Fill(a.GetRequest()) w := d.(*Workspace)
return d w.Fill(a.GetRequest())
a.applyTrustAndClean(w)
return w
}, isDraft, a, offset, limit) }, isDraft, a, offset, limit)
} }
func (a *workspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) { func (a *workspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Workspace](filters, search, (&Workspace{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject { return utils.GenericSearch[*Workspace](filters, search, (&Workspace{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject {
d.(*Workspace).Fill(a.GetRequest()) w := d.(*Workspace)
return d w.Fill(a.GetRequest())
a.applyTrustAndClean(w)
return w
}, isDraft, a, offset, limit) }, isDraft, a, offset, limit)
} }
// applyTrustAndClean calls ComputeTrustAndClean and, when resources were removed due to
// blacklisted peers, persists the cleaned workspace back to the database.
func (a *workspaceMongoAccessor) applyTrustAndClean(w *Workspace) {
if changed := w.ComputeTrustAndClean(); changed {
utils.GenericUpdateOne(w.Serialize(w), w.GetID(), a)
}
}
/* /*
This function is used to share the workspace with the peers This function is used to share the workspace with the peers
*/ */
+71 -2
View File
@@ -36,6 +36,11 @@ const (
SERVICE_RESOURCE SERVICE_RESOURCE
DYNAMIC_RESOURCE DYNAMIC_RESOURCE
LIVE_SERVICE LIVE_SERVICE
PAYMENT
REFUND
DISCOUNT
SUBSCRIPTION
POLICY
) )
var NOAPI = func() string { var NOAPI = func() string {
@@ -96,6 +101,11 @@ var InnerDefaultAPI = [...]func() string{
CATALOGAPI, CATALOGAPI,
CATALOGAPI, CATALOGAPI,
DATACENTERAPI, DATACENTERAPI,
NOAPI,
NOAPI,
NOAPI,
NOAPI,
PEERSAPI,
} }
// Bind the standard data name to the data type // Bind the standard data name to the data type
@@ -126,6 +136,11 @@ var Str = [...]string{
"service_resource", "service_resource",
"dynamic_resource", "dynamic_resource",
"live_service", "live_service",
"payment",
"refund",
"discount",
"subscription",
"policy",
} }
func FromString(comp string) int { func FromString(comp string) int {
@@ -161,7 +176,8 @@ func DataTypeList() []DataType {
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE, return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE,
WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY, WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY,
ORDER, PURCHASE_RESOURCE, ORDER, PURCHASE_RESOURCE,
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL, EXECUTION_VERIFICATION, ALLOWED_IMAGE, SERVICE_RESOURCE, DYNAMIC_RESOURCE, LIVE_SERVICE} LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL, EXECUTION_VERIFICATION, ALLOWED_IMAGE, SERVICE_RESOURCE, DYNAMIC_RESOURCE, LIVE_SERVICE,
PAYMENT, REFUND, DISCOUNT, SUBSCRIPTION, POLICY}
} }
type PropalgationMessage struct { type PropalgationMessage struct {
@@ -191,6 +207,44 @@ const (
// PB_PROPAGATE is used by oc-discovery to broadcast a peer's online/offline // PB_PROPAGATE is used by oc-discovery to broadcast a peer's online/offline
// state to other oc-discovery nodes in the federation via PROPALGATION_EVENT. // state to other oc-discovery nodes in the federation via PROPALGATION_EVENT.
PB_PROPAGATE PB_PROPAGATE
// PB_SOURCE_PRESIGN is sent by oc-datacenter to request a pre-signed Minio URL
// for a private source resource (isReachable=false, Phase 4).
// oc-discovery routes it to the resource owner peer via ProtocolSourcePresignResource.
PB_SOURCE_PRESIGN
// PB_ORG_PARTNER is propagated via PB_PROPAGATE through oc-discovery to the
// organization master's oc-discovery, which notifies its oc-peer via
// ORG_PARTNER_EVENT. The master's oc-peer confirms or rejects by emitting a
// PROPALGATION_EVENT back, which oc-discovery routes to the originating
// oc-discovery, which in turn notifies our oc-peer via ORG_PARTNER_EVENT to
// finalize the relation.
PB_ORG_PARTNER
// PB_WATCH_RESOURCE is emitted by oc-workspace when a non-self resource is
// stored in a workspace. oc-discovery contacts the creator peer to register
// the watching peerID in the creator's watcher cache so it receives future
// CREATE/DELETE events for that resource.
// Payload: { "creator_peer_id": "...", "resource_id": "..." }
PB_WATCH_RESOURCE
// PB_UNWATCH_RESOURCE is emitted by oc-workspace when a non-self resource is
// removed from all workspaces. oc-discovery contacts the creator peer to
// deregister the watching peerID from the creator's watcher cache.
// Payload: { "creator_peer_id": "...", "resource_id": "..." }
PB_UNWATCH_RESOURCE
// PB_BOOKING_SYNC is emitted by master every 24 h to each known NANO.
// Payload: {"peer_id": nano.PeerID, "booking_sync_ids": ["id1", "id2", ...]}
// Nano compares the list against its own confirmed bookings and calls
// SendBookingToMaster for any it has that master is missing.
PB_BOOKING_SYNC
// PB_VERIFY_RESOURCE is emitted by oc-workspace or oc-workflow on workspace
// activation / workflow opening to verify that an embedded non-self resource
// is still current. oc-discovery forwards the request to the creator peer via
// ProtocolVerifyResource; the result comes back as a VERIFY_RESOURCE NATS event.
// Payload: { "creator_peer_id": "…", "data_type": N, "resource_payload": {…} }
PB_VERIFY_RESOURCE
) )
func GetActionString(ss string) PubSubAction { func GetActionString(ss string) PubSubAction {
@@ -223,6 +277,16 @@ func GetActionString(ss string) PubSubAction {
return PB_OBSERVE_CLOSE return PB_OBSERVE_CLOSE
case "propagate": case "propagate":
return PB_PROPAGATE return PB_PROPAGATE
case "source_presign":
return PB_SOURCE_PRESIGN
case "org_partner":
return PB_ORG_PARTNER
case "watch_resource":
return PB_WATCH_RESOURCE
case "unwatch_resource":
return PB_UNWATCH_RESOURCE
case "booking_sync":
return PB_BOOKING_SYNC
default: default:
return NONE return NONE
} }
@@ -245,7 +309,12 @@ var path = []string{
"none", // 12 NONE "none", // 12 NONE
"observe", // 13 PB_OBSERVE "observe", // 13 PB_OBSERVE
"observe_close", // 14 PB_OBSERVE_CLOSE "observe_close", // 14 PB_OBSERVE_CLOSE
"propagate", // 15 PB_PROPAGATE "propagate", // 15 PB_PROPAGATE
"source_presign", // 16 PB_SOURCE_PRESIGN
"org_partner", // 17 PB_ORG_PARTNER
"watch_resource", // 18 PB_WATCH_RESOURCE
"unwatch_resource", // 19 PB_UNWATCH_RESOURCE
"booking_sync", // 20 PB_BOOKING_SYNC
} }
func (m PubSubAction) String() string { func (m PubSubAction) String() string {
+10
View File
@@ -599,6 +599,16 @@ func (k *KubernetesService) CreateSecret(context context.Context, minioId string
return nil return nil
} }
// DeleteSecretsByLabel deletes all Secrets in the given namespace matching labelSelector.
// Used by oc-datacenter to clean up ephemeral source-presigned Secrets after workflow completion.
func (k *KubernetesService) DeleteSecretsByLabel(ctx context.Context, namespace, labelSelector string) error {
return k.Set.CoreV1().Secrets(namespace).DeleteCollection(
ctx,
metav1.DeleteOptions{},
metav1.ListOptions{LabelSelector: labelSelector},
)
}
// CreatePVC creates a static PersistentVolume + PersistentVolumeClaim in the given namespace. // CreatePVC creates a static PersistentVolume + PersistentVolumeClaim in the given namespace.
// Static provisioning (no StorageClass) avoids the WaitForFirstConsumer deadlock // Static provisioning (no StorageClass) avoids the WaitForFirstConsumer deadlock
// with Admiralty virtual nodes — the PVC binds immediately. // with Admiralty virtual nodes — the PVC binds immediately.
+18 -1
View File
@@ -32,6 +32,7 @@ var meths = []string{"remove execution", "create execution", "planner execution"
"considers event", "admiralty config event", "minio config event", "pvc config event", "considers event", "admiralty config event", "minio config event", "pvc config event",
"workflow started event", "workflow step done event", "workflow done event", "workflow started event", "workflow step done event", "workflow done event",
"peer behavior event", "peer observe response event", "peer observe event", "peer behavior event", "peer observe response event", "peer observe event",
"source presign event", "org partner event", "verify resource event",
} }
const ( const (
@@ -78,6 +79,21 @@ const (
// or stop observing a remote peer. Payload contains the target peer_id and a // or stop observing a remote peer. Payload contains the target peer_id and a
// boolean close flag. // boolean close flag.
PEER_OBSERVE_EVENT PEER_OBSERVE_EVENT
// SOURCE_PRESIGN_EVENT is emitted by oc-discovery on the resource-owner peer
// when it receives a PB_SOURCE_PRESIGN request routed via libp2p.
// oc-datacenter listens to it to generate a pre-signed Minio URL and reply
// via PB_CONSIDERS (Phase 4 — isReachable=false).
SOURCE_PRESIGN_EVENT
// ORG_PARTNER_EVENT is emitted by a peer to its OrganizationMaster to ask:
// "is peer X one of your members?". The master replies via the same channel.
ORG_PARTNER_EVENT
// VERIFY_RESOURCE is emitted by oc-discovery when it receives a verify response
// from a remote peer via ProtocolVerifyResource. oc-workspace and oc-workflow
// listen to this event to update their stale caches.
VERIFY_RESOURCE
) )
func (n NATSMethod) String() string { func (n NATSMethod) String() string {
@@ -90,7 +106,8 @@ func NameToMethod(name string) NATSMethod {
CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT, CONFIRM_EVENT, CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT, CONFIRM_EVENT,
CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT, PVC_CONFIG_EVENT, CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT, PVC_CONFIG_EVENT,
WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT, WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT,
PEER_BEHAVIOR_EVENT, PEER_OBSERVE_RESPONSE_EVENT, PEER_OBSERVE_EVENT} { PEER_BEHAVIOR_EVENT, PEER_OBSERVE_RESPONSE_EVENT, PEER_OBSERVE_EVENT,
SOURCE_PRESIGN_EVENT, ORG_PARTNER_EVENT, VERIFY_RESOURCE} {
if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) { if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) {
return v return v
} }