Compare commits
42 Commits
9bb3d897b3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a66b42c01 | |||
| 58e97fbe74 | |||
| 1425a31494 | |||
| 6ee169f444 | |||
| 5be3c0a10a | |||
| d9723e6431 | |||
| c726361deb | |||
| d19ff1f8b2 | |||
| 69244163b4 | |||
| 842364d145 | |||
| 9ab374b720 | |||
| aa2bca48ef | |||
| 322ea38bb4 | |||
| c1490a7746 | |||
| 49f60d9416 | |||
| 548ed84b13 | |||
| 178cd48314 | |||
| b31df8cfed | |||
| a0a53f0477 | |||
| dffaa6326f | |||
| 8155f4b17a | |||
| 51307bb067 | |||
| 6ac788a8ff | |||
| a7d0c1208b | |||
| 3924fca289 | |||
| 797df972ac | |||
| 71ae0d2cfc | |||
| 5806bdd3d2 | |||
| 99fbe82a51 | |||
| 7d8bec9a78 | |||
| afd8a2d97c | |||
| 82a4708f46 | |||
| a3bca24982 | |||
| 41706949fd | |||
| b1429596bb | |||
| ce110ee634 | |||
| 7e5b69b1d2 | |||
| 26948da3c1 | |||
| 4e1b1164cc | |||
| 73b844f664 | |||
| cef23b5f30 | |||
| e6a9558cbf |
+53
-14
@@ -3,6 +3,7 @@ package dbs
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
@@ -160,11 +161,16 @@ type Filter struct {
|
||||
// Keys inside "and"/"or" are json tag names; the function resolves each to its
|
||||
// full dotted BSON path using the target struct. Unknown keys are kept as-is.
|
||||
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Filters {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("Panic recovered FiltersFromFlatMap: %v\n", r)
|
||||
}
|
||||
}()
|
||||
filters := &Filters{
|
||||
And: make(map[string][]Filter),
|
||||
Or: make(map[string][]Filter),
|
||||
}
|
||||
paths := jsonToBsonPaths(reflect.TypeOf(target), "")
|
||||
paths := jsonToBsonPaths(reflect.TypeOf(target), "", "")
|
||||
resolve := func(jsonKey string) string {
|
||||
if p, ok := paths[jsonKey]; ok {
|
||||
return p
|
||||
@@ -179,11 +185,12 @@ func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Fil
|
||||
}
|
||||
for jsonKey, val := range m {
|
||||
bsonKey := resolve(jsonKey)
|
||||
items, ok := val.([]interface{})
|
||||
//items, ok := val.([]interface{})
|
||||
fmt.Println(jsonKey, val, bsonKey)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, item := range items {
|
||||
for _, item := range val.([]interface{}) {
|
||||
entry, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
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
|
||||
// codebase: they are stored as a nested sub-document whose key is the lowercased
|
||||
// struct type name (e.g. utils.AbstractObject → "abstractobject").
|
||||
func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string {
|
||||
// struct type name (e.g. utils.AbstractObject → "abstractobject"). Their JSON
|
||||
// fields are promoted (flat), so bsonPrefix advances but jsonPrefix does not.
|
||||
//
|
||||
// For fields inside slices or maps, both the leaf json name and the full dotted
|
||||
// json path (e.g. "instances.access_protocol") are registered as keys so callers
|
||||
// can use either form unambiguously.
|
||||
func jsonToBsonPaths(t reflect.Type, bsonPrefix string, jsonPrefix string) map[string]string {
|
||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() == reflect.Map {
|
||||
t = t.Elem()
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
}
|
||||
result := make(map[string]string)
|
||||
if t.Kind() != reflect.Struct {
|
||||
return result
|
||||
@@ -232,17 +250,21 @@ func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string {
|
||||
bsonName := strings.Split(bsonTag, ",")[0]
|
||||
|
||||
// Anonymous embedded struct with no tags: use lowercase type name as BSON prefix.
|
||||
// JSON fields are promoted so jsonPrefix stays the same.
|
||||
if field.Anonymous && jsonName == "" && bsonName == "" {
|
||||
ft := field.Type
|
||||
for ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
embedPrefix := strings.ToLower(ft.Name())
|
||||
if prefix != "" {
|
||||
embedPrefix = prefix + "." + embedPrefix
|
||||
embedBsonPrefix := strings.ToLower(ft.Name())
|
||||
re := regexp.MustCompile(`\[[^\]]*\]`)
|
||||
embedBsonPrefix = re.ReplaceAllString(embedBsonPrefix, "")
|
||||
embedBsonPrefix = strings.ReplaceAll(embedBsonPrefix, "*", "")
|
||||
if bsonPrefix != "" {
|
||||
embedBsonPrefix = bsonPrefix + "." + embedBsonPrefix
|
||||
}
|
||||
for k, v := range jsonToBsonPaths(ft, embedPrefix) {
|
||||
for k, v := range jsonToBsonPaths(ft, embedBsonPrefix, jsonPrefix) {
|
||||
if _, exists := result[k]; !exists {
|
||||
result[k] = v
|
||||
}
|
||||
@@ -258,19 +280,36 @@ func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string {
|
||||
bsonName = jsonName
|
||||
}
|
||||
|
||||
fullPath := bsonName
|
||||
if prefix != "" {
|
||||
fullPath = prefix + "." + bsonName
|
||||
fullBsonPath := bsonName
|
||||
if bsonPrefix != "" {
|
||||
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
|
||||
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Map {
|
||||
ft = ft.Elem()
|
||||
for ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
for k, v := range jsonToBsonPaths(ft, fullPath) {
|
||||
for k, v := range jsonToBsonPaths(ft, fullBsonPath, fullJsonPath) {
|
||||
if _, exists := result[k]; !exists {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ import (
|
||||
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
|
||||
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
|
||||
"cloud.o-forge.io/core/oc-lib/models/booking"
|
||||
"cloud.o-forge.io/core/oc-lib/models/collaborative_area"
|
||||
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
|
||||
@@ -67,6 +72,12 @@ const (
|
||||
ALLOWED_IMAGE = tools.ALLOWED_IMAGE
|
||||
SERVICE_RESOURCE = tools.SERVICE_RESOURCE
|
||||
LIVE_SERVICE = tools.LIVE_SERVICE
|
||||
BILL = tools.BILL
|
||||
PAYMENT = tools.PAYMENT
|
||||
REFUND = tools.REFUND
|
||||
DISCOUNT = tools.DISCOUNT
|
||||
SUBSCRIPTION = tools.SUBSCRIPTION
|
||||
POLICY = tools.POLICY
|
||||
)
|
||||
|
||||
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
|
||||
@@ -727,6 +738,41 @@ func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToBill() *billing.Bill {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.BILL {
|
||||
return l.Data.(*billing.Bill)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToPayment() *payment.Payment {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.PAYMENT {
|
||||
return l.Data.(*payment.Payment)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToRefund() *refund.Refund {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.REFUND {
|
||||
return l.Data.(*refund.Refund)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToDiscount() *discount.Discount {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.DISCOUNT {
|
||||
return l.Data.(*discount.Discount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LibData) ToSubscription() *subscription.Subscription {
|
||||
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.SUBSCRIPTION {
|
||||
return l.Data.(*subscription.Subscription)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ------------- Loading resources ----------GetAccessor
|
||||
|
||||
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
|
||||
|
||||
@@ -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
|
||||
@@ -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 (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
@@ -7,14 +7,13 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
return &billMongoAccessor{
|
||||
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,
|
||||
Type: tools.BILL,
|
||||
New: func() *Bill { return &Bill{} },
|
||||
@@ -0,0 +1,146 @@
|
||||
package discount
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type DiscountType int
|
||||
|
||||
const (
|
||||
PERCENTAGE DiscountType = iota // réduction en pourcentage
|
||||
FIXED_AMOUNT // réduction montant fixe
|
||||
)
|
||||
|
||||
func (d DiscountType) String() string {
|
||||
return [...]string{"percentage", "fixed_amount"}[d]
|
||||
}
|
||||
|
||||
func DiscountTypeList() []DiscountType {
|
||||
return []DiscountType{PERCENTAGE, FIXED_AMOUNT}
|
||||
}
|
||||
|
||||
type DiscountScope int
|
||||
|
||||
const (
|
||||
SCOPE_ALL DiscountScope = iota // applicable à tout
|
||||
SCOPE_RESOURCE_TYPE // applicable à un type de ressource
|
||||
SCOPE_RESOURCE // applicable à une ressource spécifique
|
||||
SCOPE_SUBSCRIPTION // applicable aux souscriptions
|
||||
)
|
||||
|
||||
func (d DiscountScope) String() string {
|
||||
return [...]string{"all", "resource_type", "resource", "subscription"}[d]
|
||||
}
|
||||
|
||||
// Discount représente une réduction applicable sur les ressources ou abonnements.
|
||||
type Discount struct {
|
||||
utils.AbstractObject
|
||||
Code string `json:"code,omitempty" bson:"code,omitempty"`
|
||||
DiscountType DiscountType `json:"discount_type" bson:"discount_type"`
|
||||
Scope DiscountScope `json:"scope" bson:"scope"`
|
||||
Value float64 `json:"value" bson:"value"` // pourcentage (0-100) ou montant fixe
|
||||
Currency string `json:"currency,omitempty" bson:"currency,omitempty"` // pour FIXED_AMOUNT
|
||||
ResourceTypes []tools.DataType `json:"resource_types,omitempty" bson:"resource_types,omitempty"` // si SCOPE_RESOURCE_TYPE
|
||||
ResourceIDs []string `json:"resource_ids,omitempty" bson:"resource_ids,omitempty"` // si SCOPE_RESOURCE
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
|
||||
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
|
||||
MaxUsage int `json:"max_usage,omitempty" bson:"max_usage,omitempty"` // 0 = illimité
|
||||
CurrentUsage int `json:"current_usage" bson:"current_usage"`
|
||||
MinAmount float64 `json:"min_amount,omitempty" bson:"min_amount,omitempty"` // montant minimum du bill pour appliquer
|
||||
Active bool `json:"active" bson:"active" default:"true"`
|
||||
}
|
||||
|
||||
// IsValid vérifie si la réduction est applicable au moment présent.
|
||||
func (d *Discount) IsValid(billAmount float64) bool {
|
||||
now := time.Now().UTC()
|
||||
if !d.Active {
|
||||
return false
|
||||
}
|
||||
if d.MaxUsage > 0 && d.CurrentUsage >= d.MaxUsage {
|
||||
return false
|
||||
}
|
||||
if d.ValidFrom != nil && now.Before(*d.ValidFrom) {
|
||||
return false
|
||||
}
|
||||
if d.ValidUntil != nil && now.After(*d.ValidUntil) {
|
||||
return false
|
||||
}
|
||||
if d.MinAmount > 0 && billAmount < d.MinAmount {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Apply applique la réduction sur un prix HT et retourne le prix réduit.
|
||||
func (d *Discount) Apply(priceHT float64) float64 {
|
||||
switch d.DiscountType {
|
||||
case PERCENTAGE:
|
||||
return priceHT - (priceHT * d.Value / 100)
|
||||
case FIXED_AMOUNT:
|
||||
result := priceHT - d.Value
|
||||
if result < 0 {
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
return priceHT
|
||||
}
|
||||
|
||||
// DiscountAmount retourne le montant de la réduction sans l'appliquer.
|
||||
func (d *Discount) DiscountAmount(priceHT float64) float64 {
|
||||
switch d.DiscountType {
|
||||
case PERCENTAGE:
|
||||
return priceHT * d.Value / 100
|
||||
case FIXED_AMOUNT:
|
||||
if d.Value > priceHT {
|
||||
return priceHT
|
||||
}
|
||||
return d.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AppliesToResource vérifie si cette réduction s'applique à une ressource donnée.
|
||||
func (d *Discount) AppliesToResource(resourceID string, resourceType tools.DataType) bool {
|
||||
switch d.Scope {
|
||||
case SCOPE_ALL:
|
||||
return true
|
||||
case SCOPE_RESOURCE:
|
||||
for _, id := range d.ResourceIDs {
|
||||
if id == resourceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case SCOPE_RESOURCE_TYPE:
|
||||
for _, t := range d.ResourceTypes {
|
||||
if t == resourceType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IncrementUsage incrémente le compteur d'utilisation.
|
||||
func (d *Discount) IncrementUsage() {
|
||||
d.CurrentUsage++
|
||||
}
|
||||
|
||||
func (d *Discount) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (d *Discount) StoreDraftDefault() {
|
||||
d.IsDraft = true
|
||||
}
|
||||
|
||||
func (d *Discount) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
return d.IsDraft, set
|
||||
}
|
||||
|
||||
func (d *Discount) CanDelete() bool {
|
||||
return d.IsDraft || d.CurrentUsage == 0
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package discount
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type discountMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Discount]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *discountMongoAccessor {
|
||||
return &discountMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Discount]{
|
||||
Logger: logs.CreateLogger(tools.DISCOUNT.String()),
|
||||
Request: request,
|
||||
Type: tools.DISCOUNT,
|
||||
New: func() *Discount { return &Discount{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type PaymentStatus int
|
||||
|
||||
const (
|
||||
PAYMENT_PENDING PaymentStatus = iota
|
||||
PAYMENT_PROCESSING // en cours de traitement blockchain/réseau
|
||||
PAYMENT_COMPLETED // confirmé
|
||||
PAYMENT_FAILED // échoué
|
||||
PAYMENT_CANCELLED // annulé avant exécution
|
||||
PAYMENT_REFUNDED // remboursé
|
||||
)
|
||||
|
||||
func (s PaymentStatus) String() string {
|
||||
return [...]string{"pending", "processing", "completed", "failed", "cancelled", "refunded"}[s]
|
||||
}
|
||||
|
||||
func PaymentStatusList() []PaymentStatus {
|
||||
return []PaymentStatus{PAYMENT_PENDING, PAYMENT_PROCESSING, PAYMENT_COMPLETED, PAYMENT_FAILED, PAYMENT_CANCELLED, PAYMENT_REFUNDED}
|
||||
}
|
||||
|
||||
type PaymentMethod int
|
||||
|
||||
const (
|
||||
METHOD_BLOCKCHAIN PaymentMethod = iota
|
||||
METHOD_CREDIT_CARD
|
||||
METHOD_BANK_TRANSFER
|
||||
METHOD_CRYPTO
|
||||
METHOD_INTERNAL_CREDIT // crédit interne à la plateforme
|
||||
)
|
||||
|
||||
func (m PaymentMethod) String() string {
|
||||
return [...]string{"blockchain", "credit_card", "bank_transfer", "crypto", "internal_credit"}[m]
|
||||
}
|
||||
|
||||
func PaymentMethodList() []PaymentMethod {
|
||||
return []PaymentMethod{METHOD_BLOCKCHAIN, METHOD_CREDIT_CARD, METHOD_BANK_TRANSFER, METHOD_CRYPTO, METHOD_INTERNAL_CREDIT}
|
||||
}
|
||||
|
||||
// Payment représente une transaction de paiement — instantanée, mensuelle ou annuelle.
|
||||
type Payment struct {
|
||||
utils.AbstractObject
|
||||
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
|
||||
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
|
||||
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
|
||||
PayerPeerID string `json:"payer_peer_id,omitempty" bson:"payer_peer_id,omitempty"`
|
||||
RecipientPeerID string `json:"recipient_peer_id,omitempty" bson:"recipient_peer_id,omitempty"`
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
Status PaymentStatus `json:"status" bson:"status"`
|
||||
Method PaymentMethod `json:"method" bson:"method"`
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"` // PAY_ONCE, PAY_EVERY_MONTH, PAY_EVERY_YEAR
|
||||
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
|
||||
WalletFrom string `json:"wallet_from,omitempty" bson:"wallet_from,omitempty"`
|
||||
WalletTo string `json:"wallet_to,omitempty" bson:"wallet_to,omitempty"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at,omitempty" bson:"scheduled_at,omitempty"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
FailureReason string `json:"failure_reason,omitempty" bson:"failure_reason,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty" bson:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// NewInstantPayment crée un paiement immédiat (PAY_ONCE).
|
||||
func NewInstantPayment(billID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod) *Payment {
|
||||
return &Payment{
|
||||
BillID: billID,
|
||||
PayerPeerID: payerPeerID,
|
||||
RecipientPeerID: recipientPeerID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
Status: PAYMENT_PENDING,
|
||||
Method: method,
|
||||
PaymentType: pricing.PAY_ONCE,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScheduledPayment crée un paiement programmé (mensuel ou annuel).
|
||||
func NewScheduledPayment(subscriptionID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod, paymentType pricing.PaymentType, scheduledAt time.Time) *Payment {
|
||||
return &Payment{
|
||||
SubscriptionID: subscriptionID,
|
||||
PayerPeerID: payerPeerID,
|
||||
RecipientPeerID: recipientPeerID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
Status: PAYMENT_PENDING,
|
||||
Method: method,
|
||||
PaymentType: paymentType,
|
||||
ScheduledAt: &scheduledAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Complete marque le paiement comme confirmé.
|
||||
func (p *Payment) Complete(transactionID string) {
|
||||
now := time.Now().UTC()
|
||||
p.Status = PAYMENT_COMPLETED
|
||||
p.TransactionID = transactionID
|
||||
p.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Fail marque le paiement comme échoué.
|
||||
func (p *Payment) Fail(reason string) {
|
||||
now := time.Now().UTC()
|
||||
p.Status = PAYMENT_FAILED
|
||||
p.FailureReason = reason
|
||||
p.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Cancel annule le paiement s'il est encore en attente.
|
||||
func (p *Payment) Cancel() bool {
|
||||
if p.Status != PAYMENT_PENDING {
|
||||
return false
|
||||
}
|
||||
p.Status = PAYMENT_CANCELLED
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRefundable indique si le paiement peut faire l'objet d'un remboursement.
|
||||
func (p *Payment) IsRefundable() bool {
|
||||
return p.Status == PAYMENT_COMPLETED
|
||||
}
|
||||
|
||||
func (p *Payment) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (p *Payment) StoreDraftDefault() {
|
||||
p.IsDraft = true
|
||||
}
|
||||
|
||||
func (p *Payment) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Payment)
|
||||
if !p.IsDraft && p.Status != incoming.Status {
|
||||
return true, &Payment{
|
||||
Status: incoming.Status,
|
||||
TransactionID: incoming.TransactionID,
|
||||
FailureReason: incoming.FailureReason,
|
||||
}
|
||||
}
|
||||
return p.IsDraft, set
|
||||
}
|
||||
|
||||
func (p *Payment) CanDelete() bool {
|
||||
return p.IsDraft || p.Status == PAYMENT_PENDING
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type paymentMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Payment]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *paymentMongoAccessor {
|
||||
return &paymentMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Payment]{
|
||||
Logger: logs.CreateLogger(tools.PAYMENT.String()),
|
||||
Request: request,
|
||||
Type: tools.PAYMENT,
|
||||
New: func() *Payment { return &Payment{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
)
|
||||
|
||||
type ScheduleStatus int
|
||||
|
||||
const (
|
||||
SCHEDULE_ACTIVE ScheduleStatus = iota
|
||||
SCHEDULE_PAUSED // mis en pause manuellement
|
||||
SCHEDULE_CANCELLED // résilié
|
||||
SCHEDULE_COMPLETED // terminé normalement (abonnement expiré)
|
||||
SCHEDULE_FAILED // trop d'échecs consécutifs
|
||||
)
|
||||
|
||||
func (s ScheduleStatus) String() string {
|
||||
return [...]string{"active", "paused", "cancelled", "completed", "failed"}[s]
|
||||
}
|
||||
|
||||
// PaymentSchedule pilote la récurrence des paiements d'un abonnement.
|
||||
type PaymentSchedule struct {
|
||||
SubscriptionID string `json:"subscription_id" bson:"subscription_id"`
|
||||
Frequency pricing.PaymentType `json:"frequency" bson:"frequency"` // PAY_EVERY_WEEK / PAY_EVERY_MONTH / PAY_EVERY_YEAR
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency"`
|
||||
Status ScheduleStatus `json:"status" bson:"status"`
|
||||
NextPaymentDate time.Time `json:"next_payment_date" bson:"next_payment_date"`
|
||||
LastExecutedAt *time.Time `json:"last_executed_at,omitempty" bson:"last_executed_at,omitempty"`
|
||||
FailureCount int `json:"failure_count" bson:"failure_count"`
|
||||
MaxRetries int `json:"max_retries" bson:"max_retries" default:"3"`
|
||||
}
|
||||
|
||||
// nextDate calcule la prochaine date selon la fréquence.
|
||||
func (ps *PaymentSchedule) nextDate() time.Time {
|
||||
switch ps.Frequency {
|
||||
case pricing.PAY_EVERY_WEEK:
|
||||
return ps.NextPaymentDate.AddDate(0, 0, 7)
|
||||
case pricing.PAY_EVERY_MONTH:
|
||||
return ps.NextPaymentDate.AddDate(0, 1, 0)
|
||||
case pricing.PAY_EVERY_YEAR:
|
||||
return ps.NextPaymentDate.AddDate(1, 0, 0)
|
||||
}
|
||||
return ps.NextPaymentDate
|
||||
}
|
||||
|
||||
// Advance enregistre l'exécution réussie et avance à la prochaine échéance.
|
||||
func (ps *PaymentSchedule) Advance() {
|
||||
now := time.Now().UTC()
|
||||
ps.LastExecutedAt = &now
|
||||
ps.FailureCount = 0
|
||||
ps.NextPaymentDate = ps.nextDate()
|
||||
}
|
||||
|
||||
// RecordFailure incrémente le compteur d'échecs et désactive après MaxRetries.
|
||||
func (ps *PaymentSchedule) RecordFailure() {
|
||||
ps.FailureCount++
|
||||
if ps.MaxRetries > 0 && ps.FailureCount >= ps.MaxRetries {
|
||||
ps.Status = SCHEDULE_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
// IsDue retourne vrai si le paiement est dû maintenant.
|
||||
func (ps *PaymentSchedule) IsDue() bool {
|
||||
return ps.Status == SCHEDULE_ACTIVE && !time.Now().UTC().Before(ps.NextPaymentDate)
|
||||
}
|
||||
|
||||
// Pause suspend temporairement le calendrier.
|
||||
func (ps *PaymentSchedule) Pause() {
|
||||
if ps.Status == SCHEDULE_ACTIVE {
|
||||
ps.Status = SCHEDULE_PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
// Resume réactive un calendrier mis en pause.
|
||||
func (ps *PaymentSchedule) Resume() {
|
||||
if ps.Status == SCHEDULE_PAUSED {
|
||||
ps.Status = SCHEDULE_ACTIVE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package refund
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type RefundStatus int
|
||||
|
||||
const (
|
||||
REFUND_PENDING RefundStatus = iota
|
||||
REFUND_APPROVED // approuvé, en attente de traitement
|
||||
REFUND_REJECTED // rejeté
|
||||
REFUND_PROCESSING // en cours de virement/blockchain
|
||||
REFUND_COMPLETED // remboursé
|
||||
REFUND_CANCELLED // annulé avant approbation
|
||||
)
|
||||
|
||||
func (s RefundStatus) String() string {
|
||||
return [...]string{"pending", "approved", "rejected", "processing", "completed", "cancelled"}[s]
|
||||
}
|
||||
|
||||
func RefundStatusList() []RefundStatus {
|
||||
return []RefundStatus{REFUND_PENDING, REFUND_APPROVED, REFUND_REJECTED, REFUND_PROCESSING, REFUND_COMPLETED, REFUND_CANCELLED}
|
||||
}
|
||||
|
||||
// Refund représente une demande de remboursement sur un paiement validé.
|
||||
type Refund struct {
|
||||
utils.AbstractObject
|
||||
PaymentID string `json:"payment_id" bson:"payment_id" validate:"required"`
|
||||
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
|
||||
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
|
||||
RefundType pricing.RefundType `json:"refund_type" bson:"refund_type"`
|
||||
Amount float64 `json:"amount" bson:"amount" validate:"required"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
Reason string `json:"reason,omitempty" bson:"reason,omitempty"`
|
||||
Status RefundStatus `json:"status" bson:"status"`
|
||||
RequestedAt time.Time `json:"requested_at" bson:"requested_at"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
|
||||
ProcessedByID string `json:"processed_by_id,omitempty" bson:"processed_by_id,omitempty"`
|
||||
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
|
||||
Notes string `json:"notes,omitempty" bson:"notes,omitempty"`
|
||||
// ratio appliqué sur le montant original (0-100). 0 = non renseigné (remboursement total).
|
||||
RefundRatio float64 `json:"refund_ratio,omitempty" bson:"refund_ratio,omitempty"`
|
||||
}
|
||||
|
||||
// NewRefund crée une demande de remboursement total.
|
||||
func NewRefund(paymentID, billID string, amount float64, currency string, refundType pricing.RefundType, reason string) *Refund {
|
||||
return &Refund{
|
||||
PaymentID: paymentID,
|
||||
BillID: billID,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
RefundType: refundType,
|
||||
Reason: reason,
|
||||
Status: REFUND_PENDING,
|
||||
RequestedAt: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewPartialRefund crée une demande de remboursement partiel selon un ratio pourcentage.
|
||||
func NewPartialRefund(paymentID, billID string, originalAmount, ratioPercent float64, currency string, refundType pricing.RefundType, reason string) *Refund {
|
||||
amount := originalAmount * ratioPercent / 100
|
||||
r := NewRefund(paymentID, billID, amount, currency, refundType, reason)
|
||||
r.RefundRatio = ratioPercent
|
||||
return r
|
||||
}
|
||||
|
||||
// Approve approuve la demande de remboursement.
|
||||
func (r *Refund) Approve(processedByID string) {
|
||||
r.Status = REFUND_APPROVED
|
||||
r.ProcessedByID = processedByID
|
||||
}
|
||||
|
||||
// Reject rejette la demande de remboursement.
|
||||
func (r *Refund) Reject(processedByID, notes string) {
|
||||
now := time.Now().UTC()
|
||||
r.Status = REFUND_REJECTED
|
||||
r.ProcessedByID = processedByID
|
||||
r.ProcessedAt = &now
|
||||
r.Notes = notes
|
||||
}
|
||||
|
||||
// Process passe le remboursement en cours de traitement.
|
||||
func (r *Refund) Process() bool {
|
||||
if r.Status != REFUND_APPROVED {
|
||||
return false
|
||||
}
|
||||
r.Status = REFUND_PROCESSING
|
||||
return true
|
||||
}
|
||||
|
||||
// Complete finalise le remboursement avec l'identifiant de transaction.
|
||||
func (r *Refund) Complete(transactionID string) {
|
||||
now := time.Now().UTC()
|
||||
r.Status = REFUND_COMPLETED
|
||||
r.TransactionID = transactionID
|
||||
r.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// Cancel annule la demande si elle est encore en attente.
|
||||
func (r *Refund) Cancel() bool {
|
||||
if r.Status != REFUND_PENDING {
|
||||
return false
|
||||
}
|
||||
r.Status = REFUND_CANCELLED
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Refund) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (r *Refund) StoreDraftDefault() {
|
||||
r.IsDraft = true
|
||||
}
|
||||
|
||||
func (r *Refund) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Refund)
|
||||
if !r.IsDraft && r.Status != incoming.Status {
|
||||
return true, &Refund{
|
||||
Status: incoming.Status,
|
||||
TransactionID: incoming.TransactionID,
|
||||
Notes: incoming.Notes,
|
||||
ProcessedByID: incoming.ProcessedByID,
|
||||
}
|
||||
}
|
||||
return r.IsDraft, set
|
||||
}
|
||||
|
||||
func (r *Refund) CanDelete() bool {
|
||||
return r.IsDraft || r.Status == REFUND_PENDING
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package refund
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type refundMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Refund]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *refundMongoAccessor {
|
||||
return &refundMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Refund]{
|
||||
Logger: logs.CreateLogger(tools.REFUND.String()),
|
||||
Request: request,
|
||||
Type: tools.REFUND,
|
||||
New: func() *Refund { return &Refund{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type SubscriptionStatus int
|
||||
|
||||
const (
|
||||
SUBSCRIPTION_PENDING SubscriptionStatus = iota // en attente de premier paiement
|
||||
SUBSCRIPTION_TRIAL // période d'essai
|
||||
SUBSCRIPTION_ACTIVE // actif
|
||||
SUBSCRIPTION_PAUSED // suspendu temporairement
|
||||
SUBSCRIPTION_CANCELLED // résilié par l'utilisateur
|
||||
SUBSCRIPTION_EXPIRED // date de fin dépassée
|
||||
)
|
||||
|
||||
func (s SubscriptionStatus) String() string {
|
||||
return [...]string{"pending", "trial", "active", "paused", "cancelled", "expired"}[s]
|
||||
}
|
||||
|
||||
func SubscriptionStatusList() []SubscriptionStatus {
|
||||
return []SubscriptionStatus{SUBSCRIPTION_PENDING, SUBSCRIPTION_TRIAL, SUBSCRIPTION_ACTIVE, SUBSCRIPTION_PAUSED, SUBSCRIPTION_CANCELLED, SUBSCRIPTION_EXPIRED}
|
||||
}
|
||||
|
||||
// SubscriptionItem représente un élément d'un abonnement (ressource louée).
|
||||
type SubscriptionItem struct {
|
||||
ResourceID string `json:"resource_id" bson:"resource_id"`
|
||||
ResourceType tools.DataType `json:"resource_type" bson:"resource_type"`
|
||||
Quantity int `json:"quantity" bson:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price" bson:"unit_price"`
|
||||
}
|
||||
|
||||
// Subscription représente un abonnement mensuel ou annuel à des ressources.
|
||||
type Subscription struct {
|
||||
utils.AbstractObject
|
||||
SubscriberPeerID string `json:"subscriber_peer_id" bson:"subscriber_peer_id" validate:"required"`
|
||||
ProviderPeerID string `json:"provider_peer_id,omitempty" bson:"provider_peer_id,omitempty"`
|
||||
Status SubscriptionStatus `json:"status" bson:"status"`
|
||||
PlanType pricing.PaymentType `json:"plan_type" bson:"plan_type"` // PAY_EVERY_MONTH ou PAY_EVERY_YEAR
|
||||
Items []*SubscriptionItem `json:"items,omitempty" bson:"items,omitempty"`
|
||||
Amount float64 `json:"amount" bson:"amount"`
|
||||
Currency string `json:"currency" bson:"currency" default:"EUR"`
|
||||
StartDate time.Time `json:"start_date" bson:"start_date"`
|
||||
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"`
|
||||
NextBillingDate time.Time `json:"next_billing_date" bson:"next_billing_date"`
|
||||
AutoRenew bool `json:"auto_renew" bson:"auto_renew" default:"true"`
|
||||
TrialEndDate *time.Time `json:"trial_end_date,omitempty" bson:"trial_end_date,omitempty"`
|
||||
DiscountIDs []string `json:"discount_ids,omitempty" bson:"discount_ids,omitempty"`
|
||||
CancelledAt *time.Time `json:"cancelled_at,omitempty" bson:"cancelled_at,omitempty"`
|
||||
CancelReason string `json:"cancel_reason,omitempty" bson:"cancel_reason,omitempty"`
|
||||
}
|
||||
|
||||
// newSubscription est le constructeur interne commun.
|
||||
func newSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, plan pricing.PaymentType, nextBilling time.Time) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return &Subscription{
|
||||
SubscriberPeerID: subscriberPeerID,
|
||||
ProviderPeerID: providerPeerID,
|
||||
Status: SUBSCRIPTION_PENDING,
|
||||
PlanType: plan,
|
||||
Items: items,
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
StartDate: now,
|
||||
NextBillingDate: nextBilling,
|
||||
AutoRenew: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWeeklySubscription crée un abonnement hebdomadaire.
|
||||
func NewWeeklySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_WEEK, now.AddDate(0, 0, 7))
|
||||
}
|
||||
|
||||
// NewMonthlySubscription crée un abonnement mensuel.
|
||||
func NewMonthlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_MONTH, now.AddDate(0, 1, 0))
|
||||
}
|
||||
|
||||
// NewYearlySubscription crée un abonnement annuel.
|
||||
func NewYearlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_YEAR, now.AddDate(1, 0, 0))
|
||||
}
|
||||
|
||||
// NewTrialSubscription crée un abonnement mensuel avec période d'essai.
|
||||
func NewTrialSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, trialDays int) *Subscription {
|
||||
now := time.Now().UTC()
|
||||
trialEnd := now.AddDate(0, 0, trialDays)
|
||||
s := NewMonthlySubscription(subscriberPeerID, providerPeerID, items, amount, currency)
|
||||
s.Status = SUBSCRIPTION_TRIAL
|
||||
s.TrialEndDate = &trialEnd
|
||||
s.NextBillingDate = trialEnd
|
||||
return s
|
||||
}
|
||||
|
||||
// Activate passe l'abonnement au statut actif (après premier paiement).
|
||||
func (s *Subscription) Activate() {
|
||||
s.Status = SUBSCRIPTION_ACTIVE
|
||||
}
|
||||
|
||||
// Pause suspend l'abonnement.
|
||||
func (s *Subscription) Pause() {
|
||||
if s.Status == SUBSCRIPTION_ACTIVE {
|
||||
s.Status = SUBSCRIPTION_PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
// Resume réactive un abonnement suspendu.
|
||||
func (s *Subscription) Resume() {
|
||||
if s.Status == SUBSCRIPTION_PAUSED {
|
||||
s.Status = SUBSCRIPTION_ACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel résilie l'abonnement.
|
||||
func (s *Subscription) Cancel(reason string) {
|
||||
now := time.Now().UTC()
|
||||
s.Status = SUBSCRIPTION_CANCELLED
|
||||
s.CancelledAt = &now
|
||||
s.CancelReason = reason
|
||||
s.AutoRenew = false
|
||||
}
|
||||
|
||||
// Renew avance la prochaine date de facturation d'une période.
|
||||
func (s *Subscription) Renew() {
|
||||
switch s.PlanType {
|
||||
case pricing.PAY_EVERY_WEEK:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(0, 0, 7)
|
||||
case pricing.PAY_EVERY_MONTH:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(0, 1, 0)
|
||||
case pricing.PAY_EVERY_YEAR:
|
||||
s.NextBillingDate = s.NextBillingDate.AddDate(1, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// IsExpired vérifie si l'abonnement a dépassé sa date de fin.
|
||||
func (s *Subscription) IsExpired() bool {
|
||||
if s.EndDate == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().UTC().After(*s.EndDate)
|
||||
}
|
||||
|
||||
// IsBillingDue vérifie si la prochaine échéance est atteinte.
|
||||
func (s *Subscription) IsBillingDue() bool {
|
||||
return s.Status == SUBSCRIPTION_ACTIVE && !time.Now().UTC().Before(s.NextBillingDate)
|
||||
}
|
||||
|
||||
// IsInTrial vérifie si l'abonnement est en période d'essai.
|
||||
func (s *Subscription) IsInTrial() bool {
|
||||
return s.Status == SUBSCRIPTION_TRIAL && s.TrialEndDate != nil && time.Now().UTC().Before(*s.TrialEndDate)
|
||||
}
|
||||
|
||||
// ComputeAmount recalcule le montant total depuis les items.
|
||||
func (s *Subscription) ComputeAmount() float64 {
|
||||
total := 0.0
|
||||
for _, item := range s.Items {
|
||||
total += item.UnitPrice * float64(item.Quantity)
|
||||
}
|
||||
s.Amount = total
|
||||
return total
|
||||
}
|
||||
|
||||
func (s *Subscription) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
func (s *Subscription) StoreDraftDefault() {
|
||||
s.IsDraft = true
|
||||
}
|
||||
|
||||
func (s *Subscription) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
incoming := set.(*Subscription)
|
||||
if !s.IsDraft && s.Status != incoming.Status {
|
||||
return true, &Subscription{
|
||||
Status: incoming.Status,
|
||||
AutoRenew: incoming.AutoRenew,
|
||||
CancelReason: incoming.CancelReason,
|
||||
}
|
||||
}
|
||||
return s.IsDraft, set
|
||||
}
|
||||
|
||||
func (s *Subscription) CanDelete() bool {
|
||||
return s.IsDraft || s.Status == SUBSCRIPTION_CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
|
||||
type subscriptionMongoAccessor struct {
|
||||
utils.AbstractAccessor[*Subscription]
|
||||
}
|
||||
|
||||
func NewAccessor(request *tools.APIRequest) *subscriptionMongoAccessor {
|
||||
return &subscriptionMongoAccessor{
|
||||
AbstractAccessor: utils.AbstractAccessor[*Subscription]{
|
||||
Logger: logs.CreateLogger(tools.SUBSCRIPTION.String()),
|
||||
Request: request,
|
||||
Type: tools.SUBSCRIPTION,
|
||||
New: func() *Subscription { return &Subscription{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package bill_test
|
||||
package billing_test
|
||||
|
||||
import (
|
||||
"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/order"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
@@ -12,71 +12,67 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ---- Bill model ----
|
||||
|
||||
func TestBill_StoreDraftDefault(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
b.StoreDraftDefault()
|
||||
assert.True(t, b.IsDraft)
|
||||
}
|
||||
|
||||
func TestBill_CanDelete_Draft(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
b.IsDraft = true
|
||||
assert.True(t, b.CanDelete())
|
||||
}
|
||||
|
||||
func TestBill_CanDelete_NonDraft(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
b.IsDraft = false
|
||||
assert.False(t, b.CanDelete())
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
|
||||
b := &bill.Bill{Status: enum.PENDING}
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = false
|
||||
set := &bill.Bill{Status: enum.PAID}
|
||||
set := &billing.Bill{Status: enum.PAID}
|
||||
ok, returned := b.CanUpdate(set)
|
||||
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) {
|
||||
b := &bill.Bill{Status: enum.PENDING}
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = false
|
||||
set := &bill.Bill{Status: enum.PENDING}
|
||||
set := &billing.Bill{Status: enum.PENDING}
|
||||
ok, _ := b.CanUpdate(set)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestBill_CanUpdate_Draft(t *testing.T) {
|
||||
b := &bill.Bill{Status: enum.PENDING}
|
||||
b := &billing.Bill{Status: enum.PENDING}
|
||||
b.IsDraft = true
|
||||
set := &bill.Bill{Status: enum.PAID}
|
||||
set := &billing.Bill{Status: enum.PAID}
|
||||
ok, _ := b.CanUpdate(set)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestBill_GetAccessor(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
acc := b.GetAccessor(&tools.APIRequest{})
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
|
||||
func TestBill_GetAccessor_NilRequest(t *testing.T) {
|
||||
b := &bill.Bill{}
|
||||
b := &billing.Bill{}
|
||||
acc := b.GetAccessor(nil)
|
||||
assert.NotNil(t, acc)
|
||||
}
|
||||
|
||||
// ---- GenerateBill ----
|
||||
|
||||
func TestGenerateBill_Basic(t *testing.T) {
|
||||
o := &order.Order{
|
||||
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
|
||||
}
|
||||
req := &tools.APIRequest{PeerID: "peer-abc"}
|
||||
b, err := bill.GenerateBill(o, req)
|
||||
b, err := billing.GenerateBill(o, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, b)
|
||||
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")
|
||||
}
|
||||
|
||||
// ---- SumUpBill ----
|
||||
|
||||
func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
|
||||
b := &bill.Bill{Total: 0}
|
||||
b := &billing.Bill{Total: 0}
|
||||
result, err := b.SumUpBill(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0.0, result.Total)
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
)
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
type Booking struct {
|
||||
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
|
||||
|
||||
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
|
||||
// (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it.
|
||||
OriginRef string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"`
|
||||
|
||||
// BillingStrategy est la fréquence de facturation appliquée à ce booking
|
||||
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
|
||||
// Transmis depuis WorkflowExecution.SelectedBillingStrategy lors du Book().
|
||||
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
|
||||
|
||||
// PaymentType est le mode de paiement choisi pour cette ressource spécifique
|
||||
// (PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR).
|
||||
// Résolu depuis WorkflowExecution.SelectedPaymentMode[itemID] lors du Book().
|
||||
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
|
||||
}
|
||||
|
||||
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
|
||||
@@ -135,5 +146,5 @@ func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
|
||||
}
|
||||
|
||||
func (r *Booking) CanDelete() bool {
|
||||
return r.IsDraft // only draft bookings can be deleted
|
||||
return true // only draft bookings can be deleted
|
||||
}
|
||||
|
||||
+8
-18
@@ -1,23 +1,13 @@
|
||||
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 (
|
||||
PLANNED BookingMode = iota // predictible
|
||||
PREEMPTED // can be both predictible or unpredictible, first one asking for a quick exec, second on event, but we pay to preempt in any case.
|
||||
WHEN_POSSIBLE // unpredictable, two mode of payment can be available on that case: fixed, or per USE
|
||||
PLANNED = enum.PLANNED
|
||||
PREEMPTED = enum.PREEMPTED
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
|
||||
// InstanceCapacity holds the maximum available resources of a single resource instance.
|
||||
type InstanceCapacity struct {
|
||||
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
|
||||
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
|
||||
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
|
||||
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
|
||||
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
|
||||
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
|
||||
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
|
||||
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
|
||||
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ type PlannerITF interface {
|
||||
// any availability check against a blocked resource returns false immediately.
|
||||
type Planner struct {
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
|
||||
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
|
||||
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
|
||||
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
|
||||
BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
|
||||
}
|
||||
|
||||
|
||||
@@ -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 / ...)
|
||||
*/
|
||||
@@ -1,5 +1,7 @@
|
||||
package enum
|
||||
|
||||
import "fmt"
|
||||
|
||||
type InfrastructureType int
|
||||
|
||||
const (
|
||||
@@ -18,3 +20,11 @@ func (t InfrastructureType) String() string {
|
||||
func InfrastructureList() []InfrastructureType {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package enum
|
||||
|
||||
import "fmt"
|
||||
|
||||
type StorageSize int
|
||||
|
||||
// StorageType - Enum that defines the type of storage
|
||||
@@ -54,3 +56,11 @@ func (t StorageType) String() string {
|
||||
func TypeList() []StorageType {
|
||||
return []StorageType{FILE, STREAM, API, DATABASE, S3, MEMORY, HARDWARE, AZURE, GCS}
|
||||
}
|
||||
|
||||
func (d StorageType) Compare(indexStr interface{}) bool {
|
||||
return fmt.Sprintf("%v", indexStr) == fmt.Sprintf("%v", d.EnumIndex()) || fmt.Sprintf("%v", indexStr) == d.String()
|
||||
}
|
||||
|
||||
func (d StorageType) EnumIndex() int {
|
||||
return int(d)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,73 @@
|
||||
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 {
|
||||
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
|
||||
Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
|
||||
Args string `json:"args,omitempty" bson:"args,omitempty"` // Args is the container arguments
|
||||
Env map[string]string `json:"env,omitempty" bson:"env,omitempty"` // Env is the container environment variables
|
||||
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
|
||||
PathSource
|
||||
|
||||
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
|
||||
Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
|
||||
}
|
||||
|
||||
type Expose struct {
|
||||
|
||||
@@ -7,12 +7,12 @@ type Artifact struct {
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Name string `json:"name" bson:"name" validate:"required"`
|
||||
Attr string `json:"attr,omitempty" bson:"attr,omitempty"`
|
||||
Value string `json:"value,omitempty" bson:"value,omitempty"`
|
||||
Origin string `json:"origin,omitempty" bson:"origin,omitempty"`
|
||||
Readonly bool `json:"readonly" bson:"readonly" default:"true"`
|
||||
Optionnal bool `json:"optionnal" bson:"optionnal" default:"true"`
|
||||
Name string `json:"name" bson:"name" validate:"required"`
|
||||
Attr string `json:"attr,omitempty" bson:"attr,omitempty"`
|
||||
Value string `json:"value,omitempty" bson:"value,omitempty"`
|
||||
Origin string `json:"origin,omitempty" bson:"origin,omitempty"`
|
||||
Readonly bool `json:"readonly" bson:"readonly" default:"true"`
|
||||
Required bool `json:"required" bson:"required" default:"true"`
|
||||
}
|
||||
|
||||
type InOutputs struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package pricing
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ type PricedItemITF interface {
|
||||
IsBooked() bool
|
||||
GetQuantity() int
|
||||
AddQuantity(amount int)
|
||||
GetBookingMode() booking.BookingMode
|
||||
GetBookingMode() enum.BookingMode
|
||||
GetCreatorID() string
|
||||
SelectPricing() PricingProfileITF
|
||||
GetLocationStart() *time.Time
|
||||
|
||||
@@ -111,13 +111,11 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
|
||||
|
||||
fromAverageDuration := after.Sub(now).Seconds()
|
||||
var tEnd time.Time
|
||||
if end == nil {
|
||||
tEnd = start.Add(5 * time.Minute)
|
||||
} else {
|
||||
fromDateDuration := float64(0)
|
||||
if end != nil {
|
||||
tEnd = *end
|
||||
fromDateDuration = tEnd.Sub(start).Seconds()
|
||||
}
|
||||
fromDateDuration := tEnd.Sub(start).Seconds()
|
||||
|
||||
if fromAverageDuration > fromDateDuration {
|
||||
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) {
|
||||
locationDurationInSecond = getAverageTimeInSecond(locationDurationInSecond, start, end)
|
||||
if locationDurationInSecond <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
priceStr := fmt.Sprintf("%v", price)
|
||||
p, err := strconv.ParseFloat(priceStr, 64)
|
||||
if err != nil {
|
||||
@@ -176,7 +177,6 @@ func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration
|
||||
}
|
||||
|
||||
return p.Price, nil
|
||||
|
||||
case PERMANENT:
|
||||
if variations != nil {
|
||||
price := p.Price
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
@@ -36,7 +38,8 @@ type LiveDatacenter struct {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
@@ -26,11 +28,11 @@ func (p ServiceProtocol) String() string {
|
||||
// rather than trusted from the ServiceResource, which may be stale.
|
||||
type LiveService struct {
|
||||
AbstractLive
|
||||
MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"`
|
||||
Protocol ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
|
||||
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
|
||||
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,omitempty"`
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
|
||||
MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"`
|
||||
Protocol ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
|
||||
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
|
||||
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,omitempty"`
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
|
||||
}
|
||||
|
||||
func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
@@ -38,5 +40,7 @@ func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
}
|
||||
|
||||
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"])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/enum"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
@@ -26,7 +28,9 @@ type LiveStorage struct {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
+12
-2
@@ -3,10 +3,15 @@ package models
|
||||
import (
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/allowed_image"
|
||||
"cloud.o-forge.io/core/oc-lib/models/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/live"
|
||||
"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/utils"
|
||||
"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_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
|
||||
tools.LIVE_SERVICE.String(): func() utils.DBObject { return &live.LiveService{} },
|
||||
tools.BILL.String(): func() utils.DBObject { return &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.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
|
||||
|
||||
@@ -22,7 +22,12 @@ type Order struct {
|
||||
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"`
|
||||
Bookings []*booking.Booking `json:"bookings" bson:"bookings"`
|
||||
|
||||
// Billing groupe les bookings par fréquence de facturation, peuplé par GenerateOrder.
|
||||
Billing map[pricing.BillingStrategy][]*booking.Booking `json:"billing" bson:"billing"`
|
||||
|
||||
// SubscriptionIDs liste les abonnements récurrents créés pour cet order
|
||||
// (un par peer × stratégie de facturation). Peuplé par DraftFirstBill.
|
||||
SubscriptionIDs []string `json:"subscription_ids,omitempty" bson:"subscription_ids,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Order) StoreDraftDefault() {
|
||||
|
||||
@@ -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
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/models/organization"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
"github.com/biter777/countries"
|
||||
@@ -30,12 +31,23 @@ const (
|
||||
NANO
|
||||
PENDING_NANO
|
||||
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 {
|
||||
for i, p := range path {
|
||||
fmt.Println("GetRelationPath", i, p)
|
||||
if str == p {
|
||||
return i
|
||||
}
|
||||
@@ -115,6 +127,25 @@ type Peer struct {
|
||||
BlacklistReason string `json:"blacklist_reason,omitempty" bson:"blacklist_reason,omitempty"`
|
||||
BehaviorWarnings []BehaviorWarning `json:"behavior_warnings,omitempty" bson:"behavior_warnings,omitempty"`
|
||||
|
||||
// MasterID is the libp2p PeerID of this peer's MASTER node.
|
||||
// Set by a NANO in its own signed PeerRecord so intermediaries cannot forge it.
|
||||
// When oc-discovery fails to reach a NANO, it routes the booking to MasterID instead.
|
||||
MasterID string `json:"master_id,omitempty" bson:"master_id,omitempty"`
|
||||
|
||||
// OrganizationMasterID is the MongoDB _id of the peer acting as this node's
|
||||
// organization master. Set automatically when an ORGANIZATION_MASTER relation
|
||||
// is validated (equivalent of MasterID for the Nano/Master hierarchy).
|
||||
OrganizationMasterID string `json:"organization_master_id,omitempty" bson:"organization_master_id,omitempty"`
|
||||
|
||||
// Organization holds optional descriptive data about the peer's organization.
|
||||
// Null when the peer has not registered any organization data.
|
||||
Organization *organization.Organization `json:"organization,omitempty" bson:"organization,omitempty"`
|
||||
|
||||
// PolicyID references the Policy document that governs which inbound
|
||||
// libp2p streams are authorized for this peer.
|
||||
// When empty, all non-vital streams are denied by default.
|
||||
PolicyID string `json:"policy_id,omitempty" bson:"policy_id,omitempty"`
|
||||
|
||||
// Volatile connectivity state — never persisted to DB (bson:"-").
|
||||
// Set in-memory by oc-peer when it receives a PEER_OBSERVE_RESPONSE_EVENT.
|
||||
// Considered offline when LastHeartbeat is older than 60 s (30 s interval + 30 s grace).
|
||||
@@ -131,6 +162,11 @@ func (ri *Peer) Extend(typ ...string) map[string][]tools.DataType {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.PEER)
|
||||
case "policy":
|
||||
if _, ok := ext[t]; !ok {
|
||||
ext[t] = []tools.DataType{}
|
||||
}
|
||||
ext[t] = append(ext[t], tools.POLICY)
|
||||
}
|
||||
}
|
||||
return ext
|
||||
|
||||
@@ -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}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInsta
|
||||
|
||||
type DataInstance struct {
|
||||
ResourceInstance[*DataResourcePartnership]
|
||||
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
|
||||
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
|
||||
}
|
||||
|
||||
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
|
||||
|
||||
+240
-33
@@ -20,9 +20,9 @@ import (
|
||||
*/
|
||||
type DynamicResource struct {
|
||||
AbstractResource
|
||||
Type tools.DataType `bson:"type,omitempty" json:"type,omitempty"`
|
||||
Filters map[string]interface{} `bson:"filters,omitempty" json:"filters,omitempty"`
|
||||
SortRules map[string]string `bson:"rules,omitempty" json:"rules,omitempty"`
|
||||
Type tools.DataType `bson:"type,omitempty" json:"type,omitempty"`
|
||||
Filters dbs.Filters `bson:"filters,omitempty" json:"filters,omitempty"`
|
||||
SortRules map[string]string `bson:"rules,omitempty" json:"rules,omitempty"`
|
||||
|
||||
PeerIds map[int]string `bson:"peer_ids,omitempty" json:"peer_ids,omitempty"`
|
||||
ResourceIds map[int]string `bson:"resource_ids,omitempty" json:"resource_ids,omitempty"`
|
||||
@@ -37,44 +37,249 @@ type DynamicResource struct {
|
||||
WatchedDynamicResource []string `bson:"watched_dynamic_resource,omitempty" json:"watched_dynamic_resource,omitempty"`
|
||||
}
|
||||
|
||||
// WorkspaceCandidatesProvider can be set by the workspace package to supply
|
||||
// contextual workspace resources for a given DataType and request without
|
||||
// creating a circular import (workspace → resources → workspace).
|
||||
// When set, SetAllowedInstances uses workspace-scoped resources instead of
|
||||
// the full catalog for requests that carry a username.
|
||||
var WorkspaceCandidatesProvider func(dt tools.DataType, request *tools.APIRequest) []ResourceInterface
|
||||
|
||||
func (d *DynamicResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DynamicResource) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
|
||||
d.Instances = []ResourceInstanceITF{}
|
||||
for k, v := range map[tools.DataType]ResourceInterface{
|
||||
tools.COMPUTE_RESOURCE: &ComputeResource{},
|
||||
tools.DATA_RESOURCE: &DataResource{},
|
||||
tools.STORAGE_RESOURCE: &StorageResource{},
|
||||
tools.PROCESSING_RESOURCE: &ProcessingResource{},
|
||||
tools.WORKFLOW_RESOURCE: &WorkflowResource{}} {
|
||||
if d.Type != k {
|
||||
continue
|
||||
}
|
||||
access := NewAccessor[*DynamicResource](k, request)
|
||||
a, _, _ := access.Search(dbs.FiltersFromFlatMap(d.Filters, v), "", false, 0, 100000)
|
||||
d.PeerIds = map[int]string{}
|
||||
d.ResourceIds = map[int]string{}
|
||||
for _, res := range a {
|
||||
for _, i := range res.(ResourceInterface).SetAllowedInstances(request, instance_id...) {
|
||||
d.PeerIds[len(d.Instances)] = res.GetCreatorID()
|
||||
d.ResourceIds[len(d.Instances)] = res.GetID()
|
||||
d.Instances = append(d.Instances, i)
|
||||
}
|
||||
}
|
||||
break
|
||||
if WorkspaceCandidatesProvider != nil {
|
||||
candidates := WorkspaceCandidatesProvider(d.Type, request)
|
||||
return d.SetAllowedInstancesFromSet(candidates, request, instance_id...)
|
||||
}
|
||||
sorted := make([]ResourceInstanceITF, len(d.Instances))
|
||||
copy(sorted, d.Instances)
|
||||
slices.SortStableFunc(sorted, func(a, b ResourceInstanceITF) int {
|
||||
d.SortRules["partnerships"] = "%v not contains 2"
|
||||
return d.compareByRules(a, b, d.SortRules)
|
||||
})
|
||||
d.WatchedDynamicResource = []string{}
|
||||
d.Instances = []ResourceInstanceITF{}
|
||||
d.sortAndResetInstances()
|
||||
return d.Instances
|
||||
}
|
||||
|
||||
// SetAllowedInstancesFromSet fills d.Instances from a pre-loaded workspace resource set
|
||||
// instead of querying the catalog. Filters are applied in-memory against the candidates.
|
||||
// Called by WorkspaceResourceSet.Fill so dynamic resources only see workspace-scoped resources.
|
||||
func (d *DynamicResource) SetAllowedInstancesFromSet(candidates []ResourceInterface, request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
|
||||
d.Instances = []ResourceInstanceITF{}
|
||||
d.PeerIds = map[int]string{}
|
||||
d.ResourceIds = map[int]string{}
|
||||
for _, res := range candidates {
|
||||
if !d.matchesFilters(res) {
|
||||
continue
|
||||
}
|
||||
for _, i := range res.SetAllowedInstances(request, instance_id...) {
|
||||
d.PeerIds[len(d.Instances)] = res.GetCreatorID()
|
||||
d.ResourceIds[len(d.Instances)] = res.GetID()
|
||||
d.Instances = append(d.Instances, i)
|
||||
}
|
||||
}
|
||||
d.sortAndResetInstances()
|
||||
return d.Instances
|
||||
}
|
||||
|
||||
func (d *DynamicResource) sortAndResetInstances() {
|
||||
if d.SortRules != nil {
|
||||
sorted := make([]ResourceInstanceITF, len(d.Instances))
|
||||
copy(sorted, d.Instances)
|
||||
slices.SortStableFunc(sorted, func(a, b ResourceInstanceITF) int {
|
||||
d.SortRules["partnerships"] = "%v not contains 2"
|
||||
return d.compareByRules(a, b, d.SortRules)
|
||||
})
|
||||
d.Instances = sorted
|
||||
}
|
||||
d.WatchedDynamicResource = []string{}
|
||||
}
|
||||
|
||||
// matchesFilters applies d.Filters in-memory against a serialized resource.
|
||||
// Keys in d.Filters are JSON tag names; Serialize returns JSON tag names — no bson conversion needed.
|
||||
func (d *DynamicResource) matchesFilters(res ResourceInterface) bool {
|
||||
if len(d.Filters.And) == 0 && len(d.Filters.Or) == 0 {
|
||||
return true
|
||||
}
|
||||
m := res.Serialize(res)
|
||||
for field, fs := range d.Filters.And {
|
||||
vals := nestedVals(m, strings.Split(field, "."))
|
||||
for _, f := range fs {
|
||||
if !anyMatchesOp(vals, f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(d.Filters.Or) > 0 {
|
||||
matched := false
|
||||
for field, fs := range d.Filters.Or {
|
||||
vals := nestedVals(m, strings.Split(field, "."))
|
||||
for _, f := range fs {
|
||||
if anyMatchesOp(vals, f) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nestedVals navigates a dot-path into m and collects all leaf values.
|
||||
// Arrays at any level are expanded: each element is recursed into.
|
||||
func nestedVals(m map[string]interface{}, path []string) []interface{} {
|
||||
if len(path) == 0 || m == nil {
|
||||
return nil
|
||||
}
|
||||
val, ok := m[path[0]]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if len(path) == 1 {
|
||||
if arr, ok := val.([]interface{}); ok {
|
||||
return arr
|
||||
}
|
||||
return []interface{}{val}
|
||||
}
|
||||
rest := path[1:]
|
||||
switch v := val.(type) {
|
||||
case map[string]interface{}:
|
||||
return nestedVals(v, rest)
|
||||
case []interface{}:
|
||||
var out []interface{}
|
||||
for _, elem := range v {
|
||||
if em, ok := elem.(map[string]interface{}); ok {
|
||||
out = append(out, nestedVals(em, rest)...)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// anyMatchesOp returns true if at least one value in vals satisfies filter f.
|
||||
func anyMatchesOp(vals []interface{}, f dbs.Filter) bool {
|
||||
if f.Operator == dbs.EXISTS.String() {
|
||||
exists := len(vals) > 0 && vals[0] != nil
|
||||
want := true
|
||||
if b, ok := f.Value.(bool); ok {
|
||||
want = b
|
||||
}
|
||||
return exists == want
|
||||
}
|
||||
if f.Operator == dbs.IN.String() {
|
||||
list, ok := f.Value.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, v := range vals {
|
||||
sv := fmt.Sprintf("%v", v)
|
||||
for _, item := range list {
|
||||
if sv == fmt.Sprintf("%v", item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, v := range vals {
|
||||
if opMatches(v, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func opMatches(val interface{}, f dbs.Filter) bool {
|
||||
switch f.Operator {
|
||||
case dbs.EQUAL.String():
|
||||
return fmt.Sprintf("%v", val) == fmt.Sprintf("%v", f.Value)
|
||||
case dbs.NOT.String():
|
||||
return fmt.Sprintf("%v", val) != fmt.Sprintf("%v", f.Value)
|
||||
case dbs.LIKE.String():
|
||||
return strings.Contains(strings.ToLower(fmt.Sprintf("%v", val)), strings.ToLower(fmt.Sprintf("%v", f.Value)))
|
||||
case dbs.GT.String(), dbs.GTE.String(), dbs.LT.String(), dbs.LTE.String():
|
||||
return numericCmp(val, f.Value, f.Operator)
|
||||
case dbs.ELEMMATCH.String():
|
||||
arr, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
sub, ok := f.Value.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, elem := range arr {
|
||||
em, ok := elem.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
allOk := true
|
||||
for k, sv := range sub {
|
||||
if fmt.Sprintf("%v", em[k]) != fmt.Sprintf("%v", sv) {
|
||||
allOk = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allOk {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func numericCmp(a, b interface{}, op string) bool {
|
||||
fa, aOk := toFloat64(a)
|
||||
fb, bOk := toFloat64(b)
|
||||
if !aOk || !bOk {
|
||||
sa, sb := fmt.Sprintf("%v", a), fmt.Sprintf("%v", b)
|
||||
switch op {
|
||||
case dbs.GT.String():
|
||||
return sa > sb
|
||||
case dbs.GTE.String():
|
||||
return sa >= sb
|
||||
case dbs.LT.String():
|
||||
return sa < sb
|
||||
case dbs.LTE.String():
|
||||
return sa <= sb
|
||||
}
|
||||
return false
|
||||
}
|
||||
switch op {
|
||||
case dbs.GT.String():
|
||||
return fa > fb
|
||||
case dbs.GTE.String():
|
||||
return fa >= fb
|
||||
case dbs.LT.String():
|
||||
return fa < fb
|
||||
case dbs.LTE.String():
|
||||
return fa <= fb
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func toFloat64(v interface{}) (float64, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n, true
|
||||
case float32:
|
||||
return float64(n), true
|
||||
case int:
|
||||
return float64(n), true
|
||||
case int32:
|
||||
return float64(n), true
|
||||
case int64:
|
||||
return float64(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
|
||||
func (d *DynamicResource) AddInstances(instance ResourceInstanceITF) {
|
||||
d.Instances = append(d.Instances, instance)
|
||||
}
|
||||
@@ -91,13 +296,15 @@ func (d *DynamicResource) GetSelectedInstance(index *int) ResourceInstanceITF {
|
||||
d.SelectedIndex = i
|
||||
|
||||
for i := range inst.GetPartnerships() {
|
||||
fmt.Println(inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy), d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy)
|
||||
if inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy) != nil {
|
||||
d.SelectedPartnershipIndex = &i
|
||||
break
|
||||
}
|
||||
}
|
||||
if d.SelectedPartnershipIndex == nil {
|
||||
continue
|
||||
i := 0
|
||||
d.SelectedPartnershipIndex = &i
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -29,6 +29,8 @@ type ResourceInterface interface {
|
||||
GetEnv() []models.Param
|
||||
GetInputs() []models.Param
|
||||
GetOutputs() []models.Param
|
||||
GetExploitationAuthorizations() []ExploitationAuthorization
|
||||
GetConsents() []Consent
|
||||
}
|
||||
|
||||
type ResourceInstanceITF interface {
|
||||
|
||||
+125
-1
@@ -15,7 +15,8 @@ type ResourceSet struct {
|
||||
Services []string `bson:"services,omitempty" json:"services,omitempty"`
|
||||
Dynamics []string `bson:"dynamics,omitempty" json:"dynamics,omitempty"`
|
||||
|
||||
// DynamicResources are stored inline — no DB collection, resolved at runtime via SetAllowedInstances.
|
||||
// Runtime-only resource objects — not persisted. Populated by Fill() from the ID lists above.
|
||||
// Use WorkspaceResourceSet when full object persistence is needed (workspace fluid catalog).
|
||||
DynamicResources []*DynamicResource `bson:"-" json:"dynamic_resources,omitempty"`
|
||||
DataResources []*DataResource `bson:"-" json:"data_resources,omitempty"`
|
||||
StorageResources []*StorageResource `bson:"-" json:"storage_resources,omitempty"`
|
||||
@@ -26,6 +27,129 @@ type ResourceSet struct {
|
||||
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() {
|
||||
r.DataResources = nil
|
||||
r.StorageResources = nil
|
||||
|
||||
@@ -38,7 +38,13 @@ func (d *NativeTool) ClearEnv() utils.DBObject {
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -31,23 +31,23 @@ type ProcessingResource struct {
|
||||
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
|
||||
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"`
|
||||
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
|
||||
License string `json:"license,omitempty" bson:"license,omitempty"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
// IsService marks a long-running processing that acts as a persistent service.
|
||||
// Such processings do not require a Compute booking (they manage their own lifecycle).
|
||||
IsService bool `json:"is_service" bson:"is_service" default:"false"`
|
||||
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
|
||||
// License is now in AbstractResource — kept here as alias for backward compat with existing DB docs.
|
||||
// New code should use AbstractResource.License.
|
||||
}
|
||||
|
||||
func (r *ProcessingResource) GetType() string {
|
||||
return tools.PROCESSING_RESOURCE.String()
|
||||
}
|
||||
|
||||
type ProcessingResourceAccess struct {
|
||||
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
|
||||
}
|
||||
|
||||
type ProcessingInstance struct {
|
||||
ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]
|
||||
Access *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
|
||||
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
|
||||
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
|
||||
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
|
||||
}
|
||||
|
||||
func NewProcessingInstance(name string, peerID string) ResourceInstanceITF {
|
||||
|
||||
@@ -3,6 +3,7 @@ package resources
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
@@ -39,6 +40,20 @@ type AbstractResource struct {
|
||||
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
|
||||
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
|
||||
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
|
||||
|
||||
// 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 {
|
||||
@@ -83,6 +98,33 @@ func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExploitationAuthorizations returns all AEs attached to this resource.
|
||||
// Used by oc-schedulerd's CheckWorkflowAE via structural interface assertion.
|
||||
func (r *AbstractResource) GetExploitationAuthorizations() []ExploitationAuthorization {
|
||||
return r.ExploitationAuthorizations
|
||||
}
|
||||
|
||||
// GetConsents returns the consent questions declared by this resource.
|
||||
func (r *AbstractResource) GetConsents() []Consent {
|
||||
return r.Consents
|
||||
}
|
||||
|
||||
// FilterExploitationAuthorizations removes AEs that are not visible to peerID.
|
||||
// Must be called before serializing the resource for a consumer peer.
|
||||
// The resource owner (CreatorID) always sees all AEs unfiltered.
|
||||
func (r *AbstractResource) FilterExploitationAuthorizations(peerID string, isAdmin bool) {
|
||||
if isAdmin {
|
||||
return // admin or owner: no filtering
|
||||
}
|
||||
filtered := r.ExploitationAuthorizations[:0]
|
||||
for _, ae := range r.ExploitationAuthorizations {
|
||||
if ae.IsVisibleTo(peerID, r.CreatorID) {
|
||||
filtered = append(filtered, ae)
|
||||
}
|
||||
}
|
||||
r.ExploitationAuthorizations = filtered
|
||||
}
|
||||
|
||||
func (ri *AbstractResource) ClearEnv() utils.DBObject {
|
||||
ri.Env = []models.Param{}
|
||||
ri.Inputs = []models.Param{}
|
||||
@@ -112,7 +154,8 @@ func (r *AbstractResource) StoreDraftDefault() {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -201,12 +244,15 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
|
||||
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
|
||||
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
|
||||
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
|
||||
// Filter AEs: only return AEs visible to the requesting peer.
|
||||
if request != nil {
|
||||
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
|
||||
}
|
||||
}
|
||||
inst := []ResourceInstanceITF{}
|
||||
for _, i := range abs.Instances {
|
||||
inst = append(inst, i)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
@@ -528,3 +574,18 @@ func ToResource(
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"cloud.o-forge.io/core/oc-lib/config"
|
||||
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||
"cloud.o-forge.io/core/oc-lib/logs"
|
||||
"cloud.o-forge.io/core/oc-lib/models/common/models"
|
||||
"cloud.o-forge.io/core/oc-lib/models/live"
|
||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||
"cloud.o-forge.io/core/oc-lib/tools"
|
||||
@@ -16,6 +19,48 @@ type ResourceMongoAccessor[T ResourceInterface] struct {
|
||||
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
|
||||
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest) *ResourceMongoAccessor[T] {
|
||||
if !slices.Contains([]tools.DataType{
|
||||
@@ -67,6 +112,40 @@ func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, er
|
||||
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) {
|
||||
if dca.GetType() == tools.COMPUTE_RESOURCE {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
res, _, _ := a.LoadOne(r.Instances[0].GetID())
|
||||
fmt.Println(res, r.Instances[0].GetID())
|
||||
if res == nil {
|
||||
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
|
||||
}
|
||||
@@ -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})
|
||||
res, _, _ := a.LoadOne(r.Instances[0].GetID())
|
||||
if res == nil {
|
||||
return nil, 404, errors.New("can't create a non existing service resource not reported onto compute units catalog")
|
||||
return nil, 404, errors.New("can't create a non existing service resource not reported onto service catalog")
|
||||
}
|
||||
if !res.(*live.LiveService).IsCompatible(data.Serialize(data)) {
|
||||
return nil, 404, errors.New("live service target is not compatible")
|
||||
@@ -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})
|
||||
res, _, _ := a.LoadOne(r.Instances[0].GetID())
|
||||
if res == nil {
|
||||
return nil, 404, errors.New("can't create a non existing storage resource not reported onto compute units catalog")
|
||||
return nil, 404, errors.New("can't create a non existing storage resource not reported onto storage catalog")
|
||||
}
|
||||
if !res.(*live.LiveStorage).IsCompatible(data.Serialize(data)) {
|
||||
return nil, 404, errors.New("live storage target is not compatible")
|
||||
@@ -133,6 +221,7 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
|
||||
i = res.GetID()
|
||||
idsToUpdate = res.(*live.LiveStorage).ResourcesID
|
||||
}
|
||||
applyAccessSourceOutput(data)
|
||||
res, code, err := utils.GenericStoreOne(data, dca)
|
||||
if res != nil && i != "" {
|
||||
idsToUpdate = append(idsToUpdate, res.GetID())
|
||||
@@ -140,9 +229,119 @@ func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObje
|
||||
"resources_id": idsToUpdate,
|
||||
}, i)
|
||||
}
|
||||
if err == nil && res != nil {
|
||||
b, _ := json.Marshal(res)
|
||||
go emitResourceNATS(tools.CREATE_RESOURCE, dca.GetType(), b)
|
||||
}
|
||||
return res, code, err
|
||||
}
|
||||
|
||||
// PurgedResourcePayload holds a silently-deleted resource's type and serialized payload.
|
||||
type PurgedResourcePayload struct {
|
||||
DT tools.DataType
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// purgeByType searches and silently deletes all resources of type T created by creatorID.
|
||||
// Uses AbstractAccessor.DeleteOne directly to bypass the NATS-emitting override.
|
||||
func purgeByType[T ResourceInterface](dt tools.DataType, creatorID string) []PurgedResourcePayload {
|
||||
a := NewAccessor[T](dt, nil)
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
res, _, _ := a.Search(&dbs.Filters{
|
||||
And: map[string][]dbs.Filter{
|
||||
"creator_id": {{Operator: dbs.EQUAL.String(), Value: creatorID}},
|
||||
},
|
||||
}, "", false, 0, 10000)
|
||||
var result []PurgedResourcePayload
|
||||
for _, item := range res {
|
||||
b, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
a.AbstractAccessor.DeleteOne(item.GetID())
|
||||
result = append(result, PurgedResourcePayload{DT: dt, Payload: b})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PurgeCreatorResources deletes all catalog resources created by creatorPeerID from
|
||||
// the DB without emitting NATS. Used for non-blacklist peer privilege downgrades where
|
||||
// workspace state should be left untouched.
|
||||
func PurgeCreatorResources(creatorPeerID string) []PurgedResourcePayload {
|
||||
var result []PurgedResourcePayload
|
||||
result = append(result, purgeByType[*ComputeResource](tools.COMPUTE_RESOURCE, creatorPeerID)...)
|
||||
result = append(result, purgeByType[*DataResource](tools.DATA_RESOURCE, creatorPeerID)...)
|
||||
result = append(result, purgeByType[*ProcessingResource](tools.PROCESSING_RESOURCE, creatorPeerID)...)
|
||||
result = append(result, purgeByType[*StorageResource](tools.STORAGE_RESOURCE, creatorPeerID)...)
|
||||
result = append(result, purgeByType[*WorkflowResource](tools.WORKFLOW_RESOURCE, creatorPeerID)...)
|
||||
result = append(result, purgeByType[*ServiceResource](tools.SERVICE_RESOURCE, creatorPeerID)...)
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterMapFromResourcePayload deserializes a resource payload by DataType, zeros out
|
||||
// the AbstractInstanciatedResource (and its AbstractResource / Instances sub-fields),
|
||||
// then marshals back to get only the concrete type's own JSON fields.
|
||||
// Returns nil for WORKFLOW_RESOURCE and unknown types.
|
||||
// JSON keys only — not BSON paths.
|
||||
func FilterMapFromResourcePayload(dt tools.DataType, payload []byte) map[string]interface{} {
|
||||
var m map[string]interface{}
|
||||
switch dt {
|
||||
case tools.COMPUTE_RESOURCE:
|
||||
var r ComputeResource
|
||||
if json.Unmarshal(payload, &r) != nil {
|
||||
return nil
|
||||
}
|
||||
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*ComputeResourceInstance]{}
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, &m)
|
||||
case tools.DATA_RESOURCE:
|
||||
var r DataResource
|
||||
if json.Unmarshal(payload, &r) != nil {
|
||||
return nil
|
||||
}
|
||||
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*DataInstance]{}
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, &m)
|
||||
case tools.PROCESSING_RESOURCE:
|
||||
var r ProcessingResource
|
||||
if json.Unmarshal(payload, &r) != nil {
|
||||
return nil
|
||||
}
|
||||
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*ProcessingInstance]{}
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, &m)
|
||||
case tools.STORAGE_RESOURCE:
|
||||
var r StorageResource
|
||||
if json.Unmarshal(payload, &r) != nil {
|
||||
return nil
|
||||
}
|
||||
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*StorageResourceInstance]{}
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, &m)
|
||||
case tools.SERVICE_RESOURCE:
|
||||
var r ServiceResource
|
||||
if json.Unmarshal(payload, &r) != nil {
|
||||
return nil
|
||||
}
|
||||
r.AbstractInstanciatedResource = AbstractInstanciatedResource[*ServiceInstance]{}
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, &m)
|
||||
case tools.WORKFLOW_RESOURCE:
|
||||
var r WorkflowResource
|
||||
if json.Unmarshal(payload, &r) != nil {
|
||||
return nil
|
||||
}
|
||||
r.AbstractResource = AbstractResource{}
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, &m)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
|
||||
return dca.StoreOne(data)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,13 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ func (r *AbstractObject) DeepCopy() *AbstractObject {
|
||||
return &obj
|
||||
}
|
||||
|
||||
func (r *AbstractObject) SetDraft(draft bool) {
|
||||
r.IsDraft = draft
|
||||
}
|
||||
|
||||
func (r *AbstractObject) SetName(name string) {
|
||||
r.Name = name
|
||||
}
|
||||
@@ -146,7 +150,7 @@ func (ao *AbstractObject) UpToDate(user string, peer string, create bool) {
|
||||
ao.UpdateDate = time.Now()
|
||||
ao.UpdaterID = peer
|
||||
ao.UserUpdaterID = user
|
||||
if create && ao.CreatorID != "" {
|
||||
if create && ao.CreatorID == "" {
|
||||
ao.CreationDate = time.Now()
|
||||
ao.CreatorID = peer
|
||||
ao.UserCreatorID = user
|
||||
|
||||
+86
-4
@@ -109,6 +109,9 @@ func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor)
|
||||
obj := a.NewObj()
|
||||
b, _ := json.Marshal(r)
|
||||
json.Unmarshal(b, obj)
|
||||
if change["is_draft"] == true {
|
||||
obj.SetDraft(change["is_draft"] == true)
|
||||
}
|
||||
if !a.GetRequest().Admin {
|
||||
var ok bool
|
||||
ok, r = r.CanUpdate(obj)
|
||||
@@ -125,10 +128,8 @@ func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor)
|
||||
r.Sign()
|
||||
}
|
||||
|
||||
loaded := r.Serialize(r) // get the loaded object
|
||||
for k, v := range change { // apply the changes, with a flatten method
|
||||
loaded[k] = v
|
||||
}
|
||||
loaded := r.Serialize(r) // get the loaded object
|
||||
deepMerge(loaded, change)
|
||||
newObj := a.NewObj()
|
||||
b, err = json.Marshal(loaded)
|
||||
if err != nil {
|
||||
@@ -252,6 +253,87 @@ func IsMySelf(peerID string, wfa Accessor) (bool, string) {
|
||||
return peerID == pp.GetID(), pp.GetID()
|
||||
}
|
||||
|
||||
// deepMerge overlays patch values onto base, preserving base values for keys
|
||||
// absent from patch, nil patch values, and empty strings when base is non-empty.
|
||||
// This prevents partial frontend payloads from silently erasing server-managed
|
||||
// fields (source, env, country, owners, creator_id, creation_date, …).
|
||||
func deepMerge(base, patch map[string]interface{}) {
|
||||
for k, pv := range patch {
|
||||
bv := base[k]
|
||||
switch pvTyped := pv.(type) {
|
||||
case map[string]interface{}:
|
||||
if bvMap, ok := bv.(map[string]interface{}); ok {
|
||||
deepMerge(bvMap, pvTyped)
|
||||
} else {
|
||||
base[k] = pv
|
||||
}
|
||||
case []interface{}:
|
||||
if bvSlice, ok := bv.([]interface{}); ok {
|
||||
base[k] = mergeSlices(bvSlice, pvTyped)
|
||||
} else {
|
||||
base[k] = pv
|
||||
}
|
||||
case string:
|
||||
// Don't overwrite a non-empty base value with an empty string.
|
||||
if pvTyped != "" {
|
||||
base[k] = pv
|
||||
}
|
||||
default:
|
||||
if pv != nil {
|
||||
base[k] = pv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeSlices merges two slices element-wise.
|
||||
// For slices of maps it matches elements by their "id" field when available;
|
||||
// falls back to positional matching. An empty patch slice leaves base intact.
|
||||
func mergeSlices(base, patch []interface{}) []interface{} {
|
||||
if len(patch) == 0 {
|
||||
return base
|
||||
}
|
||||
for _, e := range patch {
|
||||
if _, ok := e.(map[string]interface{}); !ok {
|
||||
return patch // non-map elements: replace wholesale
|
||||
}
|
||||
}
|
||||
baseByID := map[string]map[string]interface{}{}
|
||||
for _, e := range base {
|
||||
if em, ok := e.(map[string]interface{}); ok {
|
||||
if id, ok := em["id"].(string); ok && id != "" {
|
||||
baseByID[id] = em
|
||||
}
|
||||
}
|
||||
}
|
||||
result := make([]interface{}, 0, len(patch))
|
||||
for i, pe := range patch {
|
||||
pm, _ := pe.(map[string]interface{})
|
||||
if pm == nil {
|
||||
result = append(result, pe)
|
||||
continue
|
||||
}
|
||||
var baseElem map[string]interface{}
|
||||
if id, ok := pm["id"].(string); ok && id != "" {
|
||||
baseElem = baseByID[id]
|
||||
}
|
||||
if baseElem == nil && i < len(base) {
|
||||
baseElem, _ = base[i].(map[string]interface{})
|
||||
}
|
||||
if baseElem != nil {
|
||||
merged := make(map[string]interface{}, len(baseElem))
|
||||
for k, v := range baseElem {
|
||||
merged[k] = v
|
||||
}
|
||||
deepMerge(merged, pm)
|
||||
result = append(result, merged)
|
||||
} else {
|
||||
result = append(result, pe)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateNodeID() (string, error) {
|
||||
folderStatic := "/var/lib/opencloud-node"
|
||||
if _, err := os.Stat(folderStatic); err == nil {
|
||||
|
||||
@@ -26,6 +26,7 @@ type DBObject interface {
|
||||
GetID() string
|
||||
GetName() string
|
||||
SetName(name string)
|
||||
SetDraft(draft bool)
|
||||
IsDrafted() bool
|
||||
CanDelete() bool
|
||||
StoreDraftDefault()
|
||||
|
||||
@@ -145,6 +145,50 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
|
||||
return max, nil
|
||||
}
|
||||
|
||||
// DataStorageLink represents a resolved Data→Storage pair found in the graph.
|
||||
type DataStorageLink struct {
|
||||
DataItemID string
|
||||
StorageItemID string
|
||||
}
|
||||
|
||||
// GetDataStorageLinks returns all links that connect a Data item to a Storage item.
|
||||
// These links are mandatory when the Data instance has a Source configured:
|
||||
// the workflow builder uses them to know where to download the data before
|
||||
// any processing step that consumes that storage.
|
||||
func (g *Graph) GetDataStorageLinks() []DataStorageLink {
|
||||
var result []DataStorageLink
|
||||
for _, link := range g.Links {
|
||||
srcItem, srcOk := g.Items[link.Source.ID]
|
||||
dstItem, dstOk := g.Items[link.Destination.ID]
|
||||
if !srcOk || !dstOk {
|
||||
continue
|
||||
}
|
||||
if g.IsData(srcItem) && g.IsStorage(dstItem) {
|
||||
result = append(result, DataStorageLink{
|
||||
DataItemID: link.Source.ID,
|
||||
StorageItemID: link.Destination.ID,
|
||||
})
|
||||
} else if g.IsStorage(srcItem) && g.IsData(dstItem) {
|
||||
result = append(result, DataStorageLink{
|
||||
DataItemID: link.Destination.ID,
|
||||
StorageItemID: link.Source.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLinkedStorageForData returns the storage item IDs linked to a given Data item.
|
||||
func (g *Graph) GetLinkedStorageForData(dataItemID string) []string {
|
||||
var storageIDs []string
|
||||
for _, dsl := range g.GetDataStorageLinks() {
|
||||
if dsl.DataItemID == dataItemID {
|
||||
storageIDs = append(storageIDs, dsl.StorageItemID)
|
||||
}
|
||||
}
|
||||
return storageIDs
|
||||
}
|
||||
|
||||
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
|
||||
if item, ok := g.Items[id]; ok {
|
||||
if item.NativeTool != nil {
|
||||
|
||||
@@ -15,32 +15,32 @@ type GraphItem struct {
|
||||
}
|
||||
|
||||
func (g *GraphItem) GetResource() (tools.DataType, resources.ResourceInterface) {
|
||||
if g.Data != nil {
|
||||
return tools.DATA_RESOURCE, g.Data
|
||||
} else if g.Compute != nil {
|
||||
return tools.COMPUTE_RESOURCE, g.Compute
|
||||
} else if g.Workflow != nil {
|
||||
return tools.WORKFLOW_RESOURCE, g.Workflow
|
||||
} else if g.Processing != nil {
|
||||
return tools.PROCESSING_RESOURCE, g.Processing
|
||||
} else if g.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, g.Storage
|
||||
} else if g.NativeTool != nil {
|
||||
return tools.NATIVE_TOOL, g.NativeTool
|
||||
} else if g.Service != nil {
|
||||
return tools.SERVICE_RESOURCE, g.Service
|
||||
} else if g.Dynamic != nil {
|
||||
return tools.DYNAMIC_RESOURCE, g.Dynamic
|
||||
if g.ItemResource.Data != nil {
|
||||
return tools.DATA_RESOURCE, g.ItemResource.Data
|
||||
} else if g.ItemResource.Compute != nil {
|
||||
return tools.COMPUTE_RESOURCE, g.ItemResource.Compute
|
||||
} else if g.ItemResource.Workflow != nil {
|
||||
return tools.WORKFLOW_RESOURCE, g.ItemResource.Workflow
|
||||
} else if g.ItemResource.Processing != nil {
|
||||
return tools.PROCESSING_RESOURCE, g.ItemResource.Processing
|
||||
} else if g.ItemResource.Storage != nil {
|
||||
return tools.STORAGE_RESOURCE, g.ItemResource.Storage
|
||||
} else if g.ItemResource.NativeTool != nil {
|
||||
return tools.NATIVE_TOOL, g.ItemResource.NativeTool
|
||||
} else if g.ItemResource.Service != nil {
|
||||
return tools.SERVICE_RESOURCE, g.ItemResource.Service
|
||||
} else if g.ItemResource.Dynamic != nil {
|
||||
return tools.DYNAMIC_RESOURCE, g.ItemResource.Dynamic
|
||||
}
|
||||
return tools.INVALID, nil
|
||||
}
|
||||
|
||||
func (g *GraphItem) Clear() {
|
||||
g.Data = nil
|
||||
g.Compute = nil
|
||||
g.Workflow = nil
|
||||
g.Processing = nil
|
||||
g.Storage = nil
|
||||
g.Service = nil
|
||||
g.Dynamic = nil
|
||||
g.ItemResource.Data = nil
|
||||
g.ItemResource.Compute = nil
|
||||
g.ItemResource.Workflow = nil
|
||||
g.ItemResource.Processing = nil
|
||||
g.ItemResource.Storage = nil
|
||||
g.ItemResource.Service = nil
|
||||
g.ItemResource.Dynamic = nil
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ func (l *GraphLink) IsComputeLink(g Graph) (bool, string) {
|
||||
if g.Items == nil {
|
||||
return false, ""
|
||||
}
|
||||
if d, ok := g.Items[l.Source.ID]; ok && d.Compute != nil {
|
||||
return true, d.Compute.UUID
|
||||
if d, ok := g.Items[l.Source.ID]; ok && d.ItemResource.Compute != nil {
|
||||
return true, d.ItemResource.Compute.UUID
|
||||
}
|
||||
if d, ok := g.Items[l.Destination.ID]; ok && d.Compute != nil {
|
||||
return true, d.Compute.UUID
|
||||
if d, ok := g.Items[l.Destination.ID]; ok && d.ItemResource.Compute != nil {
|
||||
return true, d.ItemResource.Compute.UUID
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
+18
-18
@@ -104,17 +104,17 @@ func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
|
||||
|
||||
func plantUMLPrefix(item graph.GraphItem) string {
|
||||
switch {
|
||||
case item.NativeTool != nil:
|
||||
case item.ItemResource.NativeTool != nil:
|
||||
return "e"
|
||||
case item.Data != nil:
|
||||
case item.ItemResource.Data != nil:
|
||||
return "d"
|
||||
case item.Processing != nil:
|
||||
case item.ItemResource.Processing != nil:
|
||||
return "p"
|
||||
case item.Storage != nil:
|
||||
case item.ItemResource.Storage != nil:
|
||||
return "s"
|
||||
case item.Compute != nil:
|
||||
case item.ItemResource.Compute != nil:
|
||||
return "c"
|
||||
case item.Workflow != nil:
|
||||
case item.ItemResource.Workflow != nil:
|
||||
return "wf"
|
||||
}
|
||||
return "u"
|
||||
@@ -123,24 +123,24 @@ func plantUMLPrefix(item graph.GraphItem) string {
|
||||
// plantUMLItemLine builds the PlantUML declaration line for one graph item.
|
||||
func plantUMLItemLine(varName string, item graph.GraphItem) string {
|
||||
switch {
|
||||
case item.NativeTool != nil:
|
||||
case item.ItemResource.NativeTool != nil:
|
||||
// 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:
|
||||
return plantUMLResourceLine("Data", varName, item.Data)
|
||||
case item.ItemResource.Data != nil:
|
||||
return plantUMLResourceLine("Data", varName, item.ItemResource.Data)
|
||||
|
||||
case item.Processing != nil:
|
||||
return plantUMLResourceLine("Processing", varName, item.Processing)
|
||||
case item.ItemResource.Processing != nil:
|
||||
return plantUMLResourceLine("Processing", varName, item.ItemResource.Processing)
|
||||
|
||||
case item.Storage != nil:
|
||||
return plantUMLResourceLine("Storage", varName, item.Storage)
|
||||
case item.ItemResource.Storage != nil:
|
||||
return plantUMLResourceLine("Storage", varName, item.ItemResource.Storage)
|
||||
|
||||
case item.Compute != nil:
|
||||
return plantUMLResourceLine("ComputeUnit", varName, item.Compute)
|
||||
case item.ItemResource.Compute != nil:
|
||||
return plantUMLResourceLine("ComputeUnit", varName, item.ItemResource.Compute)
|
||||
|
||||
case item.Workflow != nil:
|
||||
return plantUMLResourceLine("Workflow", varName, item.Workflow)
|
||||
case item.ItemResource.Workflow != nil:
|
||||
return plantUMLResourceLine("Workflow", varName, item.ItemResource.Workflow)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
+899
-52
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
w := &workspace.Workspace{
|
||||
Active: active,
|
||||
ResourceSet: resources.ResourceSet{
|
||||
WorkspaceResourceSet: resources.WorkspaceResourceSet{
|
||||
Datas: workflow.Datas,
|
||||
Processings: workflow.Processings,
|
||||
Storages: workflow.Storages,
|
||||
@@ -173,7 +173,7 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
|
||||
a.workspaceAccessor.StoreOne(&workspace.Workspace{
|
||||
Active: active,
|
||||
AbstractObject: utils.AbstractObject{Name: workflow.Name + "_workspace"},
|
||||
ResourceSet: resources.ResourceSet{
|
||||
WorkspaceResourceSet: resources.WorkspaceResourceSet{
|
||||
Datas: workflow.Datas,
|
||||
Processings: workflow.Processings,
|
||||
Storages: workflow.Storages,
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
// - State : current lifecycle state of the step
|
||||
// - Deps : itemIDs that must reach StepSuccess before this step can start
|
||||
// - 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 {
|
||||
Name string `json:"name" bson:"name"`
|
||||
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.
|
||||
// Resources (infrastructure co-active while a step runs): Compute, Storage.
|
||||
srcIsStep := srcItem.Data != nil || srcItem.Processing != nil || srcItem.Workflow != nil || srcItem.NativeTool != nil
|
||||
dstIsStep := dstItem.Data != nil || dstItem.Processing != nil || dstItem.Workflow != nil || dstItem.NativeTool != nil
|
||||
srcIsResource := srcItem.Compute != nil || srcItem.Storage != nil
|
||||
dstIsResource := dstItem.Compute != nil || dstItem.Storage != nil
|
||||
srcIsStep := srcItem.ItemResource.Data != nil || srcItem.ItemResource.Processing != nil || srcItem.ItemResource.Workflow != nil || srcItem.ItemResource.NativeTool != nil
|
||||
dstIsStep := dstItem.ItemResource.Data != nil || dstItem.ItemResource.Processing != nil || dstItem.ItemResource.Workflow != nil || dstItem.ItemResource.NativeTool != nil
|
||||
srcIsResource := srcItem.ItemResource.Compute != nil || srcItem.ItemResource.Storage != nil
|
||||
dstIsResource := dstItem.ItemResource.Compute != nil || dstItem.ItemResource.Storage != nil
|
||||
|
||||
switch {
|
||||
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
|
||||
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.
|
||||
// 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.
|
||||
@@ -57,6 +61,12 @@ type WorkflowExecution struct {
|
||||
SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
|
||||
SelectedBuyings workflow.ConfigItem `json:"selected_buyings"`
|
||||
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
|
||||
// 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 {
|
||||
return r.IsDraft // only draft bookings can be deleted
|
||||
return true // only draft bookings can be deleted
|
||||
}
|
||||
|
||||
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 {
|
||||
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{
|
||||
AbstractObject: utils.AbstractObject{
|
||||
UUID: uuid.New().String(),
|
||||
@@ -293,6 +313,8 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
|
||||
ExecutionID: d.GetID(),
|
||||
ExpectedStartDate: start,
|
||||
ExpectedEndDate: endDate,
|
||||
BillingStrategy: d.SelectedBillingStrategy,
|
||||
PaymentType: paymentType,
|
||||
}
|
||||
items = append(items, bookingItem)
|
||||
d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append(
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
package workspace
|
||||
|
||||
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/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/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
|
||||
type Workspace struct {
|
||||
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)
|
||||
IsContextual bool `json:"is_contextual" bson:"is_contextual" default:"false"` // IsContextual is a flag that indicates if the workspace is contextual
|
||||
Active bool `json:"active" bson:"active" default:"false"` // Active is a flag that indicates if the workspace is active
|
||||
Shared string `json:"shared,omitempty" bson:"shared,omitempty"` // Shared is the ID of the shared workspace
|
||||
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
|
||||
resources.WorkspaceResourceSet // WorkspaceResourceSet persists both IDs and complete resource objects
|
||||
IsContextual bool `json:"is_contextual" bson:"is_contextual" default:"false"`
|
||||
Active bool `json:"active" bson:"active" default:"false"`
|
||||
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 {
|
||||
return NewAccessor(request) // Create a new instance of the accessor
|
||||
return NewAccessor(request)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -7,10 +7,60 @@ import (
|
||||
"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/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/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
|
||||
type workspaceMongoAccessor struct {
|
||||
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) {
|
||||
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
|
||||
d.(*Workspace).Fill(a.GetRequest())
|
||||
return d, 200, nil
|
||||
w := d.(*Workspace)
|
||||
w.Fill(a.GetRequest())
|
||||
a.applyTrustAndClean(w)
|
||||
return w, 200, nil
|
||||
}, a)
|
||||
}
|
||||
|
||||
func (a *workspaceMongoAccessor) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
|
||||
return utils.GenericLoadAll[*Workspace](func(d utils.DBObject) utils.ShallowDBObject {
|
||||
d.(*Workspace).Fill(a.GetRequest())
|
||||
return d
|
||||
w := d.(*Workspace)
|
||||
w.Fill(a.GetRequest())
|
||||
a.applyTrustAndClean(w)
|
||||
return w
|
||||
}, isDraft, a, offset, limit)
|
||||
}
|
||||
|
||||
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 {
|
||||
d.(*Workspace).Fill(a.GetRequest())
|
||||
return d
|
||||
w := d.(*Workspace)
|
||||
w.Fill(a.GetRequest())
|
||||
a.applyTrustAndClean(w)
|
||||
return w
|
||||
}, 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
|
||||
*/
|
||||
|
||||
+71
-2
@@ -36,6 +36,11 @@ const (
|
||||
SERVICE_RESOURCE
|
||||
DYNAMIC_RESOURCE
|
||||
LIVE_SERVICE
|
||||
PAYMENT
|
||||
REFUND
|
||||
DISCOUNT
|
||||
SUBSCRIPTION
|
||||
POLICY
|
||||
)
|
||||
|
||||
var NOAPI = func() string {
|
||||
@@ -96,6 +101,11 @@ var InnerDefaultAPI = [...]func() string{
|
||||
CATALOGAPI,
|
||||
CATALOGAPI,
|
||||
DATACENTERAPI,
|
||||
NOAPI,
|
||||
NOAPI,
|
||||
NOAPI,
|
||||
NOAPI,
|
||||
PEERSAPI,
|
||||
}
|
||||
|
||||
// Bind the standard data name to the data type
|
||||
@@ -126,6 +136,11 @@ var Str = [...]string{
|
||||
"service_resource",
|
||||
"dynamic_resource",
|
||||
"live_service",
|
||||
"payment",
|
||||
"refund",
|
||||
"discount",
|
||||
"subscription",
|
||||
"policy",
|
||||
}
|
||||
|
||||
func FromString(comp string) int {
|
||||
@@ -161,7 +176,8 @@ func DataTypeList() []DataType {
|
||||
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE,
|
||||
WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY,
|
||||
ORDER, PURCHASE_RESOURCE,
|
||||
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 {
|
||||
@@ -191,6 +207,44 @@ const (
|
||||
// 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.
|
||||
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 {
|
||||
@@ -223,6 +277,16 @@ func GetActionString(ss string) PubSubAction {
|
||||
return PB_OBSERVE_CLOSE
|
||||
case "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:
|
||||
return NONE
|
||||
}
|
||||
@@ -245,7 +309,12 @@ var path = []string{
|
||||
"none", // 12 NONE
|
||||
"observe", // 13 PB_OBSERVE
|
||||
"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 {
|
||||
|
||||
@@ -599,6 +599,16 @@ func (k *KubernetesService) CreateSecret(context context.Context, minioId string
|
||||
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.
|
||||
// Static provisioning (no StorageClass) avoids the WaitForFirstConsumer deadlock
|
||||
// with Admiralty virtual nodes — the PVC binds immediately.
|
||||
|
||||
+18
-1
@@ -32,6 +32,7 @@ var meths = []string{"remove execution", "create execution", "planner execution"
|
||||
"considers event", "admiralty config event", "minio config event", "pvc config event",
|
||||
"workflow started event", "workflow step done event", "workflow done event",
|
||||
"peer behavior event", "peer observe response event", "peer observe event",
|
||||
"source presign event", "org partner event", "verify resource event",
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -78,6 +79,21 @@ const (
|
||||
// or stop observing a remote peer. Payload contains the target peer_id and a
|
||||
// boolean close flag.
|
||||
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 {
|
||||
@@ -90,7 +106,8 @@ func NameToMethod(name string) NATSMethod {
|
||||
CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT, CONFIRM_EVENT,
|
||||
CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT, PVC_CONFIG_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)) {
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user