Compare commits

..

58 Commits

Author SHA1 Message Date
pb
e95d1aa53b implemented StoreOne for orders 2025-06-27 17:33:39 +02:00
pb
1ab2bd2153 commented the condition that prevents the booking if pricing is not selected to allow dev while the feature is not implemented 2025-06-27 17:10:32 +02:00
pb
d35ad440fa removed the 'validate:required' from PricedItem in Booking because we don't have implemented the error handling nor the feature yet 2025-06-27 16:21:54 +02:00
mr
d58dc56024 corrected live accessor 2025-06-26 16:20:41 +02:00
mr
34b7cdcf06 corrected live accessor 2025-06-26 15:59:21 +02:00
mr
af0d7807bc corrected live accessor 2025-06-26 15:57:49 +02:00
mr
e600fedcab access to live storage 2025-06-24 12:22:27 +02:00
mr
147c7bc3a1 access to live storage 2025-06-24 11:58:52 +02:00
mr
3fdf5c3ebf Live Structure 2025-06-24 11:32:07 +02:00
mr
cd177bd779 Live Structure 2025-06-24 11:29:04 +02:00
mr
2c8dcbe93d wait for NATS 2025-06-24 08:49:53 +02:00
mr
e84d262f38 add sets up 2025-06-20 12:11:13 +02:00
mr
29b192211d add sets up 2025-06-20 12:10:36 +02:00
mr
583ca2fbac add sets up 2025-06-20 10:50:01 +02:00
mr
82d25b0bee add sets up 2025-06-20 10:48:08 +02:00
mr
181b3249b8 add sets up 2025-06-20 10:47:33 +02:00
mr
8b38249df7 add sets up 2025-06-20 09:27:55 +02:00
mr
01af8237db add sets up 2025-06-20 09:22:09 +02:00
mr
2f4884c655 add sets up 2025-06-20 09:07:53 +02:00
mr
c9ee2a1d24 add sets up 2025-06-20 09:05:44 +02:00
mr
8d5ba6a5e4 add sets up 2025-06-20 08:34:33 +02:00
mr
d3cfe019e3 add sets up 2025-06-20 08:10:52 +02:00
mr
4c2ecd3f41 add sets up 2025-06-20 07:53:32 +02:00
mr
d8ccdec501 add sets up 2025-06-20 07:51:32 +02:00
mr
938f9f1326 set up 2025-06-19 08:11:11 +02:00
mr
29bc21735d setup 2025-06-18 07:58:40 +02:00
mr
ec7a7e4746 draft of compute units catalog 2025-06-17 16:42:21 +02:00
mr
0b0952b28c draft of compute units catalog 2025-06-17 16:14:44 +02:00
mr
9e52663261 draft of compute units catalog 2025-06-17 15:35:02 +02:00
mr
8f2adb76e4 add draft compute units 2025-06-17 15:06:33 +02:00
mr
0d6c329477 draft of compute units catalog 2025-06-17 14:57:36 +02:00
mr
1c751f7253 add draft compute units 2025-06-17 14:51:41 +02:00
mr
2264d22c69 draft of compute units catalog 2025-06-17 14:21:37 +02:00
mr
9fe72ea96e draft tests 2025-06-16 13:48:32 +02:00
mr
48299810e0 draft test 2025-06-16 11:24:39 +02:00
mr
2a0ab8e549 access to workflow IsDeps func 2025-06-12 10:47:38 +02:00
mr
23a9d648d2 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main
merge oclib
2025-06-12 10:42:38 +02:00
mr
a3029fa3f9 refactor addition in oclib for better use 2025-06-12 10:42:05 +02:00
pb
387785b40c reimported logs without import cycle 2025-06-04 10:33:00 +02:00
pb
03dea55131 added another error log when the status is dead 2025-06-03 10:00:47 +02:00
pb
7b8aa989f6 added a log when an API is not reachable 2025-06-02 18:22:08 +02:00
pb
6ab6383144 corrected an use of the original http caller instead of the deep copy 2025-05-27 18:07:00 +02:00
pb
690d60f9d6 corrected the getBooking function parameters 2025-05-27 16:13:46 +02:00
pb
da0de80afd Booking check and booking post have been transformed in goroutine to improve performance when booking several execution with cron expressions 2025-05-27 15:38:24 +02:00
pb
cd7ae788b1 didn't put the blocking loop in the right place for post booking 2025-05-27 12:06:10 +02:00
pb
0d96cc53bf transformed the loop that posted the booking on oc-datacenter to a threaded operation where each call is done in a goroutine 2025-05-27 11:58:55 +02:00
pb
66fc3c5b35 added the passing of the request.Caller's URL to the deep copy 2025-05-27 11:34:44 +02:00
pb
5ab3eb8a38 forgot to pass the mutex as pointer and unlock it 2025-05-27 11:17:10 +02:00
pb
fec23b4acd modified HTTP caller to have a DeepCopy() method in order to parallelize calls without race conditions 2025-05-27 11:08:35 +02:00
pb
901622fee0 logging on the booking uuid before the post booking 2025-05-27 09:51:41 +02:00
pb
527e622774 correct the error channel 2025-05-26 19:21:28 +02:00
pb
7223b79fe8 correct the error channel 2025-05-26 19:16:39 +02:00
pb
1ade41aeae moved the code that execute the booking into a separated function so that it can be launched as goroutine and parallelize get booking$ 2025-05-26 19:05:17 +02:00
pb
58dc579255 added debug logging 2025-05-26 18:30:56 +02:00
pb
370dac201b In CheckBooking mooved the loop on bookings outside of the loop of execs, which seems to repeat the Peer execution on booking an exponential number of time 2025-05-26 17:55:45 +02:00
pb
2a763006db counting round in exec 2025-05-26 17:41:26 +02:00
pb
522c66653b Added logging for debug 2025-05-26 17:22:09 +02:00
pb
b57f050b81 increased the limit of returns by Mongo find() 2025-05-22 14:41:38 +02:00
68 changed files with 3690 additions and 607 deletions

View File

@ -287,7 +287,7 @@ func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.C
return nil, 503, err
}
opts := options.Find()
opts.SetLimit(100)
opts.SetLimit(1000)
targetDBCollection := CollectionMap[collection_name]
orList := bson.A{}
andList := bson.A{}

View File

@ -15,11 +15,14 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
@ -51,6 +54,9 @@ const (
RULE = tools.RULE
BOOKING = tools.BOOKING
ORDER = tools.ORDER
LIVE_DATACENTER = tools.LIVE_DATACENTER
LIVE_STORAGE = tools.LIVE_STORAGE
PURCHASE_RESOURCE = tools.PURCHASE_RESOURCE
)
// will turn into standards api hostnames
@ -256,12 +262,11 @@ func (r *Request) Schedule(wfID string, scheduler *workflow_execution.WorkflowSc
if err != nil {
return nil, err
}
fmt.Println("BAM", ws)
return ws, nil
}
func (r *Request) CheckBooking(wfID string, start string, end string, durationInS float64, cron string) bool {
ok, _, _, _, err := workflow_execution.NewScheduler(start, end, durationInS, cron).CheckBooking(wfID, &tools.APIRequest{
ok, _, _, _, _, err := workflow_execution.NewScheduler(start, end, durationInS, cron).GetBuyAndBook(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
@ -274,26 +279,14 @@ func (r *Request) CheckBooking(wfID string, start string, end string, durationIn
return ok
}
func (r *Request) DraftOrder(scheduler *workflow_execution.WorkflowSchedule) (*order.Order, error) {
o := &order.Order{}
if err := o.DraftOrder(scheduler, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}); err != nil {
return nil, err
}
return o, nil
}
func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.WorkflowSchedule) error {
return o.Pay(scheduler, &tools.APIRequest{
/*return o.Pay(scheduler, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
})*/
return nil
}
/*
@ -576,3 +569,31 @@ func (l *LibData) ToOrder() *order.Order {
}
return nil
}
func (l *LibData) ToLiveDatacenter() *live.LiveDatacenter {
if l.Data.GetAccessor(nil).GetType() == tools.LIVE_DATACENTER {
return l.Data.(*live.LiveDatacenter)
}
return nil
}
func (l *LibData) ToLiveStorage() *live.LiveStorage {
if l.Data.GetAccessor(nil).GetType() == tools.LIVE_STORAGE {
return l.Data.(*live.LiveStorage)
}
return nil
}
func (l *LibData) ToBookings() *booking.Booking {
if l.Data.GetAccessor(nil).GetType() == tools.BOOKING {
return l.Data.(*booking.Booking)
}
return nil
}
func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
if l.Data.GetAccessor(nil).GetType() == tools.PURCHASE_RESOURCE {
return l.Data.(*purchase_resource.PurchaseResource)
}
return nil
}

3
go.mod Normal file → Executable file
View File

@ -10,12 +10,13 @@ require (
github.com/nats-io/nats.go v1.37.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
)
require (

4
go.sum Normal file → Executable file
View File

@ -106,9 +106,13 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=

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

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

View File

@ -0,0 +1,63 @@
package bill
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 billMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the billMongoAccessor
func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
return &billMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.LIVE_DATACENTER.String()), // Create a logger with the data type
Request: request,
Type: tools.LIVE_DATACENTER,
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *billMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *billMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
// should verify if a source is existing...
return utils.GenericUpdateOne(set, id, a, &Bill{})
}
func (a *billMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Bill), a)
}
func (a *billMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Bill), a)
}
func (a *billMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Bill](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *billMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Bill](a.getExec(), isDraft, a)
}
func (a *billMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Bill](filters, search, (&Bill{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *billMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

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

View File

@ -5,6 +5,8 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"go.mongodb.org/mongo-driver/bson/primitive"
@ -14,14 +16,19 @@ import (
* Booking is a struct that represents a booking
*/
type Booking struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
DestPeerID string `json:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer
WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`
State enum.BookingStatus `json:"state,omitempty" bson:"state,omitempty" validate:"required"` // State is the state of the booking
ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
PricedItem pricing.PricedItemITF `json:"priced_item,omitempty" bson:"priced_item,omitempty"` // We need to add the validate:"required" tag once the pricing feature is implemented, removed to avoid handling the error
ResumeMetrics map[string]map[string]models.MetricResume `json:"resume_metrics,omitempty" bson:"resume_metrics,omitempty"`
ExecutionMetrics map[string][]models.MetricsSnapshot `json:"metrics,omitempty" bson:"metrics,omitempty"`
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
DestPeerID string `json:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer
WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`
State enum.BookingStatus `json:"state,omitempty" bson:"state,omitempty" validate:"required"` // State is the state of the booking
ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking
RealStartDate *time.Time `json:"real_start_date,omitempty" bson:"real_start_date,omitempty"` // RealStartDate is the real start date of the booking
RealEndDate *time.Time `json:"real_end_date,omitempty" bson:"real_end_date,omitempty"` // RealEndDate is the real end date of the booking
@ -30,6 +37,33 @@ type Booking struct {
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty" validate:"required"` // could be a Compute or a Storage
}
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
m := map[string]map[string]models.MetricResume{}
for instance, snapshot := range b.ExecutionMetrics {
m[instance] = map[string]models.MetricResume{}
for _, metric := range snapshot {
for _, mm := range metric.Metrics {
if resume, ok := m[instance][mm.Name]; !ok {
m[instance][mm.Name] = models.MetricResume{
Delta: 0,
LastValue: mm.Value,
}
} else {
delta := resume.LastValue - mm.Value
if delta == 0 {
resume.Delta = delta
} else {
resume.Delta = (resume.Delta + delta) / 2
}
resume.LastValue = mm.Value
m[instance][mm.Name] = resume
}
}
}
}
return m
}
// CheckBooking checks if a booking is possible on a specific compute resource
func (wfa *Booking) Check(id string, start time.Time, end *time.Time, parrallelAllowed int) (bool, error) {
// check if

View File

@ -11,13 +11,13 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
type bookingMongoAccessor struct {
type BookingMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
return &bookingMongoAccessor{
// New creates a new instance of the BookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
return &BookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type
Request: request,
@ -29,11 +29,11 @@ func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *bookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if set.(*Booking).State == 0 {
return nil, 400, errors.New("state is required")
}
@ -41,15 +41,15 @@ func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.D
return utils.GenericUpdateOne(realSet, id, a, &Booking{})
}
func (a *bookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *bookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now()
now = now.Add(time.Second * -60)
@ -67,15 +67,15 @@ func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
}, a)
}
func (a *bookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *BookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Booking](a.getExec(), isDraft, a)
}
func (a *bookingMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *BookingMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Booking](filters, search, (&Booking{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *bookingMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
func (a *BookingMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now()
now = now.Add(time.Second * -60)

View File

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

View File

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

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

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

@ -9,8 +9,10 @@ import (
type PricedItemITF interface {
GetID() string
GetType() tools.DataType
IsPurchased() bool
IsPurchasable() bool
IsBooked() bool
GetCreatorID() string
SelectPricing() PricingProfileITF
GetLocationStart() *time.Time
SetLocationStart(start time.Time)
SetLocationEnd(end time.Time)

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

@ -5,9 +5,11 @@ import (
)
type PricingProfileITF interface {
GetPrice(quantity float64, val float64, start time.Time, end time.Time, params ...string) (float64, error)
IsPurchased() bool
IsBooked() bool
IsPurchasable() bool
GetPurchase() BuyingStrategy
GetOverrideStrategyValue() int
GetPrice(quantity float64, val float64, start time.Time, end time.Time, params ...string) (float64, error)
}
type RefundType int

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

@ -7,20 +7,65 @@ import (
"time"
)
type BillingStrategy int // BAM BAM
// should except... on
const (
BILL_ONCE BillingStrategy = iota // is a permanent buying ( predictible )
BILL_PER_WEEK
BILL_PER_MONTH
BILL_PER_YEAR
)
func (t BillingStrategy) IsBillingStrategyAllowed(bs int) (BillingStrategy, bool) {
switch t {
case BILL_ONCE:
return BILL_ONCE, bs == 0
case BILL_PER_WEEK:
case BILL_PER_MONTH:
case BILL_PER_YEAR:
return t, bs != 0
}
return t, false
}
func (t BillingStrategy) String() string {
return [...]string{"BILL_ONCE", "BILL_PER_WEEK", "BILL_PER_MONTH", "BILL_PER_YEAR"}[t]
}
func BillingStrategyList() []BillingStrategy {
return []BillingStrategy{BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR}
}
type BuyingStrategy int
// should except... on
const (
UNLIMITED BuyingStrategy = iota
SUBSCRIPTION
PAY_PER_USE
PERMANENT BuyingStrategy = iota // is a permanent buying ( predictible )
UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible )
SUBSCRIPTION // a defined subscription ( predictible )
// PAY_PER_USE // per request. ( unpredictible )
)
func (t BuyingStrategy) String() string {
return [...]string{"UNLIMITED", "SUBSCRIPTION", "PAY PER USE"}[t]
return [...]string{"PERMANENT", "UNDEFINED_SUBSCRIPTION", "SUBSCRIPTION"}[t]
}
func (t BuyingStrategy) IsBillingStrategyAllowed(bs BillingStrategy) (BillingStrategy, bool) {
switch t {
case PERMANENT:
return BILL_ONCE, bs == BILL_ONCE
case UNDEFINED_SUBSCRIPTION:
return BILL_PER_MONTH, bs != BILL_ONCE
case SUBSCRIPTION:
/*case PAY_PER_USE:
return bs, true*/
}
return bs, false
}
func BuyingStrategyList() []BuyingStrategy {
return []BuyingStrategy{UNLIMITED, SUBSCRIPTION, PAY_PER_USE}
return []BuyingStrategy{PERMANENT, UNDEFINED_SUBSCRIPTION, SUBSCRIPTION}
}
type Strategy interface {
@ -40,6 +85,10 @@ const (
PER_MONTH
)
func IsTimeStrategy(i int) bool {
return len(TimePricingStrategyList()) < i
}
func (t TimePricingStrategy) String() string {
return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t]
}
@ -101,6 +150,7 @@ func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInS
return 0, errors.New("pricing strategy not found")
}
// may suppress in pricing strategy -> to set in map
type PricingStrategy[T Strategy] struct {
Price float64 `json:"price" bson:"price" default:"0"` // Price is the Price of the pricing
Currency string `json:"currency" bson:"currency" default:"USD"` // Currency is the currency of the pricing
@ -112,7 +162,7 @@ type PricingStrategy[T Strategy] struct {
func (p PricingStrategy[T]) GetPrice(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time) (float64, error) {
if p.BuyingStrategy == SUBSCRIPTION {
return BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
} else if p.BuyingStrategy == UNLIMITED {
} else if p.BuyingStrategy == PERMANENT {
return p.Price, nil
}
return p.Price * float64(amountOfData), nil

View File

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

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

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

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

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

View File

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

View File

@ -0,0 +1,117 @@
package live
import (
"encoding/json"
"errors"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type computeUnitsMongoAccessor[T LiveInterface] struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the computeUnitsMongoAccessor
func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *computeUnitsMongoAccessor[T] {
return &computeUnitsMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
Type: t,
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *computeUnitsMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *computeUnitsMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
// should verify if a source is existing...
return utils.GenericUpdateOne(set, id, a, &LiveDatacenter{})
}
func (a *computeUnitsMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*LiveDatacenter), a)
}
func (a *computeUnitsMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
// is a publisher... that become a resources.
if data.IsDrafted() {
return nil, 422, errors.New("can't publish a drafted compute units")
}
live := data.(T)
if live.GetMonitorPath() == "" || live.GetID() != "" {
return nil, 422, errors.New("publishing is only allowed is it can be monitored and be accessible")
}
if res, code, err := a.LoadOne(live.GetID()); err != nil {
return nil, code, err
} else {
live = res.(T)
}
resAccess := live.GetResourceAccessor(a.Request)
instance := live.GetResourceInstance()
b, _ := json.Marshal(live)
json.Unmarshal(b, instance)
if len(live.GetResourcesID()) > 0 {
for _, r := range live.GetResourcesID() {
// TODO dependent of a existing resource
res, code, err := resAccess.LoadOne(r)
if err == nil {
return nil, code, err
}
existingResource := live.GetResource()
b, _ := json.Marshal(res)
json.Unmarshal(b, existingResource)
live.SetResourceInstance(existingResource, instance)
resAccess.UpdateOne(existingResource, existingResource.GetID())
}
if live.GetID() != "" {
return a.LoadOne(live.GetID())
} else {
return a.StoreOne(live)
}
} else {
r := live.GetResource()
b, _ := json.Marshal(live)
json.Unmarshal(b, &r)
live.SetResourceInstance(r, instance)
res, code, err := resAccess.StoreOne(r)
if err != nil {
return nil, code, err
}
live.SetResourcesID(res.GetID())
if live.GetID() != "" {
return a.UpdateOne(live, live.GetID())
} else {
return a.StoreOne(live)
}
}
}
func (a *computeUnitsMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *computeUnitsMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](a.getExec(), isDraft, a)
}
func (a *computeUnitsMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*LiveDatacenter](filters, search, (&LiveDatacenter{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *computeUnitsMongoAccessor[T]) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

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

View File

@ -2,6 +2,8 @@ package models
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/bill"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/tools"
@ -21,7 +23,7 @@ import (
This package contains the models used in the application
It's used to create the models dynamically
*/
var models = map[string]func() utils.DBObject{
var ModelsCatalog = map[string]func() utils.DBObject{
tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} },
tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} },
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
@ -38,13 +40,16 @@ var models = map[string]func() utils.DBObject{
tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} },
tools.ORDER.String(): func() utils.DBObject { return &order.Order{} },
tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} },
tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} },
tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
tools.BILL.String(): func() utils.DBObject { return &bill.Bill{} },
}
// Model returns the model object based on the model type
func Model(model int) utils.DBObject {
log := logs.GetLogger()
if _, ok := models[tools.FromInt(model)]; ok {
return models[tools.FromInt(model)]()
if _, ok := ModelsCatalog[tools.FromInt(model)]; ok {
return ModelsCatalog[tools.FromInt(model)]()
}
log.Error().Msg("Can't find model " + tools.FromInt(model) + ".")
return nil
@ -53,7 +58,7 @@ func Model(model int) utils.DBObject {
// GetModelsNames returns the names of the models
func GetModelsNames() []string {
names := []string{}
for name := range models {
for name := range ModelsCatalog {
names = append(names, name)
}
return names

View File

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

View File

@ -36,7 +36,7 @@ func (a *orderMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBO
}
func (a *orderMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("Not implemented")
return utils.GenericStoreOne(data,a)
}
func (a *orderMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {

View File

@ -0,0 +1 @@
package tests

View File

@ -77,7 +77,7 @@ func (p *Peer) IsMySelf() (bool, string) {
}
// LaunchPeerExecution launches an execution on a peer
func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (map[string]interface{}, error) {
p.UUID = peerID
return cache.LaunchPeerExecution(peerID, dataID, dt, method, body, caller) // Launch the execution on the peer through the cache
}

View File

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

View File

@ -11,13 +11,13 @@ import (
type peerMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
overrideAuth bool
OverrideAuth bool
}
// New creates a new instance of the peerMongoAccessor
func NewShallowAccessor() *peerMongoAccessor {
return &peerMongoAccessor{
overrideAuth: true,
OverrideAuth: true,
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Type: tools.PEER,
@ -27,7 +27,7 @@ func NewShallowAccessor() *peerMongoAccessor {
func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
return &peerMongoAccessor{
overrideAuth: false,
OverrideAuth: false,
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Request: request,
@ -37,7 +37,7 @@ func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
}
func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool {
return !wfa.overrideAuth
return !wfa.OverrideAuth
}
/*
@ -73,12 +73,12 @@ func (wfa *peerMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, in
}
func (wfa *peerMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Peer](filters, search, wfa.getDefaultFilter(search),
return utils.GenericSearch[*Peer](filters, search, wfa.GetDefaultFilter(search),
func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (a *peerMongoAccessor) getDefaultFilter(search string) *dbs.Filters {
func (a *peerMongoAccessor) GetDefaultFilter(search string) *dbs.Filters {
if i, err := strconv.Atoi(search); err == nil {
return &dbs.Filters{
Or: map[string][]dbs.Filter{ // search by name if no filters are provided

View File

@ -0,0 +1,100 @@
package peer_test
import (
"encoding/json"
"testing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockHTTPCaller mocks tools.HTTPCaller
type MockHTTPCaller struct {
mock.Mock
URLS map[tools.DataType]map[tools.METHOD]string
}
func (c *MockHTTPCaller) GetUrls() map[tools.DataType]map[tools.METHOD]string {
return c.URLS
}
func (m *MockHTTPCaller) CallPost(url, token string, body interface{}, types ...string) ([]byte, error) {
args := m.Called(url, token, body)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPCaller) CallGet(url, token string, types ...string) ([]byte, error) {
args := m.Called(url, token)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPCaller) CallDelete(url, token string) ([]byte, error) {
args := m.Called(url, token)
return args.Get(0).([]byte), args.Error(1)
}
func TestLaunchPeerExecution_PeerNotReachable(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{
URLS: map[tools.DataType]map[tools.METHOD]string{
tools.PEER: {
tools.POST: "/execute/:id",
},
},
}
exec, err := cache.LaunchPeerExecution("peer-id", "data-id", tools.PEER, tools.POST, map[string]string{"a": "b"}, caller)
assert.Nil(t, exec)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not reachable")
}
func TestExecSuccess(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
response := map[string]interface{}{"result": "ok"}
data, _ := json.Marshal(response)
caller.On("CallPost", url, "", mock.Anything).Return(data, nil)
_, err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.NoError(t, err)
caller.AssertExpectations(t)
}
func TestExecReturnsErrorField(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
response := map[string]interface{}{"error": "something failed"}
data, _ := json.Marshal(response)
caller.On("CallPost", url, "", mock.Anything).Return(data, nil)
_, err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.Error(t, err)
assert.Equal(t, "something failed", err.Error())
}
func TestExecInvalidJSON(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
caller.On("CallPost", url, "", mock.Anything).Return([]byte("{invalid json}"), nil)
_, err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid character")
}
type mockAccessor struct {
loadOne func(string) (interface{}, int, error)
updateOne func(interface{}, string) error
}
func (m *mockAccessor) LoadOne(id string) (interface{}, int, error) {
return m.loadOne(id)
}
func (m *mockAccessor) UpdateOne(i interface{}, id string) error {
return m.updateOne(i, id)
}

View File

@ -0,0 +1,127 @@
package peer_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/utils"
)
type MockAccessor struct {
mock.Mock
utils.AbstractAccessor
}
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func newTestPeer() *peer.Peer {
return &peer.Peer{
Url: "http://localhost",
WalletAddress: "0x123",
PublicKey: "pubkey",
State: peer.SELF,
}
}
func TestDeleteOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("DeleteOne", "id").Return(newTestPeer(), 200, nil)
obj, code, err := mockAcc.DeleteOne("id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, obj)
mockAcc.AssertExpectations(t)
}
func TestUpdateOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
peerObj := newTestPeer()
mockAcc.On("UpdateOne", peerObj, "id").Return(peerObj, 200, nil)
obj, code, err := mockAcc.UpdateOne(peerObj, "id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, peerObj, obj)
mockAcc.AssertExpectations(t)
}
func TestStoreOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
peerObj := newTestPeer()
mockAcc.On("StoreOne", peerObj).Return(peerObj, 200, nil)
obj, code, err := mockAcc.StoreOne(peerObj)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, peerObj, obj)
mockAcc.AssertExpectations(t)
}
func TestLoadOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("LoadOne", "test-id").Return(newTestPeer(), 200, nil)
obj, code, err := mockAcc.LoadOne("test-id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, obj)
mockAcc.AssertExpectations(t)
}
func TestLoadAll_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
expected := []utils.ShallowDBObject{newTestPeer()}
mockAcc.On("LoadAll", false).Return(expected, 200, nil)
objs, code, err := mockAcc.LoadAll(false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, expected, objs)
mockAcc.AssertExpectations(t)
}
func TestSearch_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
filters := &dbs.Filters{}
expected := []utils.ShallowDBObject{newTestPeer()}
mockAcc.On("Search", filters, "test", false).Return(expected, 200, nil)
objs, code, err := mockAcc.Search(filters, "test", false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, expected, objs)
mockAcc.AssertExpectations(t)
}

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

@ -30,8 +30,7 @@ func (r *ComputeResource) GetType() string {
return tools.COMPUTE_RESOURCE.String()
}
func (abs *ComputeResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *ComputeResource) ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
if t != tools.COMPUTE_RESOURCE {
return nil
}
@ -63,7 +62,11 @@ type ComputeResourceInstance struct {
type ComputeResourcePartnership struct {
ResourcePartnerShip[*ComputeResourcePricingProfile]
MaxAllowedCPUsCores map[string]int `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MinGaranteedCPUsCores map[string]float64 `json:"garanteed_cpus,omitempty" bson:"garanteed_cpus,omitempty"`
MinGaranteedGPUsMemoryGB map[string]float64 `json:"garanteed_gpus,omitempty" bson:"garanteed_gpus,omitempty"`
MinGaranteedRAMSize float64 `json:"garanteed_ram,omitempty" bson:"garanteed_ram,omitempty"`
MaxAllowedCPUsCores map[string]float64 `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MaxAllowedGPUsMemoryGB map[string]float64 `json:"allowed_gpus,omitempty" bson:"allowed_gpus,omitempty"`
MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"`
}
@ -76,8 +79,19 @@ type ComputeResourcePricingProfile struct {
RAMPrice float64 `json:"ram_price" bson:"ram_price" default:"-1"` // RAMPrice is the price of the RAM
}
func (p *ComputeResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *ComputeResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *ComputeResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
return p.Pricing.BuyingStrategy
}
func (p *ComputeResourcePricingProfile) IsBooked() bool {
if p.Pricing.BuyingStrategy == pricing.PERMANENT {
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
}
func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
@ -148,12 +162,9 @@ func (r *PricedComputeResource) GetPrice() (float64, error) {
r.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
price := float64(0)
for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} {
for model, amountOfData := range l {

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

@ -37,8 +37,7 @@ func (r *DataResource) GetType() string {
return tools.DATA_RESOURCE.String()
}
func (abs *DataResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *DataResource) ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
if t != tools.DATA_RESOURCE {
return nil
}
@ -82,7 +81,7 @@ type DataResourcePartnership struct {
type DataResourcePricingStrategy int
const (
PER_DOWNLOAD DataResourcePricingStrategy = iota
PER_DOWNLOAD DataResourcePricingStrategy = iota + 6
PER_TB_DOWNLOADED
PER_GB_DOWNLOADED
PER_MB_DOWNLOADED
@ -137,8 +136,17 @@ func (p *DataResourcePricingProfile) GetPrice(amountOfData float64, explicitDura
return p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
}
func (p *DataResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *DataResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
return p.Pricing.BuyingStrategy
}
func (p *DataResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *DataResourcePricingProfile) IsBooked() bool {
// TODO WHAT ABOUT PAY PER USE... it's a complicate CASE
return p.Pricing.BuyingStrategy != pricing.PERMANENT
}
type PricedDataResource struct {
@ -161,12 +169,9 @@ func (r *PricedDataResource) GetPrice() (float64, error) {
r.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
var err error
amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 {

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

@ -11,7 +11,7 @@ type ResourceInterface interface {
Trim()
ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF
GetType() string
GetSelectedInstance() utils.DBObject
GetSelectedInstance() ResourceInstanceITF
ClearEnv() utils.DBObject
SetAllowedInstances(request *tools.APIRequest)
}
@ -22,13 +22,17 @@ type ResourceInstanceITF interface {
GetName() string
StoreDraftDefault()
ClearEnv()
GetProfile() pricing.PricingProfileITF
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string)
ClearPeerGroups()
GetSelectedPartnership(peerID string, groups []string) ResourcePartnerITF
GetPartnerships(peerID string, groups []string) []ResourcePartnerITF
}
type ResourcePartnerITF interface {
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() map[string][]string
ClearPeerGroups()
GetProfile(buying int, strategy int) pricing.PricingProfileITF
}

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

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

@ -10,17 +10,20 @@ import (
)
type PricedResource struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
PricingProfiles []pricing.PricingProfileITF `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
SelectedPricing *pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
SelectedPricing pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
}
func (abs *PricedResource) SelectPricing() pricing.PricingProfileITF {
return abs.SelectedPricing
}
func (abs *PricedResource) GetID() string {
@ -35,11 +38,18 @@ func (abs *PricedResource) GetCreatorID() string {
return abs.CreatorID
}
func (abs *PricedResource) IsPurchased() bool {
func (abs *PricedResource) IsPurchasable() bool {
if abs.SelectedPricing == nil {
return false
}
return (*abs.SelectedPricing).IsPurchased()
return (abs.SelectedPricing).IsPurchasable()
}
func (abs *PricedResource) IsBooked() bool {
if abs.SelectedPricing == nil {
return false
}
return (abs.SelectedPricing).IsBooked()
}
func (abs *PricedResource) GetLocationEnd() *time.Time {
@ -83,11 +93,8 @@ func (r *PricedResource) GetPrice() (float64, error) {
r.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd)
}

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

@ -77,8 +77,16 @@ type ProcessingResourcePricingProfile struct {
pricing.AccessPricingProfile[pricing.TimePricingStrategy] // AccessPricingProfile is the pricing profile of a data it means that we can access the data for an amount of time
}
func (p *ProcessingResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *ProcessingResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *ProcessingResourcePricingProfile) IsBooked() bool {
return p.Pricing.BuyingStrategy != pricing.PERMANENT
}
func (p *ProcessingResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
return p.Pricing.BuyingStrategy
}
func (p *ProcessingResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {

View File

@ -3,15 +3,19 @@ package purchase_resource
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 PurchaseResource struct {
utils.AbstractObject
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"`
DestPeerID string
PricedItem pricing.PricedItemITF `json:"priced_item,omitempty" bson:"priced_item,omitempty" validate:"required"`
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"`
}
func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor {

View File

@ -9,13 +9,13 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
type purchaseResourceMongoAccessor struct {
type PurchaseResourceMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
return &purchaseResourceMongoAccessor{
func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
return &PurchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type
Request: request,
@ -27,23 +27,23 @@ func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *purchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *purchaseResourceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &PurchaseResource{})
}
func (a *purchaseResourceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *purchaseResourceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *purchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*PurchaseResource](id, func(d utils.DBObject) (utils.DBObject, int, error) {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(id, a)
@ -53,15 +53,15 @@ func (a *purchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int,
}, a)
}
func (a *purchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*PurchaseResource](a.getExec(), isDraft, a)
}
func (a *purchaseResourceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *PurchaseResourceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*PurchaseResource](filters, search, (&PurchaseResource{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *purchaseResourceMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
func (a *PurchaseResourceMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(d.GetID(), a)

View File

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

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

@ -24,7 +24,7 @@ type AbstractResource struct {
SelectedInstanceIndex *int `json:"selected_instance_index,omitempty" bson:"selected_instance_index,omitempty"` // SelectedInstance is the selected instance
}
func (r *AbstractResource) GetSelectedInstance() utils.DBObject {
func (r *AbstractResource) GetSelectedInstance() ResourceInstanceITF {
return nil
}
@ -52,21 +52,27 @@ type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
Instances []T `json:"instances,omitempty" bson:"instances,omitempty"` // Bill is the bill of the resource // Bill is the bill of the resource
}
func (abs *AbstractInstanciatedResource[T]) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *AbstractInstanciatedResource[T]) ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
instances := map[string]string{}
profiles := []pricing.PricingProfileITF{}
for _, instance := range abs.Instances {
instances[instance.GetID()] = instance.GetName()
profiles = instance.GetPricingsProfiles(request.PeerID, request.Groups)
}
var profile pricing.PricingProfileITF
if t := abs.GetSelectedInstance(); t != nil {
profile = t.GetProfile()
}
if profile == nil && len(profiles) > 0 {
profile = profiles[0]
}
return &PricedResource{
Name: abs.Name,
Logo: abs.Logo,
ResourceID: abs.UUID,
ResourceType: t,
InstancesRefs: instances,
PricingProfiles: profiles,
SelectedPricing: profile,
CreatorID: abs.CreatorID,
}
}
@ -78,7 +84,7 @@ func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
return abs
}
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() utils.DBObject {
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() ResourceInstanceITF {
if r.SelectedInstanceIndex != nil && len(r.Instances) > *r.SelectedInstanceIndex {
return r.Instances[*r.SelectedInstanceIndex]
}
@ -92,7 +98,7 @@ func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.A
if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" {
return
}
abs.Instances = verifyAuthAction[T](abs.Instances, request)
abs.Instances = VerifyAuthAction[T](abs.Instances, request)
}
func (d *AbstractInstanciatedResource[T]) Trim() {
@ -105,10 +111,10 @@ func (d *AbstractInstanciatedResource[T]) Trim() {
}
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(request *tools.APIRequest) bool {
return len(verifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request)
return len(VerifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request)
}
func verifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T {
func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T {
instances := []T{}
for _, instance := range baseInstance {
_, peerGroups := instance.GetPeerGroups()
@ -137,20 +143,18 @@ type GeoPoint struct {
Longitude float64 `json:"longitude,omitempty" bson:"longitude,omitempty"`
}
type Credentials struct {
Login string `json:"login,omitempty" bson:"login,omitempty"`
Pass string `json:"password,omitempty" bson:"password,omitempty"`
}
type ResourceInstance[T ResourcePartnerITF] struct {
utils.AbstractObject
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
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"`
SelectedPartnershipIndex int `json:"selected_partnership_index,omitempty" bson:"selected_partnership_index,omitempty"`
SelectedBuyingStrategy int `json:"selected_buying_strategy,omitempty" bson:"selected_buying_strategy,omitempty"`
SelectedStrategy int `json:"selected_strategy,omitempty" bson:"selected_strategy,omitempty"`
Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
}
func (ri *ResourceInstance[T]) ClearEnv() {
@ -159,6 +163,35 @@ func (ri *ResourceInstance[T]) ClearEnv() {
ri.Outputs = []models.Param{}
}
func (ri *ResourceInstance[T]) GetProfile() pricing.PricingProfileITF {
if len(ri.Partnerships) > ri.SelectedPartnershipIndex {
prts := ri.Partnerships[ri.SelectedPartnershipIndex]
return prts.GetProfile(ri.SelectedBuyingStrategy, ri.SelectedBuyingStrategy)
}
return nil
}
func (ri *ResourceInstance[T]) GetSelectedPartnership(peerID string, groups []string) ResourcePartnerITF {
if len(ri.Partnerships) > ri.SelectedPartnershipIndex {
return ri.Partnerships[ri.SelectedPartnershipIndex]
}
return nil
}
func (ri *ResourceInstance[T]) GetPartnerships(peerID string, groups []string) []ResourcePartnerITF {
partners := []ResourcePartnerITF{}
for _, p := range ri.Partnerships {
if p.GetPeerGroups()[peerID] != nil {
for _, g := range p.GetPeerGroups()[peerID] {
if slices.Contains(groups, g) {
partners = append(partners, p)
}
}
}
}
return partners
}
func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
pricings := []pricing.PricingProfileITF{}
for _, p := range ri.Partnerships {
@ -186,29 +219,40 @@ func (ri *ResourceInstance[T]) ClearPeerGroups() {
type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"`
PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
PricingProfiles []T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
PricingProfiles map[int]map[int]T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
// to upgrade pricing profiles. to be a map BuyingStrategy, map of Strategy
}
func (ri *ResourcePartnerShip[T]) GetProfile(buying int, strategy int) pricing.PricingProfileITF {
if strat, ok := ri.PricingProfiles[buying]; ok {
if profile, ok := strat[strategy]; ok {
return profile
}
}
return nil
}
/*
Le pricing doit être selectionné lors d'un scheduling...
le type de paiement défini le type de stratégie de paiement
note : il faut rajouté - une notion de facturation
Une order est l'ensemble de la commande... un booking une réservation, une purchase un acte d'achat.
Une bill (facture) représente alors... l'emission d'une facture à un instant T en but d'être honorée, envoyée ... etc.
*/
func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
profiles := []pricing.PricingProfileITF{}
if ri.PeerGroups[peerID] != nil {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
}
if slices.Contains(groups, "*") {
if ri.PeerGroups[peerID] == nil {
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) || slices.Contains(groups, "*") {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
for _, i := range ri {
profiles = append(profiles, i)
}
}
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
}
return profiles
}
}
}
return profiles
}

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

@ -1,6 +1,7 @@
package resources
import (
"errors"
"slices"
"cloud.o-forge.io/core/oc-lib/dbs"
@ -9,17 +10,21 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
type resourceMongoAccessor[T ResourceInterface] struct {
type ResourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject
}
// New creates a new instance of the computeMongoAccessor
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *resourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE, tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE}, t) {
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *ResourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{
tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE,
tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE,
tools.DATA_RESOURCE,
}, t) {
return nil
}
return &resourceMongoAccessor[T]{
return &ResourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
@ -32,39 +37,48 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
/*
* Nothing special here, just the basic CRUD operations
*/
func (dca *resourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, dca)
}
func (dca *resourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't update a non existing computing units resource not reported onto compute units catalog")
}
set.(T).Trim()
return utils.GenericUpdateOne(set, id, dca, dca.generateData())
}
func (dca *resourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
}
data.(T).Trim()
return utils.GenericStoreOne(data, dca)
}
func (dca *resourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't copy/publish a non existing computing units resource not reported onto compute units catalog")
}
return dca.StoreOne(data)
}
func (dca *resourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) {
d.(T).SetAllowedInstances(dca.Request)
return d, 200, nil
}, dca)
}
func (wfa *resourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request)
return d
}, isDraft, wfa)
}
func (wfa *resourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
if filters == nil && search == "*" {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request)
@ -78,7 +92,7 @@ func (wfa *resourceMongoAccessor[T]) Search(filters *dbs.Filters, search string,
}, isDraft, wfa)
}
func (abs *resourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters {
func (abs *ResourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters {
return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided
"abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},

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

@ -30,8 +30,7 @@ func (r *StorageResource) GetType() string {
return tools.STORAGE_RESOURCE.String()
}
func (abs *StorageResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *StorageResource) ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
if t != tools.STORAGE_RESOURCE {
return nil
}
@ -44,8 +43,8 @@ func (abs *StorageResource) ConvertToPricedResource(
type StorageResourceInstance struct {
ResourceInstance[*StorageResourcePartnership]
Credentials *Credentials `json:"credentials,omitempty" bson:"credentials,omitempty"`
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
Path string `bson:"path,omitempty" json:"path,omitempty"` // Path is the store folders in the source
Local bool `bson:"local" json:"local"`
SecurityLevel string `bson:"security_level,omitempty" json:"security_level,omitempty"`
SizeType enum.StorageSize `bson:"size_type" json:"size_type" default:"0"` // SizeType is the type of the storage size
@ -56,7 +55,6 @@ type StorageResourceInstance struct {
}
func (ri *StorageResourceInstance) ClearEnv() {
ri.Credentials = nil
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
@ -105,7 +103,7 @@ func (t PrivilegeStoragePricingStrategy) String() string {
type StorageResourcePricingStrategy int
const (
PER_DATA_STORED StorageResourcePricingStrategy = iota
PER_DATA_STORED StorageResourcePricingStrategy = iota + 6
PER_TB_STORED
PER_GB_STORED
PER_MB_STORED
@ -152,8 +150,19 @@ type StorageResourcePricingProfile struct {
pricing.ExploitPricingProfile[StorageResourcePricingStrategy] // ExploitPricingProfile is the pricing profile of a storage it means that we exploit the resource for an amount of continuous time
}
func (p *StorageResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *StorageResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
return p.Pricing.BuyingStrategy
}
func (p *StorageResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy != pricing.UNDEFINED_SUBSCRIPTION
}
func (p *StorageResourcePricingProfile) IsBooked() bool {
if p.Pricing.BuyingStrategy == pricing.PERMANENT {
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
}
func (p *StorageResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
@ -180,12 +189,9 @@ func (r *PricedStorageResource) GetPrice() (float64, error) {
r.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID)
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
var err error
amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 {

View File

@ -0,0 +1,108 @@
package resources_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestComputeResource_GetType(t *testing.T) {
r := &resources.ComputeResource{}
assert.Equal(t, tools.COMPUTE_RESOURCE.String(), r.GetType())
}
func TestComputeResource_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
cr := &resources.ComputeResource{}
accessor := cr.GetAccessor(req)
assert.NotNil(t, accessor)
}
func TestComputeResource_ConvertToPricedResource(t *testing.T) {
req := &tools.APIRequest{}
cr := &resources.ComputeResource{}
cr.UUID = "comp123"
cr.AbstractInstanciatedResource.UUID = cr.UUID
result := cr.ConvertToPricedResource(tools.COMPUTE_RESOURCE, req)
assert.NotNil(t, result)
assert.IsType(t, &resources.PricedComputeResource{}, result)
}
func TestComputeResourcePricingProfile_GetPrice_CPUs(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
profile := resources.ComputeResourcePricingProfile{
CPUsPrices: map[string]float64{"Xeon": 2.0},
ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0},
},
},
}
price, err := profile.GetPrice(2, 3600, start, end, "cpus", "Xeon")
require.NoError(t, err)
assert.Greater(t, price, float64(0))
}
func TestComputeResourcePricingProfile_GetPrice_InvalidParams(t *testing.T) {
profile := resources.ComputeResourcePricingProfile{}
_, err := profile.GetPrice(1, 3600, time.Now(), time.Now())
assert.Error(t, err)
assert.Equal(t, "params must be set", err.Error())
}
func TestPricedComputeResource_GetPrice(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
r := resources.PricedComputeResource{
PricedResource: resources.PricedResource{
ResourceID: "comp456",
UsageStart: &start,
UsageEnd: &end,
ExplicitBookingDurationS: 3600,
},
CPUsLocated: map[string]float64{"Xeon": 2},
GPUsLocated: map[string]float64{"Tesla": 1},
RAMLocated: 4,
}
price, err := r.GetPrice()
require.NoError(t, err)
assert.Greater(t, price, float64(0))
}
func TestPricedComputeResource_GetPrice_MissingProfile(t *testing.T) {
r := resources.PricedComputeResource{
PricedResource: resources.PricedResource{
ResourceID: "comp789",
},
}
_, err := r.GetPrice()
require.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
}
func TestPricedComputeResource_FillWithDefaultProcessingUsage(t *testing.T) {
usage := &resources.ProcessingUsage{
CPUs: map[string]*models.CPU{"t": {Model: "Xeon", Cores: 4}},
GPUs: map[string]*models.GPU{"t1": {Model: "Tesla"}},
RAM: &models.RAM{SizeGb: 16},
}
r := &resources.PricedComputeResource{
CPUsLocated: make(map[string]float64),
GPUsLocated: make(map[string]float64),
RAMLocated: 0,
}
r.FillWithDefaultProcessingUsage(usage)
assert.Equal(t, float64(4), r.CPUsLocated["Xeon"])
assert.Equal(t, float64(1), r.GPUsLocated["Tesla"])
assert.Equal(t, float64(16), r.RAMLocated)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -16,7 +16,7 @@ type WorkflowResource struct {
}
func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ComputeResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
}
func (r *WorkflowResource) GetType() string {
@ -34,8 +34,7 @@ func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest) {
/* EMPTY */
}
func (w *WorkflowResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (w *WorkflowResource) ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
return &PricedResource{
Name: w.Name,
Logo: w.Logo,

View File

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

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

@ -43,6 +43,10 @@ func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor {
return nil
}
func (r *AbstractObject) SetID(id string) {
r.UUID = id
}
func (r *AbstractObject) GenerateID() {
if r.UUID == "" {
r.UUID = uuid.New().String()

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

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

@ -18,6 +18,7 @@ type ShallowDBObject interface {
// DBObject is an interface that defines the basic methods for a DBObject
type DBObject interface {
GenerateID()
SetID(id string)
GetID() string
GetName() string
IsDrafted() bool

View File

@ -0,0 +1,128 @@
package models_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestGenerateID(t *testing.T) {
ao := &utils.AbstractObject{}
ao.GenerateID()
assert.NotEmpty(t, ao.UUID)
_, err := uuid.Parse(ao.UUID)
assert.NoError(t, err)
}
func TestStoreDraftDefault(t *testing.T) {
ao := &utils.AbstractObject{IsDraft: true}
ao.StoreDraftDefault()
assert.False(t, ao.IsDraft)
}
func TestCanUpdate(t *testing.T) {
ao := &utils.AbstractObject{}
res, set := ao.CanUpdate(nil)
assert.True(t, res)
assert.Nil(t, set)
}
func TestCanDelete(t *testing.T) {
ao := &utils.AbstractObject{}
assert.True(t, ao.CanDelete())
}
func TestIsDrafted(t *testing.T) {
ao := &utils.AbstractObject{IsDraft: true}
assert.True(t, ao.IsDrafted())
}
func TestGetID(t *testing.T) {
u := uuid.New().String()
ao := &utils.AbstractObject{UUID: u}
assert.Equal(t, u, ao.GetID())
}
func TestGetName(t *testing.T) {
name := "MyObject"
ao := &utils.AbstractObject{Name: name}
assert.Equal(t, name, ao.GetName())
}
func TestGetCreatorID(t *testing.T) {
id := "creator-123"
ao := &utils.AbstractObject{CreatorID: id}
assert.Equal(t, id, ao.GetCreatorID())
}
func TestUpToDate_CreateFalse(t *testing.T) {
ao := &utils.AbstractObject{}
now := time.Now()
time.Sleep(time.Millisecond) // ensure time difference
ao.UpToDate("user123", "peer456", false)
assert.WithinDuration(t, now, ao.UpdateDate, time.Second)
assert.Equal(t, "peer456", ao.UpdaterID)
assert.Equal(t, "user123", ao.UserUpdaterID)
assert.True(t, ao.CreationDate.IsZero())
}
func TestUpToDate_CreateTrue(t *testing.T) {
ao := &utils.AbstractObject{}
now := time.Now()
time.Sleep(time.Millisecond)
ao.UpToDate("user123", "peer456", true)
assert.WithinDuration(t, now, ao.UpdateDate, time.Second)
assert.WithinDuration(t, now, ao.CreationDate, time.Second)
assert.Equal(t, "peer456", ao.UpdaterID)
assert.Equal(t, "peer456", ao.CreatorID)
assert.Equal(t, "user123", ao.UserUpdaterID)
assert.Equal(t, "user123", ao.UserCreatorID)
}
func TestVerifyAuth(t *testing.T) {
request := &tools.APIRequest{PeerID: "peer123"}
ao := &utils.AbstractObject{CreatorID: "peer123"}
assert.True(t, ao.VerifyAuth(request))
ao = &utils.AbstractObject{AccessMode: utils.Public}
assert.True(t, ao.VerifyAuth(nil))
ao = &utils.AbstractObject{AccessMode: utils.Private, CreatorID: "peer123"}
request = &tools.APIRequest{PeerID: "wrong"}
assert.False(t, ao.VerifyAuth(request))
}
func TestGetObjectFilters(t *testing.T) {
ao := &utils.AbstractObject{}
f := ao.GetObjectFilters("*")
assert.NotNil(t, f)
assert.Contains(t, f.Or, "abstractobject.name")
assert.Equal(t, dbs.LIKE.String(), f.Or["abstractobject.name"][0].Operator)
}
func TestDeserialize(t *testing.T) {
ao := &utils.AbstractObject{}
input := map[string]interface{}{"name": "test", "id": uuid.New().String()}
res := ao.Deserialize(input, &utils.AbstractObject{})
assert.NotNil(t, res)
}
func TestSerialize(t *testing.T) {
ao := &utils.AbstractObject{Name: "test", UUID: uuid.New().String()}
m := ao.Serialize(ao)
assert.Equal(t, "test", m["name"])
}
func TestAbstractAccessorMethods(t *testing.T) {
r := &utils.AbstractAccessor{Request: &tools.APIRequest{Username: "alice", PeerID: "peer1", Groups: []string{"dev"}}}
assert.True(t, r.ShouldVerifyAuth())
assert.Equal(t, "alice", r.GetUser())
assert.Equal(t, "peer1", r.GetPeerID())
assert.Equal(t, []string{"dev"}, r.GetGroups())
assert.Equal(t, r.Request.Caller, r.GetCaller())
}

View File

@ -0,0 +1,168 @@
package models_test
import (
"errors"
"testing"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// --- Mock Definitions ---
type MockDBObject struct {
mock.Mock
}
func (m *MockAccessor) GetLogger() *zerolog.Logger {
return nil
}
func (m *MockAccessor) GetGroups() []string {
return []string{}
}
func (m *MockAccessor) GetCaller() *tools.HTTPCaller {
return nil
}
func (m *MockDBObject) GenerateID() { m.Called() }
func (m *MockDBObject) StoreDraftDefault() { m.Called() }
func (m *MockDBObject) UpToDate(user, peer string, create bool) {
m.Called(user, peer, create)
}
func (m *MockDBObject) VerifyAuth(req *tools.APIRequest) bool {
args := m.Called(req)
return args.Bool(0)
}
func (m *MockDBObject) CanDelete() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockDBObject) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
args := m.Called(set)
return args.Bool(0), args.Get(1).(utils.DBObject)
}
func (m *MockDBObject) IsDrafted() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockDBObject) Serialize(obj utils.DBObject) map[string]interface{} {
args := m.Called(obj)
return args.Get(0).(map[string]interface{})
}
func (m *MockDBObject) Deserialize(mdata map[string]interface{}, obj utils.DBObject) utils.DBObject {
args := m.Called(mdata, obj)
return args.Get(0).(utils.DBObject)
}
func (m *MockDBObject) GetID() string {
args := m.Called()
return args.String(0)
}
func (m *MockDBObject) GetName() string {
args := m.Called()
return args.String(0)
}
type MockAccessor struct {
mock.Mock
}
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) ShouldVerifyAuth() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockAccessor) GetRequest() *tools.APIRequest {
args := m.Called()
return args.Get(0).(*tools.APIRequest)
}
func (m *MockAccessor) GetType() tools.DataType {
args := m.Called()
return args.Get(0).(tools.DataType)
}
func (m *MockAccessor) Search(filters *dbs.Filters, s string, d bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, s, d)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) GetUser() string {
args := m.Called()
return args.String(0)
}
func (m *MockAccessor) GetPeerID() string {
args := m.Called()
return args.String(0)
}
// --- Test Cases ---
func TestVerifyAccess_Authorized(t *testing.T) {
mockObj := new(MockDBObject)
mockAcc := new(MockAccessor)
req := &tools.APIRequest{PeerID: "peer"}
mockAcc.On("LoadOne", "123").Return(mockObj, 200, nil)
mockAcc.On("ShouldVerifyAuth").Return(true)
mockObj.On("VerifyAuth", req).Return(true)
mockAcc.On("GetRequest").Return(req)
err := utils.VerifyAccess(mockAcc, "123")
assert.NoError(t, err)
}
func TestVerifyAccess_Unauthorized(t *testing.T) {
mockObj := new(MockDBObject)
mockAcc := new(MockAccessor)
req := &tools.APIRequest{PeerID: "peer"}
mockAcc.On("LoadOne", "123").Return(mockObj, 200, nil)
mockAcc.On("ShouldVerifyAuth").Return(true)
mockObj.On("VerifyAuth", req).Return(false)
mockAcc.On("GetRequest").Return(req)
err := utils.VerifyAccess(mockAcc, "123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not allowed")
}
func TestVerifyAccess_LoadError(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("LoadOne", "bad-id").Return(nil, 404, errors.New("not found"))
err := utils.VerifyAccess(mockAcc, "bad-id")
assert.Error(t, err)
assert.Equal(t, "not found", err.Error())
}

View File

@ -31,6 +31,43 @@ func (d *Workflow) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
type Deps struct {
Source string
Dest string
}
func (w *Workflow) IsDependancy(id string) []Deps {
dependancyOfIDs := []Deps{}
for _, link := range w.Graph.Links {
if _, ok := w.Graph.Items[link.Destination.ID]; !ok {
continue
}
source := w.Graph.Items[link.Destination.ID].Processing
if id == link.Source.ID && source != nil {
dependancyOfIDs = append(dependancyOfIDs, Deps{Source: source.GetName(), Dest: link.Destination.ID})
}
sourceWF := w.Graph.Items[link.Destination.ID].Workflow
if id == link.Source.ID && sourceWF != nil {
dependancyOfIDs = append(dependancyOfIDs, Deps{Source: sourceWF.GetName(), Dest: link.Destination.ID})
}
}
return dependancyOfIDs
}
func (w *Workflow) GetDependencies(id string) (dependencies []Deps) {
for _, link := range w.Graph.Links {
if _, ok := w.Graph.Items[link.Source.ID]; !ok {
continue
}
source := w.Graph.Items[link.Source.ID].Processing
if id == link.Destination.ID && source != nil {
dependencies = append(dependencies, Deps{Source: source.GetName(), Dest: link.Source.ID})
continue
}
}
return
}
func (w *Workflow) GetGraphItems(f func(item graph.GraphItem) bool) (list_datas []graph.GraphItem) {
for _, item := range w.Graph.Items {
if f(item) {
@ -40,7 +77,8 @@ func (w *Workflow) GetGraphItems(f func(item graph.GraphItem) bool) (list_datas
return
}
func (w *Workflow) GetPricedItem(f func(item graph.GraphItem) bool, request *tools.APIRequest) map[string]pricing.PricedItemITF {
func (w *Workflow) GetPricedItem(
f func(item graph.GraphItem) bool, request *tools.APIRequest, buyingStrategy int, pricingStrategy int) map[string]pricing.PricedItemITF {
list_datas := map[string]pricing.PricedItemITF{}
for _, item := range w.Graph.Items {
if f(item) {
@ -140,8 +178,8 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, request *tools.APIR
if err != nil {
return 0, priceds, nil, err
}
if _, priceds, err = plan[resources.ResourceInterface](tools.DATA_RESOURCE, wf, priceds, request, wf.Graph.IsData,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
if _, priceds, err = plan[resources.ResourceInterface](tools.DATA_RESOURCE, wf, priceds, request,
wf.Graph.IsData, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
return start, 0
}, func(started time.Time, duration float64) *time.Time {
return end
@ -149,8 +187,8 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, request *tools.APIR
return 0, priceds, nil, err
}
for k, f := range map[tools.DataType]func(graph.GraphItem) bool{tools.STORAGE_RESOURCE: wf.Graph.IsStorage, tools.COMPUTE_RESOURCE: wf.Graph.IsCompute} {
if _, priceds, err = plan[resources.ResourceInterface](k, wf, priceds, request, f,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
if _, priceds, err = plan[resources.ResourceInterface](k, wf, priceds, request,
f, func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64) {
nearestStart, longestDuration := wf.Graph.GetAverageTimeRelatedToProcessingActivity(start, ps, res, func(i graph.GraphItem) (r resources.ResourceInterface) {
if f(i) {
_, r = i.GetResource()
@ -189,7 +227,8 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, request *tools.APIR
return longest, priceds, wf, nil
}
func plan[T resources.ResourceInterface](dt tools.DataType, wf *Workflow, priceds map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest,
func plan[T resources.ResourceInterface](
dt tools.DataType, wf *Workflow, priceds map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest,
f func(graph.GraphItem) bool, start func(resources.ResourceInterface, pricing.PricedItemITF) (time.Time, float64), end func(time.Time, float64) *time.Time) ([]T, map[tools.DataType]map[string]pricing.PricedItemITF, error) {
resources := []T{}
for _, item := range wf.GetGraphItems(f) {
@ -201,6 +240,10 @@ func plan[T resources.ResourceInterface](dt tools.DataType, wf *Workflow, priced
return resources, priceds, errors.New("could not load the processing resource")
}
priced := realItem.ConvertToPricedResource(dt, request)
// Should be commented once the Pricing selection feature has been implemented, related to the commit d35ad440fa77763ec7f49ab34a85e47e75581b61
// if priced.SelectPricing() == nil {
// return resources, priceds, errors.New("no pricings are selected... can't proceed")
// }
started, duration := start(realItem, priced)
priced.SetLocationStart(started)
if duration >= 0 {

View File

@ -0,0 +1,149 @@
package workflow_execution_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockAccessor struct {
mock.Mock
}
func (m *MockAccessor) LoadOne(id string) (interface{}, int, error) {
args := m.Called(id)
return args.Get(0), args.Int(1), args.Error(2)
}
func TestNewScheduler_ValidInput(t *testing.T) {
s := "2025-06-16T15:00:00"
e := "2025-06-16T17:00:00"
dur := 7200.0
cronStr := "0 0 * * * *"
sched := workflow_execution.NewScheduler(s, e, dur, cronStr)
assert.NotNil(t, sched)
assert.Equal(t, dur, sched.DurationS)
assert.Equal(t, cronStr, sched.Cron)
}
func TestNewScheduler_InvalidStart(t *testing.T) {
s := "invalid"
e := "2025-06-16T17:00:00"
dur := 7200.0
cronStr := "0 0 * * * *"
sched := workflow_execution.NewScheduler(s, e, dur, cronStr)
assert.Nil(t, sched)
}
func TestNewScheduler_InvalidEnd(t *testing.T) {
s := "2025-06-16T15:00:00"
e := "invalid"
dur := 7200.0
cronStr := "0 0 * * * *"
sched := workflow_execution.NewScheduler(s, e, dur, cronStr)
assert.NotNil(t, sched)
assert.Nil(t, sched.End)
}
func TestGetDates_NoCron(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
s := &workflow_execution.WorkflowSchedule{
Start: start,
End: &end,
}
schedule, err := s.GetDates()
assert.NoError(t, err)
assert.Len(t, schedule, 1)
assert.Equal(t, start, schedule[0].Start)
assert.Equal(t, end, *schedule[0].End)
}
func TestGetDates_InvalidCron(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
s := &workflow_execution.WorkflowSchedule{
Start: start,
End: &end,
Cron: "bad cron",
}
_, err := s.GetDates()
assert.Error(t, err)
}
func TestGetDates_ValidCron(t *testing.T) {
start := time.Now()
end := start.Add(10 * time.Minute)
s := &workflow_execution.WorkflowSchedule{
Start: start,
End: &end,
DurationS: 60,
Cron: "0 */2 * * * *",
}
dates, err := s.GetDates()
assert.NoError(t, err)
assert.Greater(t, len(dates), 0)
}
func TestGetExecutions_Success(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
ws := &workflow_execution.WorkflowSchedule{
UUID: uuid.New().String(),
Start: start,
End: &end,
}
wf := &workflow.Workflow{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: "TestWorkflow",
},
}
execs, err := ws.GetExecutions(wf)
assert.NoError(t, err)
assert.Greater(t, len(execs), 0)
assert.Equal(t, wf.UUID, execs[0].WorkflowID)
assert.Equal(t, ws.UUID, execs[0].ExecutionsID)
assert.Equal(t, enum.DRAFT, execs[0].State)
}
func TestSchedules_NoRequest(t *testing.T) {
ws := &workflow_execution.WorkflowSchedule{}
ws, wf, execs, err := ws.Schedules("someID", nil)
assert.Error(t, err)
assert.Nil(t, wf)
assert.Len(t, execs, 0)
assert.Equal(t, ws, ws)
}
// Additional test stubs to be completed with gomock usage for:
// - CheckBooking
// - BookExecs
// - getBooking
// - Schedules (success path)
// - Planify mocking in CheckBooking
// - Peer interaction in BookExecs
// - Caller deep copy errors in getCallerCopy
// Will be continued...

View File

@ -0,0 +1,154 @@
package workflow_execution_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return nil, args.Int(1), args.Error(2)
}
func (m *MockAccessor) Search(filters interface{}, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func TestStoreDraftDefault(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.StoreDraftDefault()
assert.False(t, exec.IsDraft)
assert.Equal(t, enum.SCHEDULED, exec.State)
}
func TestCanUpdate_StateChange(t *testing.T) {
existing := &workflow_execution.WorkflowExecution{State: enum.DRAFT}
newExec := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
canUpdate, updated := existing.CanUpdate(newExec)
assert.True(t, canUpdate)
assert.Equal(t, enum.SCHEDULED, updated.(*workflow_execution.WorkflowExecution).State)
}
func TestCanUpdate_SameState_Draft(t *testing.T) {
existing := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}, State: enum.DRAFT}
newExec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}, State: enum.DRAFT}
canUpdate, _ := existing.CanUpdate(newExec)
assert.False(t, canUpdate)
}
func TestCanDelete_TrueIfDraft(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}}
assert.True(t, exec.CanDelete())
}
func TestCanDelete_FalseIfNotDraft(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: false}}
assert.False(t, exec.CanDelete())
}
func TestEquals_True(t *testing.T) {
d := time.Now()
exec1 := &workflow_execution.WorkflowExecution{ExecDate: d, WorkflowID: "123"}
exec2 := &workflow_execution.WorkflowExecution{ExecDate: d, WorkflowID: "123"}
assert.True(t, exec1.Equals(exec2))
}
func TestEquals_False(t *testing.T) {
exec1 := &workflow_execution.WorkflowExecution{ExecDate: time.Now(), WorkflowID: "abc"}
exec2 := &workflow_execution.WorkflowExecution{ExecDate: time.Now().Add(time.Hour), WorkflowID: "def"}
assert.False(t, exec1.Equals(exec2))
}
func TestArgoStatusToState_Success(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.ArgoStatusToState("succeeded")
assert.Equal(t, enum.SUCCESS, exec.State)
}
func TestArgoStatusToState_DefaultToFailure(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.ArgoStatusToState("unknown")
assert.Equal(t, enum.FAILURE, exec.State)
}
func TestGenerateID_AssignsUUID(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.GenerateID()
assert.NotEmpty(t, exec.UUID)
}
func TestGetName_ReturnsCorrectFormat(t *testing.T) {
time := time.Now()
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{UUID: "abc"}, ExecDate: time}
assert.Contains(t, exec.GetName(), "abc")
assert.Contains(t, exec.GetName(), time.String())
}
func TestVerifyAuth_AlwaysTrue(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
assert.True(t, exec.VerifyAuth(nil))
}
func TestUpdateOne_RejectsZeroState(t *testing.T) {
accessor := &workflow_execution.WorkflowExecutionMongoAccessor{}
set := &workflow_execution.WorkflowExecution{State: 0}
_, code, err := accessor.UpdateOne(set, "someID")
assert.Equal(t, 400, code)
assert.Error(t, err)
}
func TestLoadOne_DraftExpired_ShouldDelete(t *testing.T) {
// Normally would mock time.Now and delete call; for now we test structure
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
exec := &workflow_execution.WorkflowExecution{
ExecDate: time.Now().Add(-2 * time.Minute),
State: enum.DRAFT,
AbstractObject: utils.AbstractObject{UUID: "to-delete"},
}
_, _, _ = accessor.LoadOne(exec.GetID())
// No panic = good enough placeholder
}
func TestLoadOne_ScheduledExpired_ShouldUpdateToForgotten(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
exec := &workflow_execution.WorkflowExecution{
ExecDate: time.Now().Add(-2 * time.Minute),
State: enum.SCHEDULED,
AbstractObject: utils.AbstractObject{UUID: "to-forget"},
}
_, _, _ = accessor.LoadOne(exec.GetID())
}
func TestDeleteOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.DeleteOne("someID")
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestStoreOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.StoreOne(nil)
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestCopyOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.CopyOne(nil)
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestGetExecFilters_BasicPattern(t *testing.T) {
a := workflow_execution.NewAccessor(&tools.APIRequest{})
filters := a.GetExecFilters("foo")
assert.Contains(t, filters.Or["abstractobject.name"][0].Value, "foo")
}

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

@ -8,6 +8,7 @@ import (
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
@ -21,6 +22,7 @@ import (
*/
type WorkflowExecution struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
PeerBuyByGraph map[string]map[string][]string `json:"peer_buy_by_graph,omitempty" bson:"peer_buy_by_graph,omitempty"` // BookByResource is a map of the resource id and the list of the booking id
PeerBookByGraph map[string]map[string][]string `json:"peer_book_by_graph,omitempty" bson:"peer_book_by_graph,omitempty"` // BookByResource is a map of the resource id and the list of the booking id
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty"`
ExecDate time.Time `json:"execution_date,omitempty" bson:"execution_date,omitempty" validate:"required"` // ExecDate is the execution date of the workflow, is required
@ -109,17 +111,70 @@ func (d *WorkflowExecution) VerifyAuth(request *tools.APIRequest) bool {
return true
}
/*
booking is an activity reserved for not a long time investment.
... purchase is dependant of a one time buying.
use of a datacenter or storage can't be buy for permanent access.
*/
func (d *WorkflowExecution) Buy(bs pricing.BillingStrategy, executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource {
purchases := d.buyEach(bs, executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])
purchases = append(purchases, d.buyEach(bs, executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
return purchases
}
func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID string, wfID string, dt tools.DataType, priceds map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource {
items := []*purchase_resource.PurchaseResource{}
for itemID, priced := range priceds {
if !priced.IsPurchasable() || bs != pricing.BILL_ONCE { // buy only that must be buy
continue
}
if d.PeerBuyByGraph == nil {
d.PeerBuyByGraph = map[string]map[string][]string{}
}
if d.PeerBuyByGraph[priced.GetCreatorID()] == nil {
d.PeerBuyByGraph[priced.GetCreatorID()] = map[string][]string{}
}
if d.PeerBuyByGraph[priced.GetCreatorID()][itemID] == nil {
d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = []string{}
}
start := d.ExecDate
if s := priced.GetLocationStart(); s != nil {
start = *s
}
end := start.Add(time.Duration(priced.GetExplicitDurationInS()) * time.Second)
bookingItem := &purchase_resource.PurchaseResource{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID,
},
PricedItem: priced,
ExecutionsID: executionsID,
DestPeerID: priced.GetCreatorID(),
ResourceID: priced.GetID(),
ResourceType: dt,
EndDate: &end,
}
items = append(items, bookingItem)
d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = append(
d.PeerBuyByGraph[priced.GetCreatorID()][itemID], bookingItem.GetID())
}
return items
}
func (d *WorkflowExecution) Book(executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*booking.Booking {
booking := d.bookEach(executionsID, wfID, tools.STORAGE_RESOURCE, priceds[tools.STORAGE_RESOURCE])
booking = append(booking, d.bookEach(executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])...)
booking = append(booking,d.bookEach(executionsID, wfID, tools.COMPUTE_RESOURCE, priceds[tools.COMPUTE_RESOURCE])...)
booking = append(booking,d.bookEach(executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
booking = append(booking, d.bookEach(executionsID, wfID, tools.COMPUTE_RESOURCE, priceds[tools.COMPUTE_RESOURCE])...)
booking = append(booking, d.bookEach(executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
return booking
}
func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.DataType, priceds map[string]pricing.PricedItemITF) []*booking.Booking {
items := []*booking.Booking{}
for itemID, priced := range priceds {
if !priced.IsBooked() { // book only that must be booked
continue
}
if d.PeerBookByGraph == nil {
d.PeerBookByGraph = map[string]map[string][]string{}
}
@ -139,6 +194,7 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID,
},
PricedItem: priced,
ExecutionsID: executionsID,
State: enum.SCHEDULED,
ResourceID: priced.GetID(),

View File

@ -11,13 +11,13 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
type workflowExecutionMongoAccessor struct {
type WorkflowExecutionMongoAccessor struct {
utils.AbstractAccessor
shallow bool
}
func newShallowAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
return &workflowExecutionMongoAccessor{
func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{
shallow: true,
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
@ -27,8 +27,8 @@ func newShallowAccessor(request *tools.APIRequest) *workflowExecutionMongoAccess
}
}
func NewAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
return &workflowExecutionMongoAccessor{
func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{
shallow: false,
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
@ -38,11 +38,11 @@ func NewAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
}
}
func (wfa *workflowExecutionMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
func (wfa *WorkflowExecutionMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented")
}
func (wfa *workflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
func (wfa *WorkflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if set.(*WorkflowExecution).State == 0 {
return nil, 400, errors.New("state is required")
}
@ -50,15 +50,15 @@ func (wfa *workflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id stri
return utils.GenericUpdateOne(&realSet, id, wfa, &WorkflowExecution{})
}
func (wfa *workflowExecutionMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
func (wfa *WorkflowExecutionMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented")
}
func (wfa *workflowExecutionMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
func (wfa *WorkflowExecutionMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented")
}
func (a *workflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*WorkflowExecution](id, func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now()
now = now.Add(time.Second * -60)
@ -74,15 +74,15 @@ func (a *workflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int
}, a)
}
func (a *workflowExecutionMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *WorkflowExecutionMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*WorkflowExecution](a.getExec(), isDraft, a)
}
func (a *workflowExecutionMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *WorkflowExecutionMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*WorkflowExecution](filters, search, a.GetExecFilters(search), a.getExec(), isDraft, a)
}
func (a *workflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
func (a *WorkflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now()
now = now.Add(time.Second * -60)
@ -99,7 +99,7 @@ func (a *workflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.Sh
}
}
func (a *workflowExecutionMongoAccessor) GetExecFilters(search string) *dbs.Filters {
func (a *WorkflowExecutionMongoAccessor) GetExecFilters(search string) *dbs.Filters {
return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}},

177
models/workflow_execution/workflow_scheduler.go Normal file → Executable file
View File

@ -1,16 +1,19 @@
package workflow_execution
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/bill"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/tools"
@ -33,6 +36,8 @@ type WorkflowSchedule struct {
End *time.Time `json:"end,omitempty"` // End is the end time of the schedule, is required and must be greater than the Start time
DurationS float64 `json:"duration_s" default:"-1"` // End is the end time of the schedule
Cron string `json:"cron,omitempty"` // here the cron format : ss mm hh dd MM dw task
SelectedBillingStrategy pricing.BillingStrategy `json:"selected_billing_strategy"`
}
func NewScheduler(start string, end string, durationInS float64, cron string) *WorkflowSchedule {
@ -53,46 +58,106 @@ func NewScheduler(start string, end string, durationInS float64, cron string) *W
return ws
}
func (ws *WorkflowSchedule) CheckBooking(wfID string, request *tools.APIRequest) (bool, *workflow.Workflow, []*WorkflowExecution, []*booking.Booking, error) {
func (ws *WorkflowSchedule) GetBuyAndBook(wfID string, request *tools.APIRequest) (bool, *workflow.Workflow, []*WorkflowExecution, []*purchase_resource.PurchaseResource, []*booking.Booking, error) {
if request.Caller == nil && request.Caller.URLS == nil && request.Caller.URLS[tools.BOOKING] == nil || request.Caller.URLS[tools.BOOKING][tools.GET] == "" {
return false, nil, []*WorkflowExecution{}, []*booking.Booking{}, errors.New("no caller defined")
return false, nil, []*WorkflowExecution{}, []*purchase_resource.PurchaseResource{}, []*booking.Booking{}, errors.New("no caller defined")
}
access := workflow.NewAccessor(request)
res, code, err := access.LoadOne(wfID)
if code != 200 {
return false, nil, []*WorkflowExecution{}, []*booking.Booking{}, errors.New("could not load the workflow with id: " + err.Error())
return false, nil, []*WorkflowExecution{}, []*purchase_resource.PurchaseResource{}, []*booking.Booking{}, errors.New("could not load the workflow with id: " + err.Error())
}
wf := res.(*workflow.Workflow)
longest, priceds, wf, err := wf.Planify(ws.Start, ws.End, request)
if err != nil {
return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err
return false, wf, []*WorkflowExecution{}, []*purchase_resource.PurchaseResource{}, []*booking.Booking{}, err
}
ws.DurationS = longest
ws.Message = "We estimate that the workflow will start at " + ws.Start.String() + " and last " + fmt.Sprintf("%v", ws.DurationS) + " seconds."
if ws.End != nil && ws.Start.Add(time.Duration(longest)*time.Second).After(*ws.End) {
ws.Warning = "The workflow may be too long to be executed in the given time frame, we will try to book it anyway\n"
}
execs, err := ws.getExecutions(wf)
execs, err := ws.GetExecutions(wf)
if err != nil {
return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err
return false, wf, []*WorkflowExecution{}, []*purchase_resource.PurchaseResource{}, []*booking.Booking{}, err
}
purchased := []*purchase_resource.PurchaseResource{}
bookings := []*booking.Booking{}
for _, exec := range execs {
purchased = append(purchased, exec.Buy(ws.SelectedBillingStrategy, ws.UUID, wfID, priceds)...)
bookings = append(bookings, exec.Book(ws.UUID, wfID, priceds)...)
for _, b := range bookings {
meth := request.Caller.URLS[tools.BOOKING][tools.GET]
meth = strings.ReplaceAll(meth, ":id", b.ResourceID)
meth = strings.ReplaceAll(meth, ":start_date", b.ExpectedStartDate.Format("2006-01-02T15:04:05"))
meth = strings.ReplaceAll(meth, ":end_date", b.ExpectedEndDate.Format("2006-01-02T15:04:05"))
request.Caller.URLS[tools.BOOKING][tools.GET] = meth
_, err := (&peer.Peer{}).LaunchPeerExecution(b.DestPeerID, b.ResourceID, tools.BOOKING, tools.GET, nil, request.Caller)
if err != nil {
return false, wf, execs, bookings, err
}
}
}
return true, wf, execs, bookings, nil
errCh := make(chan error, len(bookings))
var m sync.Mutex
for _, b := range bookings {
go getBooking(b, request, errCh, &m)
}
for i := 0; i < len(bookings); i++ {
if err := <-errCh; err != nil {
return false, wf, execs, purchased, bookings, err
}
}
return true, wf, execs, purchased, bookings, nil
}
func (ws *WorkflowSchedule) GenerateOrder(purchases []*purchase_resource.PurchaseResource, bookings []*booking.Booking, request *tools.APIRequest) error {
newOrder := &order.Order{
AbstractObject: utils.AbstractObject{
Name: "order_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: true,
},
ExecutionsID: ws.UUID,
Purchases: purchases,
Bookings: bookings,
Status: enum.PENDING,
}
if res, _, err := order.NewAccessor(request).StoreOne(newOrder); err == nil {
if _, err := bill.DraftFirstBill(res.(*order.Order), request); err != nil {
return err
}
return nil
} else {
return err
}
}
func getBooking(b *booking.Booking, request *tools.APIRequest, errCh chan error, m *sync.Mutex) {
m.Lock()
c, err := getCallerCopy(request, errCh)
if err != nil {
errCh <- err
return
}
m.Unlock()
meth := c.URLS[tools.BOOKING][tools.GET]
meth = strings.ReplaceAll(meth, ":id", b.ResourceID)
meth = strings.ReplaceAll(meth, ":start_date", b.ExpectedStartDate.Format("2006-01-02T15:04:05"))
meth = strings.ReplaceAll(meth, ":end_date", b.ExpectedEndDate.Format("2006-01-02T15:04:05"))
c.URLS[tools.BOOKING][tools.GET] = meth
_, err = (&peer.Peer{}).LaunchPeerExecution(b.DestPeerID, b.ResourceID, tools.BOOKING, tools.GET, nil, &c)
if err != nil {
errCh <- err
return
}
errCh <- nil
}
func getCallerCopy(request *tools.APIRequest, errCh chan error) (tools.HTTPCaller, error) {
var c tools.HTTPCaller
err := request.Caller.DeepCopy(c)
if err != nil {
errCh <- err
return tools.HTTPCaller{}, nil
}
c.URLS = request.Caller.URLS
return c, err
}
func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*WorkflowSchedule, *workflow.Workflow, []*WorkflowExecution, error) {
@ -107,21 +172,41 @@ func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*
if _, ok := methods[tools.GET]; !ok {
return ws, nil, []*WorkflowExecution{}, errors.New("no path found")
}
ok, wf, executions, bookings, err := ws.CheckBooking(wfID, request)
ok, wf, executions, purchases, bookings, err := ws.GetBuyAndBook(wfID, request)
ws.WorkflowExecution = executions
if !ok || err != nil {
return ws, nil, executions, errors.New("could not book the workflow : " + fmt.Sprintf("%v", err))
}
ws.Workflow = wf
for _, booking := range bookings {
l := logs.GetLogger()
l.Info().Msg("Booking on " + booking.DestPeerID)
_, err := (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "",
tools.BOOKING, tools.POST, booking.Serialize(booking), request.Caller)
if err != nil {
var errCh = make(chan error, len(bookings))
var m sync.Mutex
for _, purchase := range purchases {
go ws.CallDatacenter(purchase, purchase.DestPeerID, tools.PURCHASE_RESOURCE, request, errCh, &m)
}
for i := 0; i < len(purchases); i++ {
if err := <-errCh; err != nil {
return ws, wf, executions, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err))
}
}
errCh = make(chan error, len(bookings))
for _, booking := range bookings {
go ws.CallDatacenter(booking, booking.DestPeerID, tools.BOOKING, request, errCh, &m)
}
for i := 0; i < len(bookings); i++ {
if err := <-errCh; err != nil {
return ws, wf, executions, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err))
}
}
if err := ws.GenerateOrder(purchases, bookings, request); err != nil {
return ws, wf, executions, err
}
fmt.Println("Schedules")
for _, exec := range executions {
err := exec.PurgeDraft(request)
@ -135,6 +220,24 @@ func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*
return ws, wf, executions, nil
}
func (ws *WorkflowSchedule) CallDatacenter(purchase utils.DBObject, destPeerID string, dt tools.DataType, request *tools.APIRequest, errCh chan error, m *sync.Mutex) {
m.Lock()
c, err := getCallerCopy(request, errCh)
if err != nil {
errCh <- err
return
}
m.Unlock()
if res, err := (&peer.Peer{}).LaunchPeerExecution(destPeerID, "", dt, tools.POST, purchase.Serialize(purchase), &c); err != nil {
errCh <- err
return
} else {
data := res["data"].(map[string]interface{})
purchase.SetID(fmt.Sprintf("%v", data["id"]))
}
errCh <- nil
}
/*
BOOKING IMPLIED TIME, not of subscription but of execution
so is processing time execution time applied on computes
@ -147,16 +250,13 @@ VERIFY THAT WE HANDLE DIFFERENCE BETWEEN LOCATION TIME && BOOKING
* getExecutions is a function that returns the executions of a workflow
* it returns an array of workflow_execution.WorkflowExecution
*/
func (ws *WorkflowSchedule) getExecutions(workflow *workflow.Workflow) ([]*WorkflowExecution, error) {
func (ws *WorkflowSchedule) GetExecutions(workflow *workflow.Workflow) ([]*WorkflowExecution, error) {
workflows_executions := []*WorkflowExecution{}
dates, err := ws.getDates()
dates, err := ws.GetDates()
if err != nil {
return workflows_executions, err
}
for _, date := range dates {
fmt.Println("============")
fmt.Println("Date : " + fmt.Sprint(date))
fmt.Println("============")
obj := &WorkflowExecution{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(), // set the uuid of the execution
@ -173,7 +273,7 @@ func (ws *WorkflowSchedule) getExecutions(workflow *workflow.Workflow) ([]*Workf
return workflows_executions, nil
}
func (ws *WorkflowSchedule) getDates() ([]Schedule, error) {
func (ws *WorkflowSchedule) GetDates() ([]Schedule, error) {
schedule := []Schedule{}
if len(ws.Cron) > 0 { // if cron is set then end date should be set
if ws.End == nil {
@ -193,17 +293,8 @@ func (ws *WorkflowSchedule) getDates() ([]Schedule, error) {
if err != nil {
return schedule, errors.New("Bad cron message: " + err.Error())
}
toto, _ := json.MarshalIndent(sched,"","")
fmt.Println(string(toto))
s := sched.Next(ws.Start)
fmt.Println("s.IsZero() : " + fmt.Sprint(s.IsZero()) + "s.Before(*ws.End) : " + fmt.Sprint(s.Before(*ws.End)) )
tata, _ := json.MarshalIndent(s,"","")
fmt.Println("s = " + string(tata) + " et ws.End = " + fmt.Sprint(ws.End))
// loop through the cron schedule to set the executions
for s := sched.Next(ws.Start); !s.IsZero() && s.Before(*ws.End); s = sched.Next(s) {
fmt.Println("next execution :" + fmt.Sprint(s))
e := s.Add(time.Duration(ws.DurationS) * time.Second)
schedule = append(schedule, Schedule{
Start: s,

View File

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

View File

@ -7,6 +7,7 @@ import (
"cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs"
beego "github.com/beego/beego/v2/server/web"
)
@ -109,14 +110,14 @@ func (a *API) SubscribeRouter(infos []*beego.ControllerInfo) {
}
}
}
nats.SetNATSPub("api", DISCOVERY, discovery)
go nats.SetNATSPub("api", DISCOVERY, discovery)
}
// CheckRemotePeer checks the state of a remote peer
func (a *API) CheckRemotePeer(url string) (State, map[string]int) {
// Check if the database is up
var resp APIStatusResponse
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
b, err := caller.CallPost(url, "", map[string]interface{}{}) // Call the status endpoint of the peer
if err != nil {
return DEAD, map[string]int{} // If the peer is not reachable, return dead
@ -135,6 +136,7 @@ func (a *API) CheckRemotePeer(url string) (State, map[string]int) {
// CheckRemoteAPIs checks the state of remote APIs from your proper OC
func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error) {
// Check if the database is up
l := logs.GetLogger()
new := map[string]string{}
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
code := 0
@ -145,6 +147,7 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
var resp APIStatusResponse
b, err := caller.CallGet("http://"+api.API()+":8080", "/oc/version/status") // Call the status endpoint of the remote API (standard OC status endpoint)
if err != nil {
l.Error().Msg(api.String() + " not reachable")
state = REDUCED_SERVICE // If a remote API is not reachable, return reduced service
continue
}
@ -161,6 +164,7 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
reachable = true // If the remote API is reachable, set reachable to true cause we are not dead
}
if !reachable {
l.Error().Msg("Peer check returned no answers")
state = DEAD // If no remote API is reachable, return dead, nobody is alive
}
if code > 0 {

View File

@ -26,6 +26,9 @@ const (
ADMIRALTY_SECRET
ADMIRALTY_KUBECONFIG
ADMIRALTY_NODES
LIVE_DATACENTER
LIVE_STORAGE
BILL
)
var NOAPI = ""
@ -35,11 +38,12 @@ var WORKFLOWAPI = "oc-workflow"
var WORKSPACEAPI = "oc-workspace"
var PEERSAPI = "oc-peer"
var DATACENTERAPI = "oc-datacenter"
var ADMIRALTY_SOURCEAPI = DATACENTERAPI+"/admiralty/source"
var ADMIRALTY_TARGETAPI = DATACENTERAPI+"/admiralty/target"
var ADMIRALTY_SECRETAPI = DATACENTERAPI+"/admiralty/secret"
var ADMIRALTY_KUBECONFIGAPI = DATACENTERAPI+"/admiralty/kubeconfig"
var ADMIRALTY_NODESAPI = DATACENTERAPI+"/admiralty/node"
var PURCHASEAPI = "oc-catalog/purchase"
var ADMIRALTY_SOURCEAPI = DATACENTERAPI + "/admiralty/source"
var ADMIRALTY_TARGETAPI = DATACENTERAPI + "/admiralty/target"
var ADMIRALTY_SECRETAPI = DATACENTERAPI + "/admiralty/secret"
var ADMIRALTY_KUBECONFIGAPI = DATACENTERAPI + "/admiralty/kubeconfig"
var ADMIRALTY_NODESAPI = DATACENTERAPI + "/admiralty/node"
// Bind the standard API name to the data type
var DefaultAPI = [...]string{
@ -59,12 +63,15 @@ var DefaultAPI = [...]string{
NOAPI,
NOAPI,
NOAPI,
NOAPI,
PURCHASEAPI,
ADMIRALTY_SOURCEAPI,
ADMIRALTY_TARGETAPI,
ADMIRALTY_SECRETAPI,
ADMIRALTY_KUBECONFIGAPI,
ADMIRALTY_NODESAPI,
DATACENTERAPI,
DATACENTERAPI,
NOAPI,
}
// Bind the standard data name to the data type
@ -91,6 +98,9 @@ var Str = [...]string{
"admiralty_secret",
"admiralty_kubeconfig",
"admiralty_node",
"live_datacenter",
"live_storage",
"bill",
}
func FromInt(i int) string {
@ -111,5 +121,8 @@ func (d DataType) EnumIndex() int {
}
func DataTypeList() []DataType {
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE, WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY, ORDER, PURCHASE_RESOURCE,ADMIRALTY_SOURCE,ADMIRALTY_TARGET,ADMIRALTY_SECRET,ADMIRALTY_KUBECONFIG,ADMIRALTY_NODES}
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE,
WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY,
ORDER, PURCHASE_RESOURCE, ADMIRALTY_SOURCE, ADMIRALTY_TARGET, ADMIRALTY_SECRET, ADMIRALTY_KUBECONFIG, ADMIRALTY_NODES,
LIVE_DATACENTER, LIVE_STORAGE, BILL}
}

View File

@ -3,6 +3,7 @@ package tools
import (
"encoding/json"
"strings"
"time"
"cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/logs"
@ -53,22 +54,25 @@ func (s *natsCaller) ListenNats(chanName string, exec func(msg map[string]interf
log.Error().Msg(" -> NATS_SERVER is not set")
return
}
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
log.Error().Msg(" -> Could not reach NATS server : " + err.Error())
return
}
ch := make(chan *nats.Msg, 64)
subs, err := nc.ChanSubscribe(chanName, ch)
if err != nil {
log.Error().Msg("Error listening to NATS : " + err.Error())
}
defer subs.Unsubscribe()
for {
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
time.Sleep(1 * time.Minute)
continue
}
ch := make(chan *nats.Msg, 64)
subs, err := nc.ChanSubscribe(chanName, ch)
if err != nil {
log.Error().Msg("Error listening to NATS : " + err.Error())
}
defer subs.Unsubscribe()
for msg := range ch {
map_mess := map[string]interface{}{}
json.Unmarshal(msg.Data, &map_mess)
exec(map_mess)
for msg := range ch {
map_mess := map[string]interface{}{}
json.Unmarshal(msg.Data, &map_mess)
exec(map_mess)
}
break
}
}
@ -77,18 +81,23 @@ func (o *natsCaller) SetNATSPub(dataName string, method NATSMethod, data interfa
if config.GetConfig().NATSUrl == "" {
return " -> NATS_SERVER is not set"
}
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
return " -> Could not reach NATS server : " + err.Error()
}
defer nc.Close()
js, err := json.Marshal(data)
if err != nil {
return " -> " + err.Error()
}
err = nc.Publish(method.GenerateKey(dataName), js) // Publish the message on the NATS server with a channel name based on the data name (or whatever start) and the method
if err != nil {
return " -> " + err.Error() // Return an error if the message could not be published
for {
nc, err := nats.Connect(config.GetConfig().NATSUrl)
if err != nil {
time.Sleep(1 * time.Minute)
continue
}
defer nc.Close()
js, err := json.Marshal(data)
if err != nil {
return " -> " + err.Error()
}
err = nc.Publish(method.GenerateKey(dataName), js) // Publish the message on the NATS server with a channel name based on the data name (or whatever start) and the method
if err != nil {
time.Sleep(1 * time.Minute)
continue
}
break
}
return ""
}

View File

@ -47,12 +47,19 @@ func ToMethod(str string) METHOD {
return GET
}
type HTTPCallerITF interface {
GetUrls() map[DataType]map[METHOD]string
CallGet(url string, subpath string, types ...string) ([]byte, error)
CallPost(url string, subpath string, body interface{}, types ...string) ([]byte, error)
CallDelete(url string, subpath string) ([]byte, error)
}
var HTTPCallerInstance = &HTTPCaller{} // Singleton instance of the HTTPCaller
type HTTPCaller struct {
URLS map[DataType]map[METHOD]string // Map of the different methods and their urls
Disabled bool // Disabled flag
LastResults map[string]interface{} // Used to store information regarding the last execution of a given method on a given data type
URLS map[DataType]map[METHOD]string // Map of the different methods and their urls
Disabled bool // Disabled flag
LastResults map[string]interface{} // Used to store information regarding the last execution of a given method on a given data type
}
// NewHTTPCaller creates a new instance of the HTTP Caller
@ -63,6 +70,20 @@ func NewHTTPCaller(urls map[DataType]map[METHOD]string) *HTTPCaller {
}
}
func (c *HTTPCaller) GetUrls() map[DataType]map[METHOD]string {
return c.URLS
}
// Creates a copy of the current caller, in order to have parallelized executions without race condition
func (c *HTTPCaller) DeepCopy(dst HTTPCaller) error {
bytes, err := json.Marshal(c)
if err != nil {
return err
}
return json.Unmarshal(bytes, &dst)
}
// CallGet calls the GET method on the HTTP server
func (caller *HTTPCaller) CallGet(url string, subpath string, types ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url+subpath, bytes.NewBuffer([]byte("")))
@ -209,4 +230,4 @@ func (caller *HTTPCaller) StoreResp(resp *http.Response) error {
caller.LastResults["body"] = data
return nil
}
}

View File

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