99 Commits

Author SHA1 Message Date
mr 8155f4b17a prospect 2026-06-02 13:47:41 +02:00
mr 51307bb067 trace debug dynamic 2026-06-02 13:33:47 +02:00
mr 6ac788a8ff test 2026-06-02 12:48:33 +02:00
mr a7d0c1208b full filter interpretation 2026-06-02 11:35:19 +02:00
mr 3924fca289 Kick name malformed 2026-06-02 10:50:42 +02:00
mr 797df972ac plantuml duplication behavior 2026-06-02 08:42:20 +02:00
mr 71ae0d2cfc inout change vars regime 2026-06-01 16:45:05 +02:00
mr 5806bdd3d2 Correct link 2026-06-01 15:01:45 +02:00
mr 99fbe82a51 Update Auto Outputs on sourced. 2026-06-01 08:45:50 +02:00
mr 7d8bec9a78 Container can be sourced 2026-06-01 08:26:48 +02:00
mr afd8a2d97c conditionnal is_draft 2026-05-29 14:31:44 +02:00
mr 82a4708f46 Is draft 2026-05-29 14:12:40 +02:00
mr a3bca24982 isdraft pb 2026-05-29 13:43:41 +02:00
mr 41706949fd isDraft Update dafuck 2026-05-29 12:51:04 +02:00
mr b1429596bb not proper enum compararison 2026-05-29 10:38:45 +02:00
mr ce110ee634 inspect comparision 2026-05-29 10:22:07 +02:00
mr 7e5b69b1d2 Live resource failed 2026-05-29 09:12:52 +02:00
mr 26948da3c1 relation peer mismatch 2026-05-28 16:29:36 +02:00
mr 4e1b1164cc relation mismatched 2026-05-28 16:28:51 +02:00
mr 73b844f664 pass to known 2026-05-28 15:37:15 +02:00
mr cef23b5f30 Payment Flow + Access Flow Change 2026-05-27 15:50:23 +02:00
mr e6a9558cbf peerID 2026-05-26 15:04:57 +02:00
mr 9bb3d897b3 parasite log 2026-04-29 11:56:23 +02:00
mr 47d487ea80 ws token 2026-04-29 07:09:13 +02:00
mr a8b7d4d0bc debug service + dynamic 2026-04-28 13:24:25 +02:00
mr 7a12506531 live service in oclib 2026-04-28 12:05:53 +02:00
mr f926a42066 Live x Resource Synergy 2026-04-28 11:48:23 +02:00
mr e3fbe7688a SelectedEmbeddedStorages 2026-04-28 08:55:08 +02:00
mr 318fd52289 SERVICE_RESOURCE 2026-04-27 13:11:14 +02:00
mr 26fc02c5b2 oclib 2026-04-27 12:52:28 +02:00
mr f048b420d7 Addon 2026-04-27 11:16:50 +02:00
mr 0b54d6640d purchase From Nano 2026-04-23 12:05:08 +02:00
mr 7b3b9cb7bf master 2026-04-23 11:45:55 +02:00
mr d9b1ad8dde ToMaster 2026-04-23 11:40:13 +02:00
mr d6106dacde FromNano 2026-04-23 11:36:39 +02:00
mr 365a1d670c from_nano for booking 2026-04-23 11:19:23 +02:00
mr 25880077d1 pending_master 2026-04-23 11:08:02 +02:00
mr 560c997bf1 pending nano for nano flow. 2026-04-23 10:33:51 +02:00
mr 747368c79a nano 2026-04-23 10:16:13 +02:00
mr e5e5706834 master role 2026-04-23 10:11:41 +02:00
mr b9ad5d5ea7 is_nano 2026-04-23 10:04:12 +02:00
mr e70e89b630 Api Struct + Nano env 2026-04-23 09:48:39 +02:00
mr 9c2663601a Service + Storage Binded to Compute 2026-04-23 09:24:02 +02:00
mr 538496cd60 debug cache 2026-04-22 14:13:28 +02:00
mr a4366d3a09 follow purchase 2026-04-22 11:54:16 +02:00
mr 51e2dcc404 Load One pb 2026-04-22 11:47:08 +02:00
mr c208e2ccef sub delete for loop 2026-04-22 11:38:21 +02:00
mr 5cda4fdd40 missed placed 2026-04-22 11:25:32 +02:00
mr b92634ccba debug extend resource 2026-04-22 11:15:39 +02:00
mr da237b1d26 oclib 2026-04-22 10:55:52 +02:00
mr 94e3ebbdd9 temp by pass purchase 2026-04-22 10:24:06 +02:00
mr 6741e929cc purchase as string 2026-04-22 09:48:16 +02:00
mr a08c9b084d GetExtends adjust 2026-04-22 09:18:08 +02:00
mr 17a45eb5d1 GetExtends 2026-04-22 09:14:21 +02:00
mr 0c6efee276 Kick extend treatment 2026-04-21 14:45:04 +02:00
mr bbaea4fec4 extended for load all + search all 2026-04-21 14:36:52 +02:00
mr d57ee0b5e7 Extend for Human Readable 2026-04-21 14:30:45 +02:00
mr 50a5e90f33 Native access debug 2026-04-21 08:16:04 +02:00
mr 5cc04ee490 oc-lib 2026-04-17 09:45:00 +02:00
mr 883c0bec3d graph 2026-04-16 15:19:36 +02:00
mr dc0041999d Change bus 2026-04-14 12:46:22 +02:00
mr a653f9495b lock caller 2026-04-13 15:08:06 +02:00
mr d7b2ef6ae1 Prep Status 2026-04-10 09:57:51 +02:00
mr 878885c8c8 pricing profile payment mode + workflow 2026-04-09 16:14:44 +02:00
mr c340146c8d naming 2026-04-09 08:54:42 +02:00
mr 92eb2663bc add purchase info 2026-04-08 16:34:21 +02:00
mr 284533ad1d Simplify & Live Bug 2026-04-08 15:40:44 +02:00
mr dbbad79480 Resource Buy & Limitation 2026-04-08 15:18:20 +02:00
mr 046bde17d4 format Date for horrible date name 2026-04-08 15:09:32 +02:00
mr 6fe91eda87 NATIVE_TOOL 2026-04-07 11:09:27 +02:00
mr 526eaef33a could not load 2026-04-07 11:03:36 +02:00
mr b7ee6d8e7f kick canDelete 2026-04-07 09:54:48 +02:00
mr 5dbe55e630 StoreDraftDefault skip 2026-04-07 09:36:31 +02:00
mr 2e9f4cb9f4 can delete + search 2026-04-07 08:32:42 +02:00
mr 3ad0a69f54 default on serialization 2026-04-03 17:34:43 +02:00
mr 2a6d3880cd useless print 2026-04-03 16:54:20 +02:00
mr 316ebc93f9 change 2026-04-03 16:37:39 +02:00
mr 913d9b3dfb oclib then 2026-04-03 14:18:07 +02:00
mr 450e917227 PeerGrouping defaulting on access all 2026-04-03 10:36:48 +02:00
mr 54985bbc45 array missing 2026-04-02 14:55:06 +02:00
mr 4f0714cb11 Get ENV INPUTS OUTPUT 2026-04-02 14:45:51 +02:00
mr a2f6f3c252 ENV, Input, Outpu Expose, Container change of rules 2026-04-02 14:31:19 +02:00
mr 2bc4555793 entrypoint 2026-04-02 10:01:26 +02:00
mr ad12f02a70 Rights Behaviors 2026-04-02 09:43:04 +02:00
mr 20cac09f9d Add SetNotInCatalog 2026-04-01 13:04:47 +02:00
mr f3b5a54545 location 2026-03-31 20:19:01 +02:00
mr c0722483b8 IsNot in catalog strategy 2026-03-31 16:41:12 +02:00
mr 0aee593f29 Not in catalog strategy 2026-03-31 16:40:30 +02:00
mr a4ab3285e3 Add attr inspired by docker 2026-03-30 10:21:09 +02:00
mr 45f2351b2f OC LIB -> EXTRA 2026-03-27 12:41:31 +01:00
mr 39cb1c715c debug filter on catalog 2026-03-27 12:14:15 +01:00
mr 87cf2cb12a Booking State 2026-03-26 12:02:03 +01:00
mr 4580200e80 Allowed_image 2026-03-25 10:20:16 +01:00
mr 6d0c78946e Peerless + New Argo 2026-03-24 12:49:37 +01:00
mr 211339947c kubernetes + podchaperon 2026-03-23 16:20:20 +01:00
mr b76b22a8fb Pv + Pvc for admiralty purpose 2026-03-23 12:29:35 +01:00
mr fa9893e150 pvc immediate 2026-03-23 12:16:29 +01:00
mr 14b449f547 Fusion + Nats Complement 2026-03-23 11:53:21 +01:00
mr 5b197c91e0 Add CreatePVC and DeletePVC to KubernetesService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 11:42:58 +01:00
86 changed files with 4993 additions and 910 deletions
+7 -1
View File
@@ -9,6 +9,9 @@ import "sync"
// ===================================================
type Config struct {
IsApi bool
IsNano bool
APIPort int
NATSUrl string
MongoUrl string
@@ -48,10 +51,13 @@ func GetConfig() *Config {
return instance
}
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int,
func SetConfig(isNano bool, isAPI bool, mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string, port int,
pkPath, ppPath,
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI, internalWorkspaceAPI,
internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *Config {
GetConfig().IsNano = isNano
GetConfig().IsApi = isAPI
GetConfig().MongoUrl = mongoUrl
GetConfig().MongoDatabase = database
GetConfig().NATSUrl = natsUrl
+168 -2
View File
@@ -2,6 +2,8 @@ package dbs
import (
"fmt"
"reflect"
"regexp"
"runtime/debug"
"strings"
@@ -102,12 +104,12 @@ func GetBson(filters *Filters) bson.D {
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
f = bson.D{{Key: "$or", Value: orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
f = bson.D{{Key: "$and", Value: andList}}
}
}
return f
@@ -148,6 +150,170 @@ type Filter struct {
Value interface{} `json:"value,omitempty"`
}
// FiltersFromFlatMap builds a *Filters from a map[string]interface{} whose structure
// mirrors the JSON form of Filters:
//
// {
// "and": { "name": [{"operator":"like","value":"foo"}] },
// "or": { "source": [{"operator":"equal","value":"bar"}] }
// }
//
// Keys inside "and"/"or" are json tag names; the function resolves each to its
// full dotted BSON path using the target struct. Unknown keys are kept as-is.
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Filters {
filters := &Filters{
And: make(map[string][]Filter),
Or: make(map[string][]Filter),
}
paths := jsonToBsonPaths(reflect.TypeOf(target), "", "")
resolve := func(jsonKey string) string {
if p, ok := paths[jsonKey]; ok {
return p
}
return jsonKey
}
parseFilters := func(raw interface{}) map[string][]Filter {
out := make(map[string][]Filter)
m, ok := raw.(map[string]interface{})
if !ok {
return out
}
for jsonKey, val := range m {
bsonKey := resolve(jsonKey)
items, ok := val.([]interface{})
fmt.Println(jsonKey, val, ok, bsonKey)
if !ok {
continue
}
for _, item := range items {
entry, ok := item.(map[string]interface{})
if !ok {
continue
}
f := Filter{}
if op, ok := entry["operator"].(string); ok {
f.Operator = op
}
if v, ok := entry["value"]; ok {
f.Value = v
}
out[bsonKey] = append(out[bsonKey], f)
}
}
return out
}
if and, ok := flatMap["and"]; ok {
filters.And = parseFilters(and)
}
if or, ok := flatMap["or"]; ok {
filters.Or = parseFilters(or)
}
return filters
}
// jsonToBsonPaths recursively walks a struct type and returns a map of
// json_name → dotted_bson_path for every field reachable from that type.
//
// Anonymous embedded fields without any tag follow the BSON convention of this
// codebase: they are stored as a nested sub-document whose key is the lowercased
// struct type name (e.g. utils.AbstractObject → "abstractobject"). Their JSON
// fields are promoted (flat), so bsonPrefix advances but jsonPrefix does not.
//
// For fields inside slices or maps, both the leaf json name and the full dotted
// json path (e.g. "instances.access_protocol") are registered as keys so callers
// can use either form unambiguously.
func jsonToBsonPaths(t reflect.Type, bsonPrefix string, jsonPrefix string) map[string]string {
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
t = t.Elem()
}
if t.Kind() == reflect.Map {
t = t.Elem()
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
}
result := make(map[string]string)
if t.Kind() != reflect.Struct {
return result
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
bsonTag := field.Tag.Get("bson")
jsonName := strings.Split(jsonTag, ",")[0]
bsonName := strings.Split(bsonTag, ",")[0]
// Anonymous embedded struct with no tags: use lowercase type name as BSON prefix.
// JSON fields are promoted so jsonPrefix stays the same.
if field.Anonymous && jsonName == "" && bsonName == "" {
ft := field.Type
for ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct {
embedBsonPrefix := strings.ToLower(ft.Name())
re := regexp.MustCompile(`\[[^\]]*\]`)
embedBsonPrefix = re.ReplaceAllString(embedBsonPrefix, "")
embedBsonPrefix = strings.ReplaceAll(embedBsonPrefix, "*", "")
if bsonPrefix != "" {
embedBsonPrefix = bsonPrefix + "." + embedBsonPrefix
}
for k, v := range jsonToBsonPaths(ft, embedBsonPrefix, jsonPrefix) {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}
continue
}
if jsonName == "" || jsonName == "-" {
continue
}
if bsonName == "" || bsonName == "-" {
bsonName = jsonName
}
fullBsonPath := bsonName
if bsonPrefix != "" {
fullBsonPath = bsonPrefix + "." + bsonName
}
fullJsonPath := jsonName
if jsonPrefix != "" {
fullJsonPath = jsonPrefix + "." + jsonName
}
result[jsonName] = fullBsonPath
// Also register the full dotted JSON path so callers can use
// "instances.access_protocol" instead of just "access_protocol".
if fullJsonPath != jsonName {
if _, exists := result[fullJsonPath]; !exists {
result[fullJsonPath] = fullBsonPath
}
}
ft := field.Type
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
ft = ft.Elem()
}
if ft.Kind() == reflect.Map {
ft = ft.Elem()
for ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
}
if ft.Kind() == reflect.Struct {
for k, v := range jsonToBsonPaths(ft, fullBsonPath, fullJsonPath) {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}
}
return result
}
type Input = map[string]interface{}
func InputToBson(i Input, isUpdate bool) bson.D {
+10 -5
View File
@@ -282,12 +282,11 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
return res, 200, nil
}
func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.Cursor, int, error) {
func (m *MongoDB) Search(filters *dbs.Filters, collection_name string, offset int64, limit int64) (*mongo.Cursor, int, error) {
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return nil, 503, err
}
opts := options.Find()
opts.SetLimit(1000)
targetDBCollection := CollectionMap[collection_name]
if targetDBCollection == nil {
return nil, 503, errors.New("collection " + collection_name + " not initialized")
@@ -295,6 +294,9 @@ func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.C
f := dbs.GetBson(filters)
opts.SetSkip(offset) // OFFSET
opts.SetLimit(limit) // LIMIT
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
if cursor, err := targetDBCollection.Find(
@@ -329,7 +331,8 @@ func (m *MongoDB) LoadFilter(filter map[string]interface{}, collection_name stri
return res, 200, nil
}
func (m *MongoDB) LoadAll(collection_name string) (*mongo.Cursor, int, error) {
func (m *MongoDB) LoadAll(collection_name string, offset int64, limit int64) (*mongo.Cursor, int, error) {
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return nil, 503, err
}
@@ -337,8 +340,10 @@ func (m *MongoDB) LoadAll(collection_name string) (*mongo.Cursor, int, error) {
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel()
res, err := targetDBCollection.Find(MngoCtx, bson.D{})
findOptions := options.Find()
findOptions.SetSkip(offset) // OFFSET
findOptions.SetLimit(limit) // LIMIT
res, err := targetDBCollection.Find(MngoCtx, bson.D{}, findOptions)
if err != nil {
// m.Logger.Error().Msg("Couldn't find any resources. Error : " + err.Error())
return nil, 404, err
+164 -41
View File
@@ -17,6 +17,11 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/models/billing"
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
@@ -64,8 +69,20 @@ const (
PURCHASE_RESOURCE = tools.PURCHASE_RESOURCE
NATIVE_TOOL = tools.NATIVE_TOOL
EXECUTION_VERIFICATION = tools.EXECUTION_VERIFICATION
ALLOWED_IMAGE = tools.ALLOWED_IMAGE
SERVICE_RESOURCE = tools.SERVICE_RESOURCE
LIVE_SERVICE = tools.LIVE_SERVICE
BILL = tools.BILL
PAYMENT = tools.PAYMENT
REFUND = tools.REFUND
DISCOUNT = tools.DISCOUNT
SUBSCRIPTION = tools.SUBSCRIPTION
)
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
return dbs.FiltersFromFlatMap(flatMap, target)
}
func GetMySelf() (*peer.Peer, error) {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
if pp == nil {
@@ -150,6 +167,9 @@ func InitDaemon(appName string) {
// resources.InitNative()
// feed the library with the loaded config
SetConfig(
o.GetBoolDefault("IS_NANO", false),
o.GetBoolDefault("IS_API", true),
o.GetStringDefault("MONGO_URL", "mongodb://127.0.0.1:27017"),
o.GetStringDefault("MONGO_DATABASE", "DC_myDC"),
o.GetStringDefault("NATS_URL", "nats://localhost:4222"),
@@ -167,11 +187,13 @@ func InitDaemon(appName string) {
o.GetStringDefault("INTERNAL_DATACENTER_API", "oc-datacenter"),
o.GetStringDefault("INTERNAL_SCHEDULER_API", "oc-scheduler"),
)
// Beego init
beego.BConfig.AppName = appName
beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080)
beego.BConfig.WebConfig.DirectoryIndex = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
if config.GetConfig().IsApi {
// Beego init
beego.BConfig.AppName = appName
beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080)
beego.BConfig.WebConfig.DirectoryIndex = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
}
}
type IDTokenClaims struct {
@@ -191,6 +213,53 @@ type Claims struct {
Session SessionClaims `json:"session"`
}
func GetExtends(objs []utils.ShallowDBObject, typ ...string) []map[string]interface{} {
cache := map[tools.DataType]map[string]interface{}{}
m := []map[string]interface{}{}
for _, obj := range objs {
m = append(m, GetExtend(obj, obj.Extend(typ...), cache))
}
return m
}
func GetExtend(obj utils.DBObject, extends map[string][]tools.DataType, cache map[tools.DataType]map[string]interface{}) map[string]interface{} {
base := obj.Serialize(obj)
for k, v := range extends {
if base[k+"_id"] == nil || base[k+"_id"] == "" {
fmt.Println(k+"_id", "GET EXTEND")
continue
}
for _, vv := range v {
if cache[vv] != nil && cache[vv][fmt.Sprintf("%v", base[k+"_id"])] != nil {
base[k] = cache[vv][fmt.Sprintf("%v", base[k+"_id"])]
continue
}
if d, _, err := models.Model(vv.EnumIndex()).GetAccessor(&tools.APIRequest{
Admin: true,
}).LoadOne(fmt.Sprintf("%v", base[k+"_id"])); d != nil && err == nil {
base[k] = d.Serialize(d)
if cache[vv] == nil {
cache[vv] = map[string]interface{}{}
}
if cache[vv][fmt.Sprintf("%v", base[k+"_id"])] == nil {
fmt.Println("TTT", vv, k, base[k])
cache[vv][fmt.Sprintf("%v", base[k+"_id"])] = base[k]
}
break
}
}
}
return base
}
func ExtractTokenInfoWs(request http.Request) (string, string, []string) {
reqToken := request.Header.Get("Sec-WebSocket-Protocol")
if reqToken != "" {
return extractFromToken(reqToken, "user_id"), extractFromToken(reqToken, "peer_id"), strings.Split(extractFromToken(reqToken, "groups"), ",")
}
return "", "", []string{}
}
func ExtractTokenInfo(request http.Request) (string, string, []string) {
reqToken := request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
@@ -200,38 +269,57 @@ func ExtractTokenInfo(request http.Request) (string, string, []string) {
reqToken = splitToken[1]
}
if reqToken != "" {
token := strings.Split(reqToken, ".")
if len(token) > 2 {
bytes, err := base64.StdEncoding.DecodeString(token[2])
if err != nil {
return "", "", []string{}
}
var c Claims
err = json.Unmarshal(bytes, &c)
if err != nil {
return "", "", []string{}
}
return c.Session.IDToken.UserID, c.Session.IDToken.PeerID, c.Session.IDToken.Groups
}
return extractFromToken(reqToken, "user_id"), extractFromToken(reqToken, "peer_id"), strings.Split(extractFromToken(reqToken, "groups"), ",")
}
return "", "", []string{}
}
func InitAPI(appName string) {
func extractFromToken(token string, attr string) string {
parts := strings.Split(token, ".")
if len(parts) < 2 {
return ""
}
payload := parts[1]
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
b, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
return ""
}
var claims map[string]interface{}
if err := json.Unmarshal(b, &claims); err != nil {
return ""
}
ext, ok := claims["ext"].(map[string]interface{})
if !ok {
return ""
}
peerID, _ := ext[attr].(string)
return peerID
}
func InitAPI(appName string, extraRoutes ...map[string][]string) {
InitDaemon(appName)
beego.BConfig.Listen.HTTPPort = config.GetConfig().APIPort
beego.BConfig.WebConfig.DirectoryIndex = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
c := cors.Allow(&cors.Options{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
ExposeHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
})
beego.InsertFilter("*", beego.BeforeRouter, c)
api := &tools.API{}
api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo())
if config.GetConfig().IsApi {
beego.BConfig.Listen.HTTPPort = config.GetConfig().APIPort
beego.BConfig.WebConfig.DirectoryIndex = true
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
c := cors.Allow(&cors.Options{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
ExposeHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
})
beego.InsertFilter("*", beego.BeforeRouter, c)
api := &tools.API{}
api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo(), extraRoutes...)
}
}
//
@@ -253,11 +341,11 @@ func GetLogger() zerolog.Logger {
* @param logLevel string
* @return *Config
*/
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string,
func SetConfig(isNano bool, isApi bool, mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string,
port int, pppath string, pkpath string,
internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI string, internalSchedulerAPI string) *config.Config {
cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath, internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
cfg := config.SetConfig(isNano, isApi, mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath, internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI, internalSchedulerAPI)
defer func() {
if r := recover(); r != nil {
@@ -341,7 +429,7 @@ func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.Wo
* @param c ...*tools.HTTPCaller
* @return data LibDataShallow
*/
func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data LibDataShallow) {
func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool, offset int64, limit int64) (data LibDataShallow) {
defer func() { // recover the panic
if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Search : "+fmt.Sprintf("%v", r)))
@@ -354,7 +442,7 @@ func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).Search(filters, word, isDraft)
}).Search(filters, word, isDraft, offset, limit)
if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
return
@@ -369,7 +457,7 @@ func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data
* @param c ...*tools.HTTPCaller
* @return data LibDataShallow
*/
func (r *Request) LoadAll(isDraft bool) (data LibDataShallow) {
func (r *Request) LoadAll(isDraft bool, offset int64, limit int64) (data LibDataShallow) {
defer func() { // recover the panic
if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in LoadAll : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
@@ -382,7 +470,7 @@ func (r *Request) LoadAll(isDraft bool) (data LibDataShallow) {
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).LoadAll(isDraft)
}).LoadAll(isDraft, offset, limit)
if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
return
@@ -649,6 +737,41 @@ func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
return nil
}
func (l *LibData) ToBill() *billing.Bill {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.BILL {
return l.Data.(*billing.Bill)
}
return nil
}
func (l *LibData) ToPayment() *payment.Payment {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.PAYMENT {
return l.Data.(*payment.Payment)
}
return nil
}
func (l *LibData) ToRefund() *refund.Refund {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.REFUND {
return l.Data.(*refund.Refund)
}
return nil
}
func (l *LibData) ToDiscount() *discount.Discount {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.DISCOUNT {
return l.Data.(*discount.Discount)
}
return nil
}
func (l *LibData) ToSubscription() *subscription.Subscription {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.SUBSCRIPTION {
return l.Data.(*subscription.Subscription)
}
return nil
}
// ------------- Loading resources ----------GetAccessor
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
@@ -712,7 +835,7 @@ func InitNATSDecentralizedEmitter(authorizedDT ...tools.DataType) {
return // don't trust anyone... only friends and foes are privilege
}
access := NewRequestAdmin(LibDataEnum(resp.Datatype), nil)
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false); len(data.Data) > 0 {
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false, 0, 1); len(data.Data) > 0 {
delete(p, "id")
access.UpdateOne(p, data.Data[0].GetID())
} else {
@@ -731,8 +854,8 @@ func InitNATSDecentralizedEmitter(authorizedDT ...tools.DataType) {
access := NewRequestAdmin(LibDataEnum(resp.Datatype), nil)
err := json.Unmarshal(resp.Payload, &p)
if err == nil {
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false); len(data.Data) > 0 {
access.DeleteOne(fmt.Sprintf("%v", p[resp.SearchAttr]))
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false, 0, 1); len(data.Data) > 0 {
access.DeleteOne(data.Data[0].GetID())
}
}
+19 -1
View File
@@ -7,6 +7,7 @@ require (
github.com/go-playground/validator/v10 v10.22.0
github.com/google/uuid v1.6.0
github.com/goraz/onion v0.1.3
github.com/libp2p/go-libp2p/core v0.43.0-rc2
github.com/nats-io/nats.go v1.37.0
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.11.1
@@ -22,18 +23,33 @@ require (
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.16.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.1 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/time v0.9.0 // indirect
@@ -42,6 +58,7 @@ require (
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
@@ -67,7 +84,6 @@ require (
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libp2p/go-libp2p/core v0.43.0-rc2
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -89,3 +105,5 @@ require (
k8s.io/api v0.35.1
k8s.io/client-go v0.35.1
)
replace github.com/libp2p/go-libp2p/core => github.com/libp2p/go-libp2p v0.47.0
+2
View File
@@ -133,6 +133,8 @@ github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUa
github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ=
github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+56
View File
@@ -0,0 +1,56 @@
package allowed_image
import (
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
// AllowedImage représente une image de conteneur autorisée à persister
// sur un peer après l'exécution d'un workflow.
//
// La décision de rétention est entièrement locale au datacenter —
// le fournisseur de processing n'a aucun levier sur cette liste.
//
// Règle de matching (côté oc-datacenter) :
// - Registry vide = toutes les registries
// - TagConstraint vide = toutes les versions
// - TagConstraint non vide = exact ou glob (ex: "3.*", "1.2.3")
//
// Les entrées IsDefault sont créées au bootstrap et ne peuvent pas
// être supprimées via l'API.
type AllowedImage struct {
utils.AbstractObject
// Registry source (ex: "docker.io", "registry.example.com").
// Vide = wildcard, accepte n'importe quelle registry.
Registry string `json:"registry,omitempty" bson:"registry,omitempty"`
// Image est le nom de l'image sans registry ni tag
// (ex: "natsio/nats-box", "library/alpine").
Image string `json:"image" bson:"image" validate:"required"`
// TagConstraint est la contrainte sur le tag.
// Vide = toutes les versions autorisées.
// Supporte exact ("1.2.3") ou glob ("3.*", "*-alpine").
TagConstraint string `json:"tag_constraint,omitempty" bson:"tag_constraint,omitempty"`
// IsDefault marque les entrées bootstrap insérées au démarrage.
// Ces entrées ne peuvent pas être supprimées via l'API.
IsDefault bool `json:"is_default,omitempty" bson:"is_default,omitempty"`
}
func (a *AllowedImage) StoreDraftDefault() {
a.IsDraft = false // les allowed images sont actives immédiatement
}
func (a *AllowedImage) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
return true, set
}
func (a *AllowedImage) CanDelete() bool {
return !a.IsDefault // les entrées bootstrap sont non supprimables
}
func (a *AllowedImage) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
@@ -0,0 +1,23 @@
package allowed_image
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type allowedImageMongoAccessor struct {
utils.AbstractAccessor[*AllowedImage]
}
func NewAccessor(request *tools.APIRequest) *allowedImageMongoAccessor {
return &allowedImageMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*AllowedImage]{
Logger: logs.CreateLogger(tools.ALLOWED_IMAGE.String()),
Request: request,
Type: tools.ALLOWED_IMAGE,
New: func() *AllowedImage { return &AllowedImage{} },
NotImplemented: []string{"CopyOne"},
},
}
}
-240
View File
@@ -1,240 +0,0 @@
package bill
import (
"encoding/json"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* Booking is a struct that represents a booking
*/
type Bill struct {
utils.AbstractObject
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
}
func GenerateBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
// hhmmm : should get... the loop.
return &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: false,
},
OrderID: order.UUID,
Status: enum.PENDING,
// SubOrders: peerOrders,
}, nil
}
func DraftFirstBill(order *order.Order, request *tools.APIRequest) (*Bill, error) {
peers := map[string][]*PeerItemOrder{}
for _, p := range order.Purchases {
// TODO : if once
if _, ok := peers[p.DestPeerID]; !ok {
peers[p.DestPeerID] = []*PeerItemOrder{}
}
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
ResourceType: p.ResourceType,
Purchase: p,
Item: p.PricedItem,
Quantity: 1,
})
}
for _, b := range order.Bookings {
// TODO : if once
isPurchased := false
for _, p := range order.Purchases {
if p.ResourceID == b.ResourceID {
isPurchased = true
break
}
}
if isPurchased {
continue
}
if _, ok := peers[b.DestPeerID]; !ok {
peers[b.DestPeerID] = []*PeerItemOrder{}
}
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
ResourceType: b.ResourceType,
Quantity: 1,
Item: b.PricedItem,
})
}
peerOrders := map[string]*PeerOrder{}
for peerID, items := range peers {
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
if err != nil {
return nil, err
}
peerOrders[peerID] = &PeerOrder{
PeerID: peerID,
BillingAddress: pr.(*peer.Peer).WalletAddress,
Items: items,
}
}
bill := &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: true,
},
OrderID: order.UUID,
Status: enum.PENDING,
SubOrders: peerOrders,
}
return bill.SumUpBill(request)
}
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (r *Bill) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.Status != set.(*Bill).Status {
return true, &Bill{Status: set.(*Bill).Status} // only state can be updated
}
return r.IsDraft, set
}
func (r *Bill) CanDelete() bool {
return r.IsDraft // only draft order can be deleted
}
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return d, err
}
d.Total += b.Total
}
return d, nil
}
type PeerOrder struct {
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
switch dt {
case tools.PROCESSING_RESOURCE:
return &resources.PricedProcessingResource{}
case tools.STORAGE_RESOURCE:
return &resources.PricedStorageResource{}
case tools.DATA_RESOURCE:
return &resources.PricedDataResource{}
case tools.COMPUTE_RESOURCE:
return &resources.PricedComputeResource{}
case tools.WORKFLOW_RESOURCE:
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
}
return nil
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING
go func() {
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER ON RETURN UPDATE STATUS
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID {
for _, b := range d.Items {
priced := PricedByType(b.ResourceType)
bb, _ := json.Marshal(b.Item)
json.Unmarshal(bb, priced)
if !priced.IsPurchasable() {
continue
}
accessor := purchase_resource.NewAccessor(request)
accessor.StoreOne(&purchase_resource.PurchaseResource{
ResourceID: priced.GetID(),
ResourceType: priced.GetType(),
EndDate: priced.GetLocationEnd(),
})
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPriceHT(request) // missing something
if err != nil {
return err
}
d.Total += tot
}
return nil
}
type PeerItemOrder struct {
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
}
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
/////////// Temporary in order to allow GenerateOrder to complete while billing is still WIP
if d.Purchase == nil {
return 0, nil
}
///////////
priced := PricedByType(d.ResourceType)
b, _ := json.Marshal(d.Item)
err := json.Unmarshal(b, priced)
if err != nil {
return 0, err
}
accessor := purchase_resource.NewAccessor(request)
search, code, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
},
}, "", d.Purchase.IsDraft)
if code == 200 && len(search) > 0 {
for _, s := range search {
if s.(*purchase_resource.PurchaseResource).EndDate == nil || time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
return 0, nil
}
}
}
p, err := priced.GetPriceHT()
if err != nil {
return 0, err
}
return p * float64(d.Quantity), nil
}
// WTF HOW TO SELECT THE RIGHT PRICE ???
// SHOULD SET A BUYING STATUS WHEN PAYMENT IS VALIDATED
+421
View File
@@ -0,0 +1,421 @@
package billing
import (
"encoding/json"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
type Bill struct {
utils.AbstractObject
OrderID string `json:"order_id" bson:"order_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
}
func (ri *Bill) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "order":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.ORDER)
}
}
return ext
}
// IsFullySettled retourne vrai quand chaque ligne de chaque peer-order est réglée.
func (b *Bill) IsFullySettled() bool {
for _, po := range b.SubOrders {
for _, item := range po.Items {
if !item.Settled {
return false
}
}
}
return true
}
// SettledTotal retourne le montant total des lignes déjà réglées.
func (b *Bill) SettledTotal() float64 {
total := 0.0
for _, po := range b.SubOrders {
for _, item := range po.Items {
if item.Settled {
total += item.UnitPriceHT * float64(item.Quantity)
}
}
}
return total
}
// MarkItemSettled marque une ligne comme réglée d'après son itemID
// et propage le statut PAID sur le PeerOrder si toutes ses lignes sont réglées.
func (b *Bill) MarkItemSettled(itemID string) bool {
now := time.Now().UTC()
for _, po := range b.SubOrders {
for _, item := range po.Items {
if item.ItemID == itemID {
item.Settled = true
item.SettledAt = &now
// propage le statut PAID si toutes les lignes du peer sont réglées
if po.IsFullySettled() {
po.Status = enum.PAID
}
return true
}
}
}
return false
}
func GenerateBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
return &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: false,
},
OrderID: ord.UUID,
Status: enum.PENDING,
}, nil
}
// DraftFirstBill crée le premier brouillon de facture pour un order.
// Règle :
// - Calcul total indépendant du mode de paiement (photo du coût réel).
// - Les purchases sont toujours BILL_ONCE / PAY_ONCE → réglées immédiatement.
// - Les bookings avec BillingStrategy != BILL_ONCE génèrent des Subscription.
// - Chaque ligne reçoit un ItemID unique pour le suivi de règlement.
func DraftFirstBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
peers := map[string][]*PeerItemOrder{}
// Purchases : facturation immédiate, pas de subscription
for _, p := range ord.Purchases {
if _, ok := peers[p.DestPeerID]; !ok {
peers[p.DestPeerID] = []*PeerItemOrder{}
}
peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
ItemID: uuid.New().String(),
ResourceType: p.ResourceType,
Purchase: p,
Item: p.PricedItem,
Quantity: 1,
BillingStrategy: pricing.BILL_ONCE,
PaymentType: pricing.PAY_ONCE,
})
}
// Bookings : exclure les ressources déjà achetées (purchase_resource existant)
purchasedIDs := map[string]bool{}
for _, p := range ord.Purchases {
purchasedIDs[p.ResourceID] = true
}
for _, b := range ord.Bookings {
if purchasedIDs[b.ResourceID] {
continue
}
if _, ok := peers[b.DestPeerID]; !ok {
peers[b.DestPeerID] = []*PeerItemOrder{}
}
peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
ItemID: uuid.New().String(),
ResourceType: b.ResourceType,
Quantity: 1,
Item: b.PricedItem,
BillingStrategy: b.BillingStrategy,
PaymentType: b.PaymentType,
})
}
// Résolution des adresses de facturation peer
peerOrders := map[string]*PeerOrder{}
for peerID, items := range peers {
pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
if err != nil {
return nil, err
}
peerOrders[peerID] = &PeerOrder{
PeerID: peerID,
BillingAddress: pr.(*peer.Peer).WalletAddress,
Items: items,
}
}
bill := &Bill{
AbstractObject: utils.AbstractObject{
Name: "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
IsDraft: true,
},
OrderID: ord.UUID,
Status: enum.PENDING,
SubOrders: peerOrders,
}
// 1. Calcul des totaux (indépendant du mode de paiement)
if _, err := bill.SumUpBill(request); err != nil {
return bill, err
}
// 2. Création des subscriptions pour les lignes récurrentes
subIDs, err := createRecurringSubscriptions(bill, request)
if err != nil {
return bill, err
}
// 3. Liaison des subscription IDs à l'order pour traçabilité
if len(subIDs) > 0 {
ord.SubscriptionIDs = append(ord.SubscriptionIDs, subIDs...)
}
// 4. Persistance du brouillon de facture (les UnitPriceHT et SubscriptionID sont déjà set)
stored, _, err := NewAccessor(request).StoreOne(bill)
if err != nil {
return bill, err
}
return stored.(*Bill), nil
}
// createRecurringSubscriptions crée les Subscription pour chaque groupe
// (peer × BillingStrategy) dont la stratégie est récurrente.
// Modifie les PeerItemOrder en place (SubscriptionID).
// Retourne les IDs de subscriptions créées.
func createRecurringSubscriptions(b *Bill, request *tools.APIRequest) ([]string, error) {
subIDs := []string{}
for peerID, po := range b.SubOrders {
// Groupe les items récurrents par BillingStrategy
byStrategy := map[pricing.BillingStrategy][]*PeerItemOrder{}
for _, item := range po.Items {
if item.BillingStrategy == pricing.BILL_ONCE {
continue
}
byStrategy[item.BillingStrategy] = append(byStrategy[item.BillingStrategy], item)
}
for strategy, items := range byStrategy {
subItems := make([]*subscription.SubscriptionItem, 0, len(items))
totalAmount := 0.0
for _, item := range items {
subItems = append(subItems, &subscription.SubscriptionItem{
ResourceType: item.ResourceType,
Quantity: item.Quantity,
UnitPrice: item.UnitPriceHT,
})
totalAmount += item.UnitPriceHT * float64(item.Quantity)
}
var sub *subscription.Subscription
switch strategy {
case pricing.BILL_PER_YEAR:
sub = subscription.NewYearlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
case pricing.BILL_PER_WEEK:
sub = subscription.NewWeeklySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
default: // BILL_PER_MONTH et tout autre cas récurrent
sub = subscription.NewMonthlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
}
sub.IsDraft = true
res, _, err := subscription.NewAccessor(request).StoreOne(sub)
if err != nil {
return subIDs, err
}
storedSub := res.(*subscription.Subscription)
subIDs = append(subIDs, storedSub.GetID())
// Lie le SubscriptionID à chaque ligne concernée
for _, item := range items {
item.SubscriptionID = storedSub.GetID()
}
}
}
return subIDs, nil
}
func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (r *Bill) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.Status != set.(*Bill).Status {
return true, &Bill{Status: set.(*Bill).Status}
}
return r.IsDraft, set
}
func (r *Bill) CanDelete() bool {
return r.IsDraft
}
func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return d, err
}
d.Total += b.Total
}
return d, nil
}
// ---------------------------------------------------------------------------
// PeerOrder
// ---------------------------------------------------------------------------
type PeerOrder struct {
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
// IsFullySettled retourne vrai si toutes les lignes de ce peer sont réglées.
func (po *PeerOrder) IsFullySettled() bool {
for _, item := range po.Items {
if !item.Settled {
return false
}
}
return true
}
func PricedByType(dt tools.DataType) pricing.PricedItemITF {
switch dt {
case tools.PROCESSING_RESOURCE:
return &resources.PricedProcessingResource{}
case tools.STORAGE_RESOURCE:
return &resources.PricedStorageResource{}
case tools.DATA_RESOURCE:
return &resources.PricedDataResource{}
case tools.COMPUTE_RESOURCE:
return &resources.PricedComputeResource{}
case tools.WORKFLOW_RESOURCE:
return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
}
return nil
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING
go func() {
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER — UPDATE STATUS ON RETURN
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID {
now := time.Now().UTC()
for _, b := range d.Items {
priced := PricedByType(b.ResourceType)
bb, _ := json.Marshal(b.Item)
json.Unmarshal(bb, priced)
if !priced.IsPurchasable() {
continue
}
accessor := purchase_resource.NewAccessor(request)
accessor.StoreOne(&purchase_resource.PurchaseResource{
ResourceID: priced.GetID(),
ResourceType: priced.GetType(),
EndDate: priced.GetLocationEnd(),
})
// Marque la ligne comme réglée
b.Settled = true
b.SettledAt = &now
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPriceHT(request)
if err != nil {
return err
}
d.Total += tot
}
return nil
}
// ---------------------------------------------------------------------------
// PeerItemOrder
// ---------------------------------------------------------------------------
// PeerItemOrder est une ligne de facture pour un peer donné.
type PeerItemOrder struct {
// ItemID identifie de manière unique cette ligne pour le suivi de règlement.
ItemID string `json:"item_id,omitempty" bson:"item_id,omitempty"`
ResourceType tools.DataType `json:"datatype,omitempty" bson:"datatype,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item map[string]interface{} `json:"item,omitempty" bson:"item,omitempty"`
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
// UnitPriceHT est le prix unitaire HT calculé par SumUpBill/GetPriceHT.
// Utilisé pour la création des subscriptions sans recalcul.
UnitPriceHT float64 `json:"unit_price_ht,omitempty" bson:"unit_price_ht,omitempty"`
// SubscriptionID référence la Subscription créée pour les lignes récurrentes.
// Vide pour les lignes BILL_ONCE.
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
// Settled indique si cette ligne a été réglée (paiement confirmé).
Settled bool `json:"settled" bson:"settled"`
SettledAt *time.Time `json:"settled_at,omitempty" bson:"settled_at,omitempty"`
}
func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
if d.Purchase == nil {
return 0, nil
}
priced := PricedByType(d.ResourceType)
b, _ := json.Marshal(d.Item)
err := json.Unmarshal(b, priced)
if err != nil {
return 0, err
}
accessor := purchase_resource.NewAccessor(request)
search, code, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
},
}, "", d.Purchase.IsDraft, 0, 10000)
if code == 200 && len(search) > 0 {
for _, s := range search {
if s.(*purchase_resource.PurchaseResource).EndDate == nil ||
time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
return 0, nil
}
}
}
unitPrice, err := priced.GetPriceHT()
if err != nil {
return 0, err
}
d.UnitPriceHT = unitPrice // cache pour createRecurringSubscriptions
return unitPrice * float64(d.Quantity), nil
}
@@ -1,4 +1,4 @@
package bill
package billing
import (
"cloud.o-forge.io/core/oc-lib/logs"
@@ -7,14 +7,13 @@ import (
)
type billMongoAccessor struct {
utils.AbstractAccessor[*Bill] // AbstractAccessor contains the basic fields of an accessor (model, caller)
utils.AbstractAccessor[*Bill]
}
// New creates a new instance of the billMongoAccessor
func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
return &billMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Bill]{
Logger: logs.CreateLogger(tools.BILL.String()), // Create a logger with the data type
Logger: logs.CreateLogger(tools.BILL.String()),
Request: request,
Type: tools.BILL,
New: func() *Bill { return &Bill{} },
+146
View File
@@ -0,0 +1,146 @@
package discount
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type DiscountType int
const (
PERCENTAGE DiscountType = iota // réduction en pourcentage
FIXED_AMOUNT // réduction montant fixe
)
func (d DiscountType) String() string {
return [...]string{"percentage", "fixed_amount"}[d]
}
func DiscountTypeList() []DiscountType {
return []DiscountType{PERCENTAGE, FIXED_AMOUNT}
}
type DiscountScope int
const (
SCOPE_ALL DiscountScope = iota // applicable à tout
SCOPE_RESOURCE_TYPE // applicable à un type de ressource
SCOPE_RESOURCE // applicable à une ressource spécifique
SCOPE_SUBSCRIPTION // applicable aux souscriptions
)
func (d DiscountScope) String() string {
return [...]string{"all", "resource_type", "resource", "subscription"}[d]
}
// Discount représente une réduction applicable sur les ressources ou abonnements.
type Discount struct {
utils.AbstractObject
Code string `json:"code,omitempty" bson:"code,omitempty"`
DiscountType DiscountType `json:"discount_type" bson:"discount_type"`
Scope DiscountScope `json:"scope" bson:"scope"`
Value float64 `json:"value" bson:"value"` // pourcentage (0-100) ou montant fixe
Currency string `json:"currency,omitempty" bson:"currency,omitempty"` // pour FIXED_AMOUNT
ResourceTypes []tools.DataType `json:"resource_types,omitempty" bson:"resource_types,omitempty"` // si SCOPE_RESOURCE_TYPE
ResourceIDs []string `json:"resource_ids,omitempty" bson:"resource_ids,omitempty"` // si SCOPE_RESOURCE
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
MaxUsage int `json:"max_usage,omitempty" bson:"max_usage,omitempty"` // 0 = illimité
CurrentUsage int `json:"current_usage" bson:"current_usage"`
MinAmount float64 `json:"min_amount,omitempty" bson:"min_amount,omitempty"` // montant minimum du bill pour appliquer
Active bool `json:"active" bson:"active" default:"true"`
}
// IsValid vérifie si la réduction est applicable au moment présent.
func (d *Discount) IsValid(billAmount float64) bool {
now := time.Now().UTC()
if !d.Active {
return false
}
if d.MaxUsage > 0 && d.CurrentUsage >= d.MaxUsage {
return false
}
if d.ValidFrom != nil && now.Before(*d.ValidFrom) {
return false
}
if d.ValidUntil != nil && now.After(*d.ValidUntil) {
return false
}
if d.MinAmount > 0 && billAmount < d.MinAmount {
return false
}
return true
}
// Apply applique la réduction sur un prix HT et retourne le prix réduit.
func (d *Discount) Apply(priceHT float64) float64 {
switch d.DiscountType {
case PERCENTAGE:
return priceHT - (priceHT * d.Value / 100)
case FIXED_AMOUNT:
result := priceHT - d.Value
if result < 0 {
return 0
}
return result
}
return priceHT
}
// DiscountAmount retourne le montant de la réduction sans l'appliquer.
func (d *Discount) DiscountAmount(priceHT float64) float64 {
switch d.DiscountType {
case PERCENTAGE:
return priceHT * d.Value / 100
case FIXED_AMOUNT:
if d.Value > priceHT {
return priceHT
}
return d.Value
}
return 0
}
// AppliesToResource vérifie si cette réduction s'applique à une ressource donnée.
func (d *Discount) AppliesToResource(resourceID string, resourceType tools.DataType) bool {
switch d.Scope {
case SCOPE_ALL:
return true
case SCOPE_RESOURCE:
for _, id := range d.ResourceIDs {
if id == resourceID {
return true
}
}
case SCOPE_RESOURCE_TYPE:
for _, t := range d.ResourceTypes {
if t == resourceType {
return true
}
}
}
return false
}
// IncrementUsage incrémente le compteur d'utilisation.
func (d *Discount) IncrementUsage() {
d.CurrentUsage++
}
func (d *Discount) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (d *Discount) StoreDraftDefault() {
d.IsDraft = true
}
func (d *Discount) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
return d.IsDraft, set
}
func (d *Discount) CanDelete() bool {
return d.IsDraft || d.CurrentUsage == 0
}
@@ -0,0 +1,22 @@
package discount
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type discountMongoAccessor struct {
utils.AbstractAccessor[*Discount]
}
func NewAccessor(request *tools.APIRequest) *discountMongoAccessor {
return &discountMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Discount]{
Logger: logs.CreateLogger(tools.DISCOUNT.String()),
Request: request,
Type: tools.DISCOUNT,
New: func() *Discount { return &Discount{} },
},
}
}
+151
View File
@@ -0,0 +1,151 @@
package payment
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type PaymentStatus int
const (
PAYMENT_PENDING PaymentStatus = iota
PAYMENT_PROCESSING // en cours de traitement blockchain/réseau
PAYMENT_COMPLETED // confirmé
PAYMENT_FAILED // échoué
PAYMENT_CANCELLED // annulé avant exécution
PAYMENT_REFUNDED // remboursé
)
func (s PaymentStatus) String() string {
return [...]string{"pending", "processing", "completed", "failed", "cancelled", "refunded"}[s]
}
func PaymentStatusList() []PaymentStatus {
return []PaymentStatus{PAYMENT_PENDING, PAYMENT_PROCESSING, PAYMENT_COMPLETED, PAYMENT_FAILED, PAYMENT_CANCELLED, PAYMENT_REFUNDED}
}
type PaymentMethod int
const (
METHOD_BLOCKCHAIN PaymentMethod = iota
METHOD_CREDIT_CARD
METHOD_BANK_TRANSFER
METHOD_CRYPTO
METHOD_INTERNAL_CREDIT // crédit interne à la plateforme
)
func (m PaymentMethod) String() string {
return [...]string{"blockchain", "credit_card", "bank_transfer", "crypto", "internal_credit"}[m]
}
func PaymentMethodList() []PaymentMethod {
return []PaymentMethod{METHOD_BLOCKCHAIN, METHOD_CREDIT_CARD, METHOD_BANK_TRANSFER, METHOD_CRYPTO, METHOD_INTERNAL_CREDIT}
}
// Payment représente une transaction de paiement — instantanée, mensuelle ou annuelle.
type Payment struct {
utils.AbstractObject
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`
PayerPeerID string `json:"payer_peer_id,omitempty" bson:"payer_peer_id,omitempty"`
RecipientPeerID string `json:"recipient_peer_id,omitempty" bson:"recipient_peer_id,omitempty"`
Amount float64 `json:"amount" bson:"amount"`
Currency string `json:"currency" bson:"currency" default:"EUR"`
Status PaymentStatus `json:"status" bson:"status"`
Method PaymentMethod `json:"method" bson:"method"`
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"` // PAY_ONCE, PAY_EVERY_MONTH, PAY_EVERY_YEAR
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
WalletFrom string `json:"wallet_from,omitempty" bson:"wallet_from,omitempty"`
WalletTo string `json:"wallet_to,omitempty" bson:"wallet_to,omitempty"`
ScheduledAt *time.Time `json:"scheduled_at,omitempty" bson:"scheduled_at,omitempty"`
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
FailureReason string `json:"failure_reason,omitempty" bson:"failure_reason,omitempty"`
Metadata map[string]string `json:"metadata,omitempty" bson:"metadata,omitempty"`
}
// NewInstantPayment crée un paiement immédiat (PAY_ONCE).
func NewInstantPayment(billID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod) *Payment {
return &Payment{
BillID: billID,
PayerPeerID: payerPeerID,
RecipientPeerID: recipientPeerID,
Amount: amount,
Currency: currency,
Status: PAYMENT_PENDING,
Method: method,
PaymentType: pricing.PAY_ONCE,
}
}
// NewScheduledPayment crée un paiement programmé (mensuel ou annuel).
func NewScheduledPayment(subscriptionID, payerPeerID, recipientPeerID string, amount float64, currency string, method PaymentMethod, paymentType pricing.PaymentType, scheduledAt time.Time) *Payment {
return &Payment{
SubscriptionID: subscriptionID,
PayerPeerID: payerPeerID,
RecipientPeerID: recipientPeerID,
Amount: amount,
Currency: currency,
Status: PAYMENT_PENDING,
Method: method,
PaymentType: paymentType,
ScheduledAt: &scheduledAt,
}
}
// Complete marque le paiement comme confirmé.
func (p *Payment) Complete(transactionID string) {
now := time.Now().UTC()
p.Status = PAYMENT_COMPLETED
p.TransactionID = transactionID
p.ProcessedAt = &now
}
// Fail marque le paiement comme échoué.
func (p *Payment) Fail(reason string) {
now := time.Now().UTC()
p.Status = PAYMENT_FAILED
p.FailureReason = reason
p.ProcessedAt = &now
}
// Cancel annule le paiement s'il est encore en attente.
func (p *Payment) Cancel() bool {
if p.Status != PAYMENT_PENDING {
return false
}
p.Status = PAYMENT_CANCELLED
return true
}
// IsRefundable indique si le paiement peut faire l'objet d'un remboursement.
func (p *Payment) IsRefundable() bool {
return p.Status == PAYMENT_COMPLETED
}
func (p *Payment) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (p *Payment) StoreDraftDefault() {
p.IsDraft = true
}
func (p *Payment) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Payment)
if !p.IsDraft && p.Status != incoming.Status {
return true, &Payment{
Status: incoming.Status,
TransactionID: incoming.TransactionID,
FailureReason: incoming.FailureReason,
}
}
return p.IsDraft, set
}
func (p *Payment) CanDelete() bool {
return p.IsDraft || p.Status == PAYMENT_PENDING
}
@@ -0,0 +1,22 @@
package payment
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type paymentMongoAccessor struct {
utils.AbstractAccessor[*Payment]
}
func NewAccessor(request *tools.APIRequest) *paymentMongoAccessor {
return &paymentMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Payment]{
Logger: logs.CreateLogger(tools.PAYMENT.String()),
Request: request,
Type: tools.PAYMENT,
New: func() *Payment { return &Payment{} },
},
}
}
@@ -0,0 +1,82 @@
package payment
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
)
type ScheduleStatus int
const (
SCHEDULE_ACTIVE ScheduleStatus = iota
SCHEDULE_PAUSED // mis en pause manuellement
SCHEDULE_CANCELLED // résilié
SCHEDULE_COMPLETED // terminé normalement (abonnement expiré)
SCHEDULE_FAILED // trop d'échecs consécutifs
)
func (s ScheduleStatus) String() string {
return [...]string{"active", "paused", "cancelled", "completed", "failed"}[s]
}
// PaymentSchedule pilote la récurrence des paiements d'un abonnement.
type PaymentSchedule struct {
SubscriptionID string `json:"subscription_id" bson:"subscription_id"`
Frequency pricing.PaymentType `json:"frequency" bson:"frequency"` // PAY_EVERY_WEEK / PAY_EVERY_MONTH / PAY_EVERY_YEAR
Amount float64 `json:"amount" bson:"amount"`
Currency string `json:"currency" bson:"currency"`
Status ScheduleStatus `json:"status" bson:"status"`
NextPaymentDate time.Time `json:"next_payment_date" bson:"next_payment_date"`
LastExecutedAt *time.Time `json:"last_executed_at,omitempty" bson:"last_executed_at,omitempty"`
FailureCount int `json:"failure_count" bson:"failure_count"`
MaxRetries int `json:"max_retries" bson:"max_retries" default:"3"`
}
// nextDate calcule la prochaine date selon la fréquence.
func (ps *PaymentSchedule) nextDate() time.Time {
switch ps.Frequency {
case pricing.PAY_EVERY_WEEK:
return ps.NextPaymentDate.AddDate(0, 0, 7)
case pricing.PAY_EVERY_MONTH:
return ps.NextPaymentDate.AddDate(0, 1, 0)
case pricing.PAY_EVERY_YEAR:
return ps.NextPaymentDate.AddDate(1, 0, 0)
}
return ps.NextPaymentDate
}
// Advance enregistre l'exécution réussie et avance à la prochaine échéance.
func (ps *PaymentSchedule) Advance() {
now := time.Now().UTC()
ps.LastExecutedAt = &now
ps.FailureCount = 0
ps.NextPaymentDate = ps.nextDate()
}
// RecordFailure incrémente le compteur d'échecs et désactive après MaxRetries.
func (ps *PaymentSchedule) RecordFailure() {
ps.FailureCount++
if ps.MaxRetries > 0 && ps.FailureCount >= ps.MaxRetries {
ps.Status = SCHEDULE_FAILED
}
}
// IsDue retourne vrai si le paiement est dû maintenant.
func (ps *PaymentSchedule) IsDue() bool {
return ps.Status == SCHEDULE_ACTIVE && !time.Now().UTC().Before(ps.NextPaymentDate)
}
// Pause suspend temporairement le calendrier.
func (ps *PaymentSchedule) Pause() {
if ps.Status == SCHEDULE_ACTIVE {
ps.Status = SCHEDULE_PAUSED
}
}
// Resume réactive un calendrier mis en pause.
func (ps *PaymentSchedule) Resume() {
if ps.Status == SCHEDULE_PAUSED {
ps.Status = SCHEDULE_ACTIVE
}
}
+136
View File
@@ -0,0 +1,136 @@
package refund
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type RefundStatus int
const (
REFUND_PENDING RefundStatus = iota
REFUND_APPROVED // approuvé, en attente de traitement
REFUND_REJECTED // rejeté
REFUND_PROCESSING // en cours de virement/blockchain
REFUND_COMPLETED // remboursé
REFUND_CANCELLED // annulé avant approbation
)
func (s RefundStatus) String() string {
return [...]string{"pending", "approved", "rejected", "processing", "completed", "cancelled"}[s]
}
func RefundStatusList() []RefundStatus {
return []RefundStatus{REFUND_PENDING, REFUND_APPROVED, REFUND_REJECTED, REFUND_PROCESSING, REFUND_COMPLETED, REFUND_CANCELLED}
}
// Refund représente une demande de remboursement sur un paiement validé.
type Refund struct {
utils.AbstractObject
PaymentID string `json:"payment_id" bson:"payment_id" validate:"required"`
BillID string `json:"bill_id,omitempty" bson:"bill_id,omitempty"`
InvoiceID string `json:"invoice_id,omitempty" bson:"invoice_id,omitempty"`
RefundType pricing.RefundType `json:"refund_type" bson:"refund_type"`
Amount float64 `json:"amount" bson:"amount" validate:"required"`
Currency string `json:"currency" bson:"currency" default:"EUR"`
Reason string `json:"reason,omitempty" bson:"reason,omitempty"`
Status RefundStatus `json:"status" bson:"status"`
RequestedAt time.Time `json:"requested_at" bson:"requested_at"`
ProcessedAt *time.Time `json:"processed_at,omitempty" bson:"processed_at,omitempty"`
ProcessedByID string `json:"processed_by_id,omitempty" bson:"processed_by_id,omitempty"`
TransactionID string `json:"transaction_id,omitempty" bson:"transaction_id,omitempty"`
Notes string `json:"notes,omitempty" bson:"notes,omitempty"`
// ratio appliqué sur le montant original (0-100). 0 = non renseigné (remboursement total).
RefundRatio float64 `json:"refund_ratio,omitempty" bson:"refund_ratio,omitempty"`
}
// NewRefund crée une demande de remboursement total.
func NewRefund(paymentID, billID string, amount float64, currency string, refundType pricing.RefundType, reason string) *Refund {
return &Refund{
PaymentID: paymentID,
BillID: billID,
Amount: amount,
Currency: currency,
RefundType: refundType,
Reason: reason,
Status: REFUND_PENDING,
RequestedAt: time.Now().UTC(),
}
}
// NewPartialRefund crée une demande de remboursement partiel selon un ratio pourcentage.
func NewPartialRefund(paymentID, billID string, originalAmount, ratioPercent float64, currency string, refundType pricing.RefundType, reason string) *Refund {
amount := originalAmount * ratioPercent / 100
r := NewRefund(paymentID, billID, amount, currency, refundType, reason)
r.RefundRatio = ratioPercent
return r
}
// Approve approuve la demande de remboursement.
func (r *Refund) Approve(processedByID string) {
r.Status = REFUND_APPROVED
r.ProcessedByID = processedByID
}
// Reject rejette la demande de remboursement.
func (r *Refund) Reject(processedByID, notes string) {
now := time.Now().UTC()
r.Status = REFUND_REJECTED
r.ProcessedByID = processedByID
r.ProcessedAt = &now
r.Notes = notes
}
// Process passe le remboursement en cours de traitement.
func (r *Refund) Process() bool {
if r.Status != REFUND_APPROVED {
return false
}
r.Status = REFUND_PROCESSING
return true
}
// Complete finalise le remboursement avec l'identifiant de transaction.
func (r *Refund) Complete(transactionID string) {
now := time.Now().UTC()
r.Status = REFUND_COMPLETED
r.TransactionID = transactionID
r.ProcessedAt = &now
}
// Cancel annule la demande si elle est encore en attente.
func (r *Refund) Cancel() bool {
if r.Status != REFUND_PENDING {
return false
}
r.Status = REFUND_CANCELLED
return true
}
func (r *Refund) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (r *Refund) StoreDraftDefault() {
r.IsDraft = true
}
func (r *Refund) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Refund)
if !r.IsDraft && r.Status != incoming.Status {
return true, &Refund{
Status: incoming.Status,
TransactionID: incoming.TransactionID,
Notes: incoming.Notes,
ProcessedByID: incoming.ProcessedByID,
}
}
return r.IsDraft, set
}
func (r *Refund) CanDelete() bool {
return r.IsDraft || r.Status == REFUND_PENDING
}
@@ -0,0 +1,22 @@
package refund
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type refundMongoAccessor struct {
utils.AbstractAccessor[*Refund]
}
func NewAccessor(request *tools.APIRequest) *refundMongoAccessor {
return &refundMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Refund]{
Logger: logs.CreateLogger(tools.REFUND.String()),
Request: request,
Type: tools.REFUND,
New: func() *Refund { return &Refund{} },
},
}
}
+194
View File
@@ -0,0 +1,194 @@
package subscription
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type SubscriptionStatus int
const (
SUBSCRIPTION_PENDING SubscriptionStatus = iota // en attente de premier paiement
SUBSCRIPTION_TRIAL // période d'essai
SUBSCRIPTION_ACTIVE // actif
SUBSCRIPTION_PAUSED // suspendu temporairement
SUBSCRIPTION_CANCELLED // résilié par l'utilisateur
SUBSCRIPTION_EXPIRED // date de fin dépassée
)
func (s SubscriptionStatus) String() string {
return [...]string{"pending", "trial", "active", "paused", "cancelled", "expired"}[s]
}
func SubscriptionStatusList() []SubscriptionStatus {
return []SubscriptionStatus{SUBSCRIPTION_PENDING, SUBSCRIPTION_TRIAL, SUBSCRIPTION_ACTIVE, SUBSCRIPTION_PAUSED, SUBSCRIPTION_CANCELLED, SUBSCRIPTION_EXPIRED}
}
// SubscriptionItem représente un élément d'un abonnement (ressource louée).
type SubscriptionItem struct {
ResourceID string `json:"resource_id" bson:"resource_id"`
ResourceType tools.DataType `json:"resource_type" bson:"resource_type"`
Quantity int `json:"quantity" bson:"quantity"`
UnitPrice float64 `json:"unit_price" bson:"unit_price"`
}
// Subscription représente un abonnement mensuel ou annuel à des ressources.
type Subscription struct {
utils.AbstractObject
SubscriberPeerID string `json:"subscriber_peer_id" bson:"subscriber_peer_id" validate:"required"`
ProviderPeerID string `json:"provider_peer_id,omitempty" bson:"provider_peer_id,omitempty"`
Status SubscriptionStatus `json:"status" bson:"status"`
PlanType pricing.PaymentType `json:"plan_type" bson:"plan_type"` // PAY_EVERY_MONTH ou PAY_EVERY_YEAR
Items []*SubscriptionItem `json:"items,omitempty" bson:"items,omitempty"`
Amount float64 `json:"amount" bson:"amount"`
Currency string `json:"currency" bson:"currency" default:"EUR"`
StartDate time.Time `json:"start_date" bson:"start_date"`
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"`
NextBillingDate time.Time `json:"next_billing_date" bson:"next_billing_date"`
AutoRenew bool `json:"auto_renew" bson:"auto_renew" default:"true"`
TrialEndDate *time.Time `json:"trial_end_date,omitempty" bson:"trial_end_date,omitempty"`
DiscountIDs []string `json:"discount_ids,omitempty" bson:"discount_ids,omitempty"`
CancelledAt *time.Time `json:"cancelled_at,omitempty" bson:"cancelled_at,omitempty"`
CancelReason string `json:"cancel_reason,omitempty" bson:"cancel_reason,omitempty"`
}
// newSubscription est le constructeur interne commun.
func newSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, plan pricing.PaymentType, nextBilling time.Time) *Subscription {
now := time.Now().UTC()
return &Subscription{
SubscriberPeerID: subscriberPeerID,
ProviderPeerID: providerPeerID,
Status: SUBSCRIPTION_PENDING,
PlanType: plan,
Items: items,
Amount: amount,
Currency: currency,
StartDate: now,
NextBillingDate: nextBilling,
AutoRenew: true,
}
}
// NewWeeklySubscription crée un abonnement hebdomadaire.
func NewWeeklySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
now := time.Now().UTC()
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_WEEK, now.AddDate(0, 0, 7))
}
// NewMonthlySubscription crée un abonnement mensuel.
func NewMonthlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
now := time.Now().UTC()
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_MONTH, now.AddDate(0, 1, 0))
}
// NewYearlySubscription crée un abonnement annuel.
func NewYearlySubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string) *Subscription {
now := time.Now().UTC()
return newSubscription(subscriberPeerID, providerPeerID, items, amount, currency, pricing.PAY_EVERY_YEAR, now.AddDate(1, 0, 0))
}
// NewTrialSubscription crée un abonnement mensuel avec période d'essai.
func NewTrialSubscription(subscriberPeerID, providerPeerID string, items []*SubscriptionItem, amount float64, currency string, trialDays int) *Subscription {
now := time.Now().UTC()
trialEnd := now.AddDate(0, 0, trialDays)
s := NewMonthlySubscription(subscriberPeerID, providerPeerID, items, amount, currency)
s.Status = SUBSCRIPTION_TRIAL
s.TrialEndDate = &trialEnd
s.NextBillingDate = trialEnd
return s
}
// Activate passe l'abonnement au statut actif (après premier paiement).
func (s *Subscription) Activate() {
s.Status = SUBSCRIPTION_ACTIVE
}
// Pause suspend l'abonnement.
func (s *Subscription) Pause() {
if s.Status == SUBSCRIPTION_ACTIVE {
s.Status = SUBSCRIPTION_PAUSED
}
}
// Resume réactive un abonnement suspendu.
func (s *Subscription) Resume() {
if s.Status == SUBSCRIPTION_PAUSED {
s.Status = SUBSCRIPTION_ACTIVE
}
}
// Cancel résilie l'abonnement.
func (s *Subscription) Cancel(reason string) {
now := time.Now().UTC()
s.Status = SUBSCRIPTION_CANCELLED
s.CancelledAt = &now
s.CancelReason = reason
s.AutoRenew = false
}
// Renew avance la prochaine date de facturation d'une période.
func (s *Subscription) Renew() {
switch s.PlanType {
case pricing.PAY_EVERY_WEEK:
s.NextBillingDate = s.NextBillingDate.AddDate(0, 0, 7)
case pricing.PAY_EVERY_MONTH:
s.NextBillingDate = s.NextBillingDate.AddDate(0, 1, 0)
case pricing.PAY_EVERY_YEAR:
s.NextBillingDate = s.NextBillingDate.AddDate(1, 0, 0)
}
}
// IsExpired vérifie si l'abonnement a dépassé sa date de fin.
func (s *Subscription) IsExpired() bool {
if s.EndDate == nil {
return false
}
return time.Now().UTC().After(*s.EndDate)
}
// IsBillingDue vérifie si la prochaine échéance est atteinte.
func (s *Subscription) IsBillingDue() bool {
return s.Status == SUBSCRIPTION_ACTIVE && !time.Now().UTC().Before(s.NextBillingDate)
}
// IsInTrial vérifie si l'abonnement est en période d'essai.
func (s *Subscription) IsInTrial() bool {
return s.Status == SUBSCRIPTION_TRIAL && s.TrialEndDate != nil && time.Now().UTC().Before(*s.TrialEndDate)
}
// ComputeAmount recalcule le montant total depuis les items.
func (s *Subscription) ComputeAmount() float64 {
total := 0.0
for _, item := range s.Items {
total += item.UnitPrice * float64(item.Quantity)
}
s.Amount = total
return total
}
func (s *Subscription) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (s *Subscription) StoreDraftDefault() {
s.IsDraft = true
}
func (s *Subscription) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
incoming := set.(*Subscription)
if !s.IsDraft && s.Status != incoming.Status {
return true, &Subscription{
Status: incoming.Status,
AutoRenew: incoming.AutoRenew,
CancelReason: incoming.CancelReason,
}
}
return s.IsDraft, set
}
func (s *Subscription) CanDelete() bool {
return s.IsDraft || s.Status == SUBSCRIPTION_CANCELLED
}
@@ -0,0 +1,22 @@
package subscription
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type subscriptionMongoAccessor struct {
utils.AbstractAccessor[*Subscription]
}
func NewAccessor(request *tools.APIRequest) *subscriptionMongoAccessor {
return &subscriptionMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Subscription]{
Logger: logs.CreateLogger(tools.SUBSCRIPTION.String()),
Request: request,
Type: tools.SUBSCRIPTION,
New: func() *Subscription { return &Subscription{} },
},
}
}
@@ -1,9 +1,9 @@
package bill_test
package billing_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/bill"
"cloud.o-forge.io/core/oc-lib/models/billing"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/utils"
@@ -12,71 +12,67 @@ import (
"github.com/stretchr/testify/require"
)
// ---- Bill model ----
func TestBill_StoreDraftDefault(t *testing.T) {
b := &bill.Bill{}
b := &billing.Bill{}
b.StoreDraftDefault()
assert.True(t, b.IsDraft)
}
func TestBill_CanDelete_Draft(t *testing.T) {
b := &bill.Bill{}
b := &billing.Bill{}
b.IsDraft = true
assert.True(t, b.CanDelete())
}
func TestBill_CanDelete_NonDraft(t *testing.T) {
b := &bill.Bill{}
b := &billing.Bill{}
b.IsDraft = false
assert.False(t, b.CanDelete())
}
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING}
b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = false
set := &bill.Bill{Status: enum.PAID}
set := &billing.Bill{Status: enum.PAID}
ok, returned := b.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.PAID, returned.(*bill.Bill).Status)
assert.Equal(t, enum.PAID, returned.(*billing.Bill).Status)
}
func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING}
b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = false
set := &bill.Bill{Status: enum.PENDING}
set := &billing.Bill{Status: enum.PENDING}
ok, _ := b.CanUpdate(set)
assert.False(t, ok)
}
func TestBill_CanUpdate_Draft(t *testing.T) {
b := &bill.Bill{Status: enum.PENDING}
b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = true
set := &bill.Bill{Status: enum.PAID}
set := &billing.Bill{Status: enum.PAID}
ok, _ := b.CanUpdate(set)
assert.True(t, ok)
}
func TestBill_GetAccessor(t *testing.T) {
b := &bill.Bill{}
b := &billing.Bill{}
acc := b.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestBill_GetAccessor_NilRequest(t *testing.T) {
b := &bill.Bill{}
b := &billing.Bill{}
acc := b.GetAccessor(nil)
assert.NotNil(t, acc)
}
// ---- GenerateBill ----
func TestGenerateBill_Basic(t *testing.T) {
o := &order.Order{
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
}
req := &tools.APIRequest{PeerID: "peer-abc"}
b, err := bill.GenerateBill(o, req)
b, err := billing.GenerateBill(o, req)
require.NoError(t, err)
assert.NotNil(t, b)
assert.Equal(t, "order-uuid-1", b.OrderID)
@@ -85,10 +81,8 @@ func TestGenerateBill_Basic(t *testing.T) {
assert.Contains(t, b.Name, "peer-abc")
}
// ---- SumUpBill ----
func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
b := &bill.Bill{Total: 0}
b := &billing.Bill{Total: 0}
result, err := b.SumUpBill(nil)
require.NoError(t, err)
assert.Equal(t, 0.0, result.Total)
+25 -2
View File
@@ -5,16 +5,20 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* Booking is a struct that represents a booking
*/
type Booking struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty"` // We need to add the validate:"required" tag once the pricing feature is implemented, removed to avoid handling the error
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
FromNano string `json:"from_nano,omitempty" bson:"priced_item,omitempty"`
PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty"` // We need to add the validate:"required" tag once the pricing feature is implemented, removed to avoid handling the error
ResumeMetrics map[string]map[string]models.MetricResume `json:"resume_metrics,omitempty" bson:"resume_metrics,omitempty"`
ExecutionMetrics map[string][]models.MetricsSnapshot `json:"metrics,omitempty" bson:"metrics,omitempty"`
@@ -37,6 +41,25 @@ type Booking struct {
// Authorization: identifies who created this draft and the Check session it belongs to.
// Used to verify UPDATE and DELETE orders from remote schedulers.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
// Peerless is true when the booked resource has no destination peer
// (e.g. a public Docker Hub image). No peer confirmation or pricing
// negotiation is needed; the booking is stored locally only.
Peerless bool `json:"peerless,omitempty" bson:"peerless,omitempty"`
// OriginRef carries the registry reference of a peerless resource
// (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it.
OriginRef string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"`
// BillingStrategy est la fréquence de facturation appliquée à ce booking
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
// Transmis depuis WorkflowExecution.SelectedBillingStrategy lors du Book().
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
// PaymentType est le mode de paiement choisi pour cette ressource spécifique
// (PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR).
// Résolu depuis WorkflowExecution.SelectedPaymentMode[itemID] lors du Book().
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
}
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
+8 -18
View File
@@ -1,23 +1,13 @@
package booking
type BookingMode int
import "cloud.o-forge.io/core/oc-lib/models/common/enum"
// BookingMode is kept here as an alias for backward compatibility.
// The canonical definition lives in models/common/enum.
type BookingMode = enum.BookingMode
const (
PLANNED BookingMode = iota // predictible
PREEMPTED // can be both predictible or unpredictible, first one asking for a quick exec, second on event, but we pay to preempt in any case.
WHEN_POSSIBLE // unpredictable, two mode of payment can be available on that case: fixed, or per USE
PLANNED = enum.PLANNED
PREEMPTED = enum.PREEMPTED
WHEN_POSSIBLE = enum.WHEN_POSSIBLE
)
/*
Ok make a point there:
There is 3 notions about booking & payment :
Booking mode : WHEN is executed
Buying mode : Duration of payment
Pricing Mode : How Many time we pay
We can simplify Buying Mode and Pricing Mode, some Buying Mode implied limited pricing mode
Such as Rules. Just like PERMANENT BUYING can be paid only once.
Booking Mode on WHEN POSSIBLE make an exception, because we can't know when executed.
*/
+88 -19
View File
@@ -8,16 +8,18 @@ import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
// InstanceCapacity holds the maximum available resources of a single resource instance.
type InstanceCapacity struct {
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
}
// ResourceRequest describes the resource amounts needed for a prospective booking.
@@ -47,11 +49,14 @@ type PlannerITF interface {
}
// Planner is a volatile (non-persisted) object that organises bookings by resource.
// Only ComputeResource and StorageResource bookings appear in the schedule.
// ComputeResource, StorageResource and HOSTED ServiceResource bookings appear in the schedule.
// BlockedResources marks resources for which no matching Live was found at generation time:
// any availability check against a blocked resource returns false immediately.
type Planner struct {
GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
}
// Generate builds a full Planner from all active bookings.
@@ -74,7 +79,7 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
And: map[string][]dbs.Filter{
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
},
}, "*", false)
}, "*", false, 0, 10000)
if code != 200 || err != nil {
return nil, err
}
@@ -82,13 +87,14 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
And: map[string][]dbs.Filter{
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
},
}, "*", true)
}, "*", true, 0, 10000)
bookings := append(confirmed, drafts...)
p := &Planner{
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
BlockedResources: map[string]bool{},
}
for _, b := range bookings {
@@ -100,8 +106,10 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
continue
}
// Only compute and storage resources are eligible
if bk.ResourceType != tools.COMPUTE_RESOURCE && bk.ResourceType != tools.STORAGE_RESOURCE {
// Eligible resource types: compute, storage, and HOSTED services.
if bk.ResourceType != tools.COMPUTE_RESOURCE &&
bk.ResourceType != tools.STORAGE_RESOURCE &&
bk.ResourceType != tools.SERVICE_RESOURCE {
continue
}
@@ -111,7 +119,11 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
end = &e
}
instanceID, usage, cap := extractSlotData(bk, request)
instanceID, usage, cap, blocked := extractSlotData(bk, request)
if blocked {
p.BlockedResources[bk.ResourceID] = true
continue
}
if instanceID == "" {
instanceID = bk.InstanceID
}
@@ -151,6 +163,9 @@ func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
// Slots targeting other instances are ignored.
// If no capacity is known for this instance (never booked), it is fully available.
func (p *Planner) Check(resourceID string, instanceID string, req *ResourceRequest, start time.Time, end *time.Time) bool {
if p.BlockedResources[resourceID] {
return false // no Live found at generation time — cannot book
}
if end == nil {
e := start.Add(5 * time.Minute)
end = &e
@@ -265,6 +280,11 @@ func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float
pct["storage"] = (*req.StorageGB / cap.StorageGB) * 100.0
}
// HOSTED service: each booking consumes one call slot.
if cap.MaxConcurrent > 0 {
pct["calls"] = (1.0 / cap.MaxConcurrent) * 100.0
}
return pct
}
@@ -272,9 +292,11 @@ func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float
// Internal helpers
// ---------------------------------------------------------------------------
// extractSlotData parses the booking's PricedItem, loads the corresponding resource,
// and returns the instance ID, usage percentages, and instance capacity in a single pass.
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
// extractSlotData parses the booking's PricedItem, loads the corresponding Live resource
// as the authoritative capacity source, and returns the instance ID, usage percentages,
// capacity, and whether a matching Live was found.
// blocked=true means no Live exists for this resource; the resource must not be scheduled.
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
usage = map[string]float64{}
if len(bk.PricedItem) == 0 {
return
@@ -289,6 +311,8 @@ func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID
instanceID, usage, cap = extractComputeSlot(b, bk.ResourceID, request)
case tools.STORAGE_RESOURCE:
instanceID, usage, cap = extractStorageSlot(b, bk.ResourceID, request)
case tools.SERVICE_RESOURCE:
instanceID, usage, cap, blocked = extractServiceSlot(b, bk.ResourceID, request)
}
return
}
@@ -381,6 +405,51 @@ func extractStorageSlot(pricedJSON []byte, resourceID string, request *tools.API
return
}
// extractServiceSlot extracts the instance ID, usage, and capacity for a HOSTED service booking.
// The LiveService is the authoritative source for MaxConcurrent — the ServiceResource is not trusted.
// If no LiveService references this resourceID, blocked=true signals the resource cannot be scheduled.
func extractServiceSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
usage = map[string]float64{}
var priced resources.PricedServiceResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
blocked = true
return
}
// LiveService is the authoritative capacity source — look it up by resources_id.
liveResults, _, err := (&live.LiveService{}).GetAccessor(request).Search(
&dbs.Filters{
And: map[string][]dbs.Filter{
"resources_id": {{Operator: dbs.EQUAL.String(), Value: resourceID}},
},
}, "*", false, 0, 1)
if err != nil || len(liveResults) == 0 {
blocked = true // no Live → cannot schedule
return
}
ls := liveResults[0].(*live.LiveService)
if ls.MaxConcurrent <= 0 {
blocked = true
return
}
// Instance ID: use the first instance referenced by the priced item.
instanceID = priced.GetID()
if instanceID == "" {
instanceID = resourceID // fallback: treat the resource itself as the instance key
}
maxC := float64(ls.MaxConcurrent)
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
MaxConcurrent: maxC,
}
usage["calls"] = (1.0 / maxC) * 100.0
return
}
// findComputeInstance returns the instance referenced by the priced item's InstancesRefs,
// falling back to the first available instance.
func findComputeInstance(compute *resources.ComputeResource, refs map[string]string) *resources.ComputeResourceInstance {
@@ -91,7 +91,7 @@ func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils
Or: map[string][]dbs.Filter{
"abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}},
},
}, "", isDrafted)
}, "", isDrafted, 0, int64(len(arr)))
if code == 200 {
for _, r := range res {
new = append(new, r.(T))
+16
View File
@@ -0,0 +1,16 @@
package enum
type BookingMode int
const (
PLANNED BookingMode = iota // timing prédictible
PREEMPTED // peut être interrompu, premium payé
WHEN_POSSIBLE // timing imprévisible
)
/*
3 notions distinctes :
- BookingMode : QUAND est exécuté (PLANNED / PREEMPTED / WHEN_POSSIBLE)
- BillingStrategy : fréquence de facturation (ONCE / WEEKLY / MONTHLY / YEARLY)
- PaymentType : mode de paiement par ressource (PAY_ONCE / PAY_EVERY_MONTH / ...)
*/
+10
View File
@@ -1,5 +1,7 @@
package enum
import "fmt"
type InfrastructureType int
const (
@@ -18,3 +20,11 @@ func (t InfrastructureType) String() string {
func InfrastructureList() []InfrastructureType {
return []InfrastructureType{DOCKER, KUBERNETES, SLURM, HW, CONDOR}
}
func (d InfrastructureType) Compare(indexStr interface{}) bool {
return fmt.Sprintf("%v", indexStr) == fmt.Sprintf("%v", d.EnumIndex()) || fmt.Sprintf("%v", indexStr) == d.String()
}
func (d InfrastructureType) EnumIndex() int {
return int(d)
}
+10
View File
@@ -1,5 +1,7 @@
package enum
import "fmt"
type StorageSize int
// StorageType - Enum that defines the type of storage
@@ -54,3 +56,11 @@ func (t StorageType) String() string {
func TypeList() []StorageType {
return []StorageType{FILE, STREAM, API, DATABASE, S3, MEMORY, HARDWARE, AZURE, GCS}
}
func (d StorageType) Compare(indexStr interface{}) bool {
return fmt.Sprintf("%v", indexStr) == fmt.Sprintf("%v", d.EnumIndex()) || fmt.Sprintf("%v", indexStr) == d.String()
}
func (d StorageType) EnumIndex() int {
return int(d)
}
+3 -1
View File
@@ -32,6 +32,7 @@ const (
FORGOTTEN
DELAYED
CANCELLED
IN_PREPARATION
)
var str = [...]string{
@@ -43,6 +44,7 @@ var str = [...]string{
"forgotten",
"delayed",
"cancelled",
"in_preparation",
}
func FromInt(i int) string {
@@ -60,5 +62,5 @@ func (d BookingStatus) EnumIndex() int {
// List
func StatusList() []BookingStatus {
return []BookingStatus{DRAFT, SCHEDULED, STARTED, FAILURE, SUCCESS, FORGOTTEN, DELAYED, CANCELLED}
return []BookingStatus{DRAFT, SCHEDULED, STARTED, FAILURE, SUCCESS, FORGOTTEN, DELAYED, CANCELLED, IN_PREPARATION}
}
+67 -7
View File
@@ -1,13 +1,73 @@
package models
type Container struct {
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
Args string `json:"args,omitempty" bson:"args,omitempty"` // Args is the container arguments
Env map[string]string `json:"env,omitempty" bson:"env,omitempty"` // Env is the container environment variables
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
import "sort"
Exposes []Expose `bson:"exposes,omitempty" json:"exposes,omitempty"` // Expose is the execution
// SortedArgValues returns arg Values: readonly args first (sorted by Index), then non-readonly in order.
func SortedArgValues(args []Arg) []string {
var ro, nro []Arg
for _, a := range args {
if a.IsReadonly {
ro = append(ro, a)
} else {
nro = append(nro, a)
}
}
sort.Slice(ro, func(i, j int) bool { return ro[i].Index < ro[j].Index })
out := make([]string, 0, len(args))
for _, a := range ro {
out = append(out, a.Value)
}
for _, a := range nro {
out = append(out, a.Value)
}
return out
}
// ReadonlyArgValues returns only the readonly arg values sorted by Index.
func ReadonlyArgValues(args []Arg) []string {
var ro []Arg
for _, a := range args {
if a.IsReadonly {
ro = append(ro, a)
}
}
sort.Slice(ro, func(i, j int) bool { return ro[i].Index < ro[j].Index })
out := make([]string, 0, len(ro))
for _, a := range ro {
out = append(out, a.Value)
}
return out
}
// NonReadonlyArgValues returns only the non-readonly arg values in their order.
func NonReadonlyArgValues(args []Arg) []string {
out := make([]string, 0)
for _, a := range args {
if !a.IsReadonly {
out = append(out, a.Value)
}
}
return out
}
type Arg struct {
Value string `json:"value,omitempty" bson:"value,omitempty"` // Image is the container image TEMPO
Index int `json:"index,omitempty" bson:"index,omitempty"`
IsReadonly bool `json:"is_readonly,omitempty" bson:"is_readonly,omitempty"`
}
type PathSource struct {
Source string `json:"source,omitempty" bson:"source,omitempty"` // Image is the container image TEMPO
IsReachable bool `json:"is_reachable,omitempty" bson:"is_reachable,omitempty"`
Args []Arg `json:"args,omitempty" bson:"args,omitempty"` // Args is the container arguments
Volumes map[string]string `json:"volumes,omitempty" bson:"volumes,omitempty"` // Volumes is the container volumes
}
type Container struct {
PathSource
Image string `json:"image,omitempty" bson:"image,omitempty"` // Image is the container image TEMPO
Command string `json:"command,omitempty" bson:"command,omitempty"` // Command is the container command
}
type Expose struct {
+6 -6
View File
@@ -7,12 +7,12 @@ type Artifact struct {
}
type Param struct {
Name string `json:"name" bson:"name" validate:"required"`
Attr string `json:"attr,omitempty" bson:"attr,omitempty"`
Value string `json:"value,omitempty" bson:"value,omitempty"`
Origin string `json:"origin,omitempty" bson:"origin,omitempty"`
Readonly bool `json:"readonly" bson:"readonly" default:"true"`
Optionnal bool `json:"optionnal" bson:"optionnal" default:"true"`
Name string `json:"name" bson:"name" validate:"required"`
Attr string `json:"attr,omitempty" bson:"attr,omitempty"`
Value string `json:"value,omitempty" bson:"value,omitempty"`
Origin string `json:"origin,omitempty" bson:"origin,omitempty"`
Readonly bool `json:"readonly" bson:"readonly" default:"true"`
Required bool `json:"required" bson:"required" default:"true"`
}
type InOutputs struct {
+9 -7
View File
@@ -27,16 +27,18 @@ func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[stri
return near
}
// GetPlannerLongestTime returns the sum of all processing durations (conservative estimate).
// Returns -1 if any processing is a service (open-ended).
// GetPlannerLongestTime returns the sum of all processing+service durations.
// Returns -1 if any item is open-ended (no deadline).
func GetPlannerLongestTime(planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
longestTime := float64(0)
for _, priced := range planned[tools.PROCESSING_RESOURCE] {
d := priced.GetExplicitDurationInS()
if d < 0 {
return -1 // service present: booking is open-ended
for _, dt := range []tools.DataType{tools.PROCESSING_RESOURCE, tools.SERVICE_RESOURCE} {
for _, priced := range planned[dt] {
d := priced.GetExplicitDurationInS()
if d < 0 {
return -1
}
longestTime += d
}
longestTime += d
}
return longestTime
}
+3 -2
View File
@@ -3,19 +3,20 @@ package pricing
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/tools"
)
type PricedItemITF interface {
GetID() string
GetName() string
GetInstanceID() string
GetType() tools.DataType
IsPurchasable() bool
IsBooked() bool
GetQuantity() int
AddQuantity(amount int)
GetBookingMode() booking.BookingMode
GetBookingMode() enum.BookingMode
GetCreatorID() string
SelectPricing() PricingProfileITF
GetLocationStart() *time.Time
+21 -3
View File
@@ -28,10 +28,28 @@ func RefundTypeList() []RefundType {
return []RefundType{REFUND_DEAD_END, REFUND_ON_ERROR, REFUND_ON_EARLY_END}
}
type PaymentType int
const (
PAY_ONCE PaymentType = iota
PAY_EVERY_WEEK
PAY_EVERY_MONTH
PAY_EVERY_YEAR
)
func (t PaymentType) String() string {
return [...]string{"PAY ONCE", "PAY_EVERY_WEEK", "PAY_EVERY_MONTH", "PAY_EVERY_YEAR"}[t]
}
func PaymentTypeList() []PaymentType {
return []PaymentType{PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR}
}
type AccessPricingProfile[T Strategy] struct { // only use for acces such as : DATA && PROCESSING
Pricing PricingStrategy[T] `json:"pricing,omitempty" bson:"pricing,omitempty"` // Price is the price of the resource
DefaultRefund RefundType `json:"default_refund" bson:"default_refund"` // DefaultRefund is the default refund type of the pricing
RefundRatio int32 `json:"refund_ratio" bson:"refund_ratio" default:"0"` // RefundRatio is the refund ratio if missing
AllowedPaymentType []PaymentType `json:"allowed_payment_type,omitempty" bson:"allowed_payment_type,omitempty"` // Price is the price of the resource
Pricing PricingStrategy[T] `json:"pricing,omitempty" bson:"pricing,omitempty"` // Price is the price of the resource
DefaultRefund RefundType `json:"default_refund" bson:"default_refund"` // DefaultRefund is the default refund type of the pricing
RefundRatio int32 `json:"refund_ratio" bson:"refund_ratio" default:"0"` // RefundRatio is the refund ratio if missing
}
func (a AccessPricingProfile[T]) IsBooked() bool {
+10 -8
View File
@@ -111,13 +111,11 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
fromAverageDuration := after.Sub(now).Seconds()
var tEnd time.Time
if end == nil {
tEnd = start.Add(5 * time.Minute)
} else {
fromDateDuration := float64(0)
if end != nil {
tEnd = *end
fromDateDuration = tEnd.Sub(start).Seconds()
}
fromDateDuration := tEnd.Sub(start).Seconds()
if fromAverageDuration > fromDateDuration {
return fromAverageDuration
}
@@ -126,6 +124,9 @@ func getAverageTimeInSecond(averageTimeInSecond float64, start time.Time, end *t
func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInSecond float64, start time.Time, end *time.Time) (float64, error) {
locationDurationInSecond = getAverageTimeInSecond(locationDurationInSecond, start, end)
if locationDurationInSecond <= 0 {
return 0, nil
}
priceStr := fmt.Sprintf("%v", price)
p, err := strconv.ParseFloat(priceStr, 64)
if err != nil {
@@ -152,8 +153,10 @@ func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInS
// may suppress in pricing strategy -> to set in map
type PricingStrategy[T Strategy] struct {
Price float64 `json:"price" bson:"price" default:"0"` // Price is the Price of the pricing
Currency string `json:"currency" bson:"currency" default:"USD"` // Currency is the currency of the pricing
Price float64 `json:"price" bson:"price" default:"0"` // Price is the Price of the pricing
Currency string `json:"currency" bson:"currency" default:"USD"` // Currency is the currency of the pricing
// NO NEED ?
BuyingStrategy BuyingStrategy `json:"buying_strategy" bson:"buying_strategy" default:"0"` // BuyingStrategy is the buying strategy of the pricing
TimePricingStrategy TimePricingStrategy `json:"time_pricing_strategy" bson:"time_pricing_strategy" default:"0"` // TimePricingStrategy is the time pricing strategy of the pricing
OverrideStrategy T `json:"override_strategy" bson:"override_strategy" default:"-1"` // Modulation is the modulation of the pricing
@@ -174,7 +177,6 @@ func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration
}
return p.Price, nil
case PERMANENT:
if variations != nil {
price := p.Price
@@ -18,6 +18,20 @@ type ExecutionVerification struct {
Validate bool `json:"validate" bson:"validate,omitempty"`
}
func (ri *ExecutionVerification) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "wokflow":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW)
}
}
return ext
}
func (r *ExecutionVerification) StoreDraftDefault() {
r.IsDraft = false // TODO: TEMPORARY
}
+1 -6
View File
@@ -1,18 +1,13 @@
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
IsCompatible(service map[string]interface{}) bool
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
}
+27 -5
View File
@@ -3,7 +3,6 @@ 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"
@@ -32,18 +31,41 @@ type LiveCerts struct {
}
// TODO in the future multiple type of certs depending of infra type
type GeoPoint struct {
Latitude float64 `json:"latitude,omitempty" bson:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty" bson:"longitude,omitempty"`
}
type AbstractLive struct {
utils.AbstractObject
Certs LiveCerts `json:"certs,omitempty" bson:"certs,omitempty"`
MonitorPath string `json:"monitor_path,omitempty" bson:"monitor_path,omitempty"`
Location resources.GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
ResourcesID []string `json:"resources_id" bson:"resources_id"`
}
func (ri *AbstractLive) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "resource":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW_RESOURCE)
ext[t] = append(ext[t], tools.DATA_RESOURCE)
ext[t] = append(ext[t], tools.COMPUTE_RESOURCE)
ext[t] = append(ext[t], tools.STORAGE_RESOURCE)
ext[t] = append(ext[t], tools.PROCESSING_RESOURCE)
ext[t] = append(ext[t], tools.SERVICE_RESOURCE)
}
}
return ext
}
func (d *AbstractLive) GetMonitorPath() string {
return d.MonitorPath
}
@@ -52,9 +74,9 @@ 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 (d *AbstractLive) SetResourcesID(resourcesid string) {
if slices.Contains(d.ResourcesID, resourcesid) {
d.ResourcesID = append(d.ResourcesID, resourcesid)
}
}
+23 -26
View File
@@ -1,9 +1,10 @@
package live
import (
"fmt"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
@@ -12,39 +13,35 @@ import (
* LiveDatacenter is a struct that represents a compute units in your datacenters
*/
type ComputeNode struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Quantity int64 `json:"quantity" bson:"quantity" default:"1"`
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"` // RAM is the RAM
CPUs map[string]int64 `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]int64 `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
}
type LiveDatacenter struct {
AbstractLive
StorageType enum.StorageType `bson:"storage_type" json:"storage_type" default:"-1"` // Type is the type of the storage
Acronym string `bson:"acronym,omitempty" json:"acronym,omitempty"` // Acronym is the acronym of the storage
Architecture string `json:"architecture,omitempty" bson:"architecture,omitempty"` // Architecture is the architecture
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*resources.ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
Architecture string `json:"architecture,omitempty" bson:"architecture,omitempty"` // Architecture is the architecture
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
}
func (r *LiveDatacenter) IsCompatible(service map[string]interface{}) bool {
fmt.Println("COMPARE <", r.Infrastructure.Compare(service["infrastructure"]), "> AND <", service["architecture"], "> <", r.Architecture, ">")
return r.Infrastructure.Compare(service["infrastructure"]) && service["architecture"] == r.Architecture
}
func (d *LiveDatacenter) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveDatacenter](tools.LIVE_DATACENTER, request) // Create a new instance of the accessor
}
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
}
+22 -56
View File
@@ -1,9 +1,7 @@
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"
@@ -26,66 +24,34 @@ func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *
return &LiveDatacenter{}
case tools.LIVE_STORAGE:
return &LiveStorage{}
case tools.LIVE_SERVICE:
return &LiveService{}
}
return &LiveDatacenter{}
},
NotImplemented: []string{"CopyOne"},
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *liveMongoAccessor[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)
func (wfa *liveMongoAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit)
}
if len(live.GetResourcesID()) > 0 {
for _, r := range live.GetResourcesID() {
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.Serialize(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 := utils.GenericStoreOne(r, resAccess)
if err != nil {
return nil, code, err
}
live.SetResourcesID(res.GetID())
if live.GetID() != "" {
return a.UpdateOne(live.Serialize(live), live.GetID())
} else {
return a.StoreOne(live)
}
func (wfa *liveMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
if filters == nil && search == "*" {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa, offset, limit)
}
return utils.GenericSearch[T](filters, search, wfa.New().GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa, offset, limit)
}
func (a *liveMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}
+46
View File
@@ -0,0 +1,46 @@
package live
import (
"fmt"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type ServiceProtocol int
const (
HTTP ServiceProtocol = iota
GRPC
WEBSOCKET
TCP
)
func (p ServiceProtocol) String() string {
return [...]string{"HTTP", "GRPC", "WEBSOCKET", "TCP"}[p]
}
// LiveService is the authoritative description of a hosted service run by the peer.
// MaxConcurrent is the only capacity dimension that matters for scheduling:
// it caps the number of simultaneous callers the service can accept.
// All other service metadata (endpoint, protocol) is live-verified here
// rather than trusted from the ServiceResource, which may be stale.
type LiveService struct {
AbstractLive
MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"`
Protocol ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,omitempty"`
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
}
func (d *LiveService) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveService](tools.LIVE_SERVICE, request)
}
func (r *LiveService) IsCompatible(service map[string]interface{}) bool {
fmt.Println("COMPARE <", service["infrastructure"], "> <", r.Infrastructure, ">")
return r.Infrastructure.Compare(service["infrastructure"])
}
+9 -17
View File
@@ -1,8 +1,9 @@
package live
import (
"fmt"
"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"
)
@@ -14,6 +15,7 @@ import (
type LiveStorage struct {
AbstractLive
StorageType enum.StorageType `bson:"storage_type" json:"storage_type" default:"-1"`
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
Path string `bson:"path,omitempty" json:"path,omitempty"` // Path is the store folders in the source
Local bool `bson:"local" json:"local"`
@@ -25,22 +27,12 @@ type LiveStorage struct {
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
}
func (r *LiveStorage) IsCompatible(service map[string]interface{}) bool {
fmt.Println("COMPARE <", r.StorageType.Compare(service["storage_type"]), ">")
return r.StorageType.Compare(service["storage_type"])
}
func (d *LiveStorage) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveStorage](tools.LIVE_STORAGE, request) // Create a new instance of the accessor
}
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
}
-24
View File
@@ -79,18 +79,6 @@ func TestLiveDatacenter_GetAccessor_NilRequest(t *testing.T) {
assert.NotNil(t, acc)
}
func TestLiveDatacenter_GetResource(t *testing.T) {
dc := &live.LiveDatacenter{}
res := dc.GetResource()
assert.NotNil(t, res)
}
func TestLiveDatacenter_GetResourceInstance(t *testing.T) {
dc := &live.LiveDatacenter{}
inst := dc.GetResourceInstance()
assert.NotNil(t, inst)
}
func TestLiveDatacenter_IDAndName(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.AbstractLive.AbstractObject = utils.AbstractObject{UUID: "dc-id", Name: "dc-name"}
@@ -124,18 +112,6 @@ func TestLiveStorage_GetAccessor(t *testing.T) {
assert.NotNil(t, acc)
}
func TestLiveStorage_GetResource(t *testing.T) {
s := &live.LiveStorage{}
res := s.GetResource()
assert.NotNil(t, res)
}
func TestLiveStorage_GetResourceInstance(t *testing.T) {
s := &live.LiveStorage{}
inst := s.GetResourceInstance()
assert.NotNil(t, inst)
}
func TestLiveStorage_SetResourcesID_NoDuplication(t *testing.T) {
s := &live.LiveStorage{}
s.SetResourcesID("storage-1")
+15 -3
View File
@@ -2,11 +2,17 @@ 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/allowed_image"
"cloud.o-forge.io/core/oc-lib/models/billing"
"cloud.o-forge.io/core/oc-lib/models/billing/discount"
"cloud.o-forge.io/core/oc-lib/models/billing/payment"
"cloud.o-forge.io/core/oc-lib/models/billing/refund"
"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
"cloud.o-forge.io/core/oc-lib/models/execution_verification"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"cloud.o-forge.io/core/oc-lib/models/booking"
@@ -14,7 +20,6 @@ import (
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
"cloud.o-forge.io/core/oc-lib/models/peer"
resource "cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
w3 "cloud.o-forge.io/core/oc-lib/models/workspace"
@@ -30,6 +35,7 @@ var ModelsCatalog = map[string]func() utils.DBObject{
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
tools.STORAGE_RESOURCE.String(): func() utils.DBObject { return &resource.StorageResource{} },
tools.PROCESSING_RESOURCE.String(): func() utils.DBObject { return &resource.ProcessingResource{} },
tools.SERVICE_RESOURCE.String(): func() utils.DBObject { return &resource.ServiceResource{} },
tools.NATIVE_TOOL.String(): func() utils.DBObject { return &resource.NativeTool{} },
tools.WORKFLOW.String(): func() utils.DBObject { return &w2.Workflow{} },
tools.WORKFLOW_EXECUTION.String(): func() utils.DBObject { return &workflow_execution.WorkflowExecution{} },
@@ -44,8 +50,14 @@ var ModelsCatalog = map[string]func() utils.DBObject{
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{} },
tools.LIVE_SERVICE.String(): func() utils.DBObject { return &live.LiveService{} },
tools.BILL.String(): func() utils.DBObject { return &billing.Bill{} },
tools.PAYMENT.String(): func() utils.DBObject { return &payment.Payment{} },
tools.REFUND.String(): func() utils.DBObject { return &refund.Refund{} },
tools.DISCOUNT.String(): func() utils.DBObject { return &discount.Discount{} },
tools.SUBSCRIPTION.String(): func() utils.DBObject { return &subscription.Subscription{} },
tools.EXECUTION_VERIFICATION.String(): func() utils.DBObject { return &execution_verification.ExecutionVerification{} },
tools.ALLOWED_IMAGE.String(): func() utils.DBObject { return &allowed_image.AllowedImage{} },
}
// Model returns the model object based on the model type
+9 -4
View File
@@ -17,12 +17,17 @@ import (
type Order struct {
utils.AbstractObject
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"`
ExecutionsID string `json:"executions_id" bson:"executions_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"`
Bookings []*booking.Booking `json:"bookings" bson:"bookings"`
// Billing groupe les bookings par fréquence de facturation, peuplé par GenerateOrder.
Billing map[pricing.BillingStrategy][]*booking.Booking `json:"billing" bson:"billing"`
// SubscriptionIDs liste les abonnements récurrents créés pour cet order
// (un par peer × stratégie de facturation). Peuplé par DraftFirstBill.
SubscriptionIDs []string `json:"subscription_ids,omitempty" bson:"subscription_ids,omitempty"`
}
func (r *Order) StoreDraftDefault() {
+123 -7
View File
@@ -3,9 +3,19 @@ package peer
import (
"fmt"
"strings"
"time"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/biter777/countries"
)
type PeerPerm int
const (
READ PeerRelation = iota
WRITE
MONITOR
)
type PeerRelation int
@@ -16,12 +26,17 @@ const (
PARTNER
BLACKLIST
PENDING_PARTNER
MASTER
NANO
PENDING_NANO
PENDING_MASTER
)
var path = []string{"unknown", "self", "partner", "blacklist", "partner"}
var path = []string{"known", "self", "partner", "blacklist", "pending_partner", "master", "nano", "pending_nano", "pending_master"}
func GetRelationPath(str string) int {
for i, p := range path {
fmt.Println("GetRelationPath", i, p)
if str == p {
return i
}
@@ -41,12 +56,48 @@ func (m PeerRelation) EnumIndex() int {
return int(m)
}
// BehaviorWarning records a single misbehavior observed by a trusted service.
type BehaviorWarning struct {
At time.Time `json:"at" bson:"at"`
ReporterApp string `json:"reporter_app" bson:"reporter_app"`
Severity tools.BehaviorSeverity `json:"severity" bson:"severity"`
Reason string `json:"reason" bson:"reason"`
Evidence string `json:"evidence,omitempty" bson:"evidence,omitempty"`
}
// PeerLocation holds the voluntarily disclosed geographic position of a node.
// Granularity controls how precise the location is:
//
// 0 = not disclosed
// 1 = continent (±15°)
// 2 = country (±3°) — default
// 3 = region (±0.5°)
// 4 = city (±0.05°)
//
// The coordinates are always fuzzed by oc-discovery before publication,
// so a granularity-2 location identifies only the rough country area.
type PeerLocation struct {
Latitude float64 `json:"latitude" bson:"latitude"`
Longitude float64 `json:"longitude" bson:"longitude"`
Granularity int `json:"granularity" bson:"granularity"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
}
// Peer is a struct that represents a peer
type Peer struct {
utils.AbstractObject
IsNano bool `json:"is_nano" bson:"is_nano"`
Verify bool `json:"verify" bson:"verify"`
PeerID string `json:"peer_id" bson:"peer_id" validate:"required"`
PeerPerms []PeerPerm `json:"peer_perms" bson:"peer_perms"`
RelationLastChangeDate time.Time `json:"relation_last_change_date" bson:"relation_last_change_date"`
RelationLastChangeUser string `json:"relation_last_change_user" bson:"relation_last_change_user"`
Verify bool `json:"verify" bson:"verify"`
OrganizationID string `json:"organization_id" bson:"organization_id"`
PeerID string `json:"peer_id" bson:"peer_id" validate:"required"`
APIUrl string `json:"api_url" bson:"api_url" validate:"required"` // Url is the URL of the peer (base64url)
StreamAddress string `json:"stream_address" bson:"stream_address" validate:"required"` // Url is the URL of the peer (base64url)
@@ -56,12 +107,81 @@ type Peer struct {
Relation PeerRelation `json:"relation" bson:"relation" default:"0"`
ServicesState map[string]int `json:"services_state,omitempty" bson:"services_state,omitempty"`
FailedExecution []PeerExecution `json:"failed_execution" bson:"failed_execution"` // FailedExecution is the list of failed executions, to be retried
// Location is the voluntarily disclosed (and fuzzed) geographic position.
Location *PeerLocation `json:"location,omitempty" bson:"location,omitempty"`
// Trust scoring — maintained by oc-discovery from PEER_BEHAVIOR_EVENT reports.
TrustScore float64 `json:"trust_score" bson:"trust_score" default:"100"`
BlacklistReason string `json:"blacklist_reason,omitempty" bson:"blacklist_reason,omitempty"`
BehaviorWarnings []BehaviorWarning `json:"behavior_warnings,omitempty" bson:"behavior_warnings,omitempty"`
// MasterID is the libp2p PeerID of this peer's MASTER node.
// Set by a NANO in its own signed PeerRecord so intermediaries cannot forge it.
// When oc-discovery fails to reach a NANO, it routes the booking to MasterID instead.
MasterID string `json:"master_id,omitempty" bson:"master_id,omitempty"`
// Volatile connectivity state — never persisted to DB (bson:"-").
// Set in-memory by oc-peer when it receives a PEER_OBSERVE_RESPONSE_EVENT.
// Considered offline when LastHeartbeat is older than 60 s (30 s interval + 30 s grace).
Online bool `json:"online" bson:"-"`
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty" bson:"-"`
}
func (ri *Peer) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "peer":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.PEER)
}
}
return ext
}
func (ao *Peer) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}
// BlacklistThreshold is the trust score below which a peer is auto-blacklisted.
const BlacklistThreshold = 20.0
// ApplyBehaviorReport records a misbehavior, deducts the trust penalty, and
// returns true when the trust score has fallen below BlacklistThreshold so the
// caller can trigger the relation change.
func (p *Peer) ApplyBehaviorReport(r tools.PeerBehaviorReport) (shouldBlacklist bool) {
p.BehaviorWarnings = append(p.BehaviorWarnings, BehaviorWarning{
At: r.At,
ReporterApp: r.ReporterApp,
Severity: r.Severity,
Reason: r.Reason,
Evidence: r.Evidence,
})
if p.TrustScore == 0 {
p.TrustScore = 100 // initialise if never set
}
p.TrustScore -= r.Severity.Penalty()
if p.TrustScore < 0 {
p.TrustScore = 0
}
if p.TrustScore <= BlacklistThreshold {
p.BlacklistReason = r.Reason
return true
}
return false
}
// ResetTrust clears all behavior history and resets the trust score to 100.
// Must be called when a peer relation is manually set to NONE or PARTNER.
func (p *Peer) ResetTrust() {
p.TrustScore = 100
p.BlacklistReason = ""
p.BehaviorWarnings = nil
}
// AddExecution adds an execution to the list of failed executions
func (ao *Peer) AddExecution(exec PeerExecution) {
found := false
@@ -96,7 +216,3 @@ func (d *Peer) GetAccessor(request *tools.APIRequest) utils.Accessor {
data := NewAccessor(request) // Create a new instance of the accessor
return data
}
func (r *Peer) CanDelete() bool {
return false // only draft order can be deleted
}
+17
View File
@@ -42,6 +42,23 @@ func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool {
return !wfa.OverrideAuth
}
/*
TODO : organization_ID est un peer_ID duquel on se revendique faire parti.
Ca implique une clé d'organisation + une demande d'intégration.
Slave-Master IOT
*/
func (dca *peerMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
pp, _ := utils.GetMySelf(NewAccessor(&tools.APIRequest{Admin: true}))
if data != nil {
d := data.(*Peer)
if pp != nil && d.OrganizationID != "" && d.OrganizationID == pp.(*Peer).OrganizationID {
d.Relation = PARTNER // defaulting on partner if same organization.
}
}
return utils.GenericStoreOne(data, dca)
}
/*
* Nothing special here, just the basic CRUD operations
*/
+13 -13
View File
@@ -9,6 +9,7 @@ import (
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
@@ -25,7 +26,7 @@ type ComputeResource struct {
}
func (d *ComputeResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ComputeResource](tools.COMPUTE_RESOURCE, request, func() utils.DBObject { return &ComputeResource{} })
return NewAccessor[*ComputeResource](tools.COMPUTE_RESOURCE, request)
}
func (r *ComputeResource) GetType() string {
@@ -46,25 +47,24 @@ func (abs *ComputeResource) ConvertToPricedResource(t tools.DataType, selectedIn
}, nil
}
type ComputeNode struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Quantity int64 `json:"quantity" bson:"quantity" default:"1"`
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"` // RAM is the RAM
CPUs map[string]int64 `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]int64 `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
}
type ComputeResourceInstance struct {
ResourceInstance[*ComputeResourcePartnership]
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
Source string `json:"source,omitempty" bson:"source,omitempty"`
SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"`
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"`
Nodes []*live.ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
// AvailableStorages lists storage capabilities activatable on this compute unit (e.g. Minio, local volumes).
// These are shallow StorageResource entries — not independent catalog items — but carry full pricing structure.
AvailableStorages []*StorageResource `json:"available_storages,omitempty" bson:"available_storages,omitempty"`
}
// IsPeerless is always false for compute instances: a compute resource is
// infrastructure owned by a peer and can never be declared peerless.
func (ri *ComputeResourceInstance) IsPeerless() bool { return false }
func NewComputeResourceInstance(name string, peerID string) ResourceInstanceITF {
return &ComputeResourceInstance{
ResourceInstance: ResourceInstance[*ComputeResourcePartnership]{
+12 -21
View File
@@ -30,13 +30,22 @@ type DataResource struct {
}
func (d *DataResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*DataResource](tools.DATA_RESOURCE, request, func() utils.DBObject { return &DataResource{} }) // Create a new instance of the accessor
return NewAccessor[*DataResource](tools.DATA_RESOURCE, request) // Create a new instance of the accessor
}
func (r *DataResource) GetType() string {
return tools.DATA_RESOURCE.String()
}
func (ri *DataResource) StoreDraftDefault() {
ri.AbstractObject.StoreDraftDefault()
ri.Env = append(ri.Env, models.Param{
Attr: "source",
Value: "[resource]instance.source",
Readonly: true,
})
}
func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.DATA_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
@@ -53,7 +62,7 @@ func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInsta
type DataInstance struct {
ResourceInstance[*DataResourcePartnership]
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
}
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
@@ -67,24 +76,6 @@ func NewDataInstance(name string, peerID string) ResourceInstanceITF {
}
}
func (ri *DataInstance) StoreDraftDefault() {
found := false
for _, p := range ri.ResourceInstance.Env {
if p.Attr == "source" {
found = true
break
}
}
if !found {
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
Attr: "source",
Value: ri.Source,
Readonly: true,
})
}
ri.ResourceInstance.StoreDraftDefault()
}
type DataResourcePartnership struct {
ResourcePartnerShip[*DataResourcePricingProfile]
MaxDownloadableGbAllowed float64 `json:"allowed_gb,omitempty" bson:"allowed_gb,omitempty"`
@@ -95,7 +86,7 @@ type DataResourcePartnership struct {
type DataResourcePricingStrategy int
const (
PER_DOWNLOAD DataResourcePricingStrategy = iota + 6
PER_DOWNLOAD DataResourcePricingStrategy = iota + 7
PER_TB_DOWNLOADED
PER_GB_DOWNLOADED
PER_MB_DOWNLOADED
+299
View File
@@ -0,0 +1,299 @@
package resources
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"slices"
"strings"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* DynamicResource is a struct that represents a data resource
* it defines the resource data
*/
type DynamicResource struct {
AbstractResource
Type tools.DataType `bson:"type,omitempty" json:"type,omitempty"`
Filters map[string]interface{} `bson:"filters,omitempty" json:"filters,omitempty"`
SortRules map[string]string `bson:"rules,omitempty" json:"rules,omitempty"`
PeerIds map[int]string `bson:"peer_ids,omitempty" json:"peer_ids,omitempty"`
ResourceIds map[int]string `bson:"resource_ids,omitempty" json:"resource_ids,omitempty"`
SelectedIndex int `bson:"selected_index,omitempty" json:"selected_index,omitempty"`
SelectedPartnershipIndex *int `bson:"selected_partnership_index,omitempty" json:"selected_partnership_index,omitempty"`
SelectedBuyingStrategy int `bson:"selected_buying_strategy" json:"selected_buying_strategy,omitempty"`
SelectedPricingStrategy int `bson:"selected_pricing_strategy" json:"selected_pricing_strategy,omitempty"`
Instances []ResourceInstanceITF `bson:"instances,omitempty" json:"instances,omitempty"`
WatchedDynamicResource []string `bson:"watched_dynamic_resource,omitempty" json:"watched_dynamic_resource,omitempty"`
}
func (d *DynamicResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return nil
}
func (d *DynamicResource) SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF {
d.Instances = []ResourceInstanceITF{}
for k, v := range map[tools.DataType]ResourceInterface{
tools.COMPUTE_RESOURCE: &ComputeResource{},
tools.DATA_RESOURCE: &DataResource{},
tools.STORAGE_RESOURCE: &StorageResource{},
tools.PROCESSING_RESOURCE: &ProcessingResource{},
tools.WORKFLOW_RESOURCE: &WorkflowResource{}} {
if d.Type != k {
continue
}
access := NewAccessor[*DynamicResource](k, request)
a, _, _ := access.Search(dbs.FiltersFromFlatMap(d.Filters, v), "", false, 0, 100000)
fmt.Println(a, dbs.FiltersFromFlatMap(d.Filters, v), d.Filters, v)
d.PeerIds = map[int]string{}
d.ResourceIds = map[int]string{}
for _, res := range a {
for _, i := range res.(ResourceInterface).SetAllowedInstances(request, instance_id...) {
d.PeerIds[len(d.Instances)] = res.GetCreatorID()
d.ResourceIds[len(d.Instances)] = res.GetID()
d.Instances = append(d.Instances, i)
}
}
break
}
sorted := make([]ResourceInstanceITF, len(d.Instances))
copy(sorted, d.Instances)
slices.SortStableFunc(sorted, func(a, b ResourceInstanceITF) int {
d.SortRules["partnerships"] = "%v not contains 2"
return d.compareByRules(a, b, d.SortRules)
})
d.WatchedDynamicResource = []string{}
return d.Instances
}
func (d *DynamicResource) AddInstances(instance ResourceInstanceITF) {
d.Instances = append(d.Instances, instance)
}
func (d *DynamicResource) GetSelectedInstance(index *int) ResourceInstanceITF {
if len(d.Instances) == 0 {
return nil
}
for i, inst := range d.Instances {
if slices.Contains(d.WatchedDynamicResource, inst.GetID()) {
continue
}
d.WatchedDynamicResource = append(d.WatchedDynamicResource, inst.GetID())
d.SelectedIndex = i
for i := range inst.GetPartnerships() {
if inst.GetProfile(d.PeerIds[i], &i, &d.SelectedBuyingStrategy, &d.SelectedPricingStrategy) != nil {
d.SelectedPartnershipIndex = &i
break
}
}
if d.SelectedPartnershipIndex == nil {
continue
}
return inst
}
return nil
}
// compareByRules orders instances so those satisfying more sort rules come first.
// When both satisfy a rule, the one with the lower first-attribute value wins (ASC strict).
// Key format: "attrA" for single-%s rules, "attrA,attrB" for two-%s rules.
func (ri *DynamicResource) compareByRules(a, b ResourceInstanceITF, rules map[string]string) int {
ma := a.Serialize(a)
mb := b.Serialize(b)
for attrs, rule := range rules {
attrPaths := strings.Split(attrs, ",")
aOk, aFirst := ri.ruleMatchesAny(rule, attrPaths, ma)
bOk, bFirst := ri.ruleMatchesAny(rule, attrPaths, mb)
if aOk && !bOk {
return -1
}
if !aOk && bOk {
return 1
}
if aOk && bOk {
if aFirst < bFirst {
return -1
}
if aFirst > bFirst {
return 1
}
}
}
return 0
}
// ruleMatchesAny checks if any value (or combination for 2-%s rules) satisfies rule.
// Arrays at any path level are iterated. Returns (matched, firstMatchingValue).
func (ri *DynamicResource) ruleMatchesAny(rule string, attrPaths []string, m map[string]interface{}) (bool, string) {
placeholders := strings.Count(rule, "%s")
if placeholders == 0 {
return false, ""
}
valsA := ri.getVals(strings.Split(strings.TrimSpace(attrPaths[0]), "."), m)
if placeholders == 1 {
for _, v := range valsA {
if ri.byRules(rule, v) {
return true, fmt.Sprintf("%v", v)
}
}
return false, ""
}
if len(attrPaths) < 2 {
return false, ""
}
valsB := ri.getVals(strings.Split(strings.TrimSpace(attrPaths[1]), "."), m)
for _, a := range valsA {
for _, b := range valsB {
if ri.byRules(rule, a, b) {
return true, fmt.Sprintf("%v", a)
}
}
}
return false, ""
}
// getVals navigates attrs into m, collecting all leaf values.
// At each level it detects whether the value is a dict (map) or an array and acts accordingly:
// - array of maps → recurse into each element with the remaining path
// - array of scalars (leaf) → collect all as strings
// - map → recurse with the remaining path
func (ri *DynamicResource) getVals(attrs []string, m map[string]interface{}) []interface{} {
if len(attrs) == 0 {
return nil
}
attr := attrs[0]
if attr == "" || m[attr] == nil {
return nil
}
b, err := json.Marshal(m[attr])
if err != nil {
return nil
}
// Leaf level: detect array vs scalar.
if len(attrs) == 1 {
var arr []interface{}
if err := json.Unmarshal(b, &arr); err == nil {
results := []interface{}{}
for _, v := range arr {
results = append(results, fmt.Sprintf("%v", v))
}
return results
}
return []interface{}{m[attr]}
}
// Intermediate level: detect array of maps vs single map.
var arrMaps []map[string]interface{}
if err := json.Unmarshal(b, &arrMaps); err == nil {
results := []interface{}{}
for _, item := range arrMaps {
results = append(results, ri.getVals(attrs[1:], item)...)
}
return results
}
nm := map[string]interface{}{}
if err := json.Unmarshal(b, &nm); err != nil {
return nil
}
return ri.getVals(attrs[1:], nm)
}
func (ri *DynamicResource) byRules(rule string, vals ...interface{}) bool {
if len(vals) == 0 {
return false
}
formatted := fmt.Sprintf(rule, vals...)
// hm hm
switch {
case strings.Contains(rule, "not contains"):
a := strings.Split(formatted, " not contains ")
if reflect.TypeOf(vals[0]).Kind() == reflect.Map {
return vals[0].(map[string]interface{})[fmt.Sprintf("%v", a[1])] != nil
}
return strings.Contains(a[0], a[1])
case strings.Contains(rule, "contains"):
a := strings.Split(formatted, " contains ")
if reflect.TypeOf(vals[0]).Kind() == reflect.Map {
return vals[0].(map[string]interface{})[fmt.Sprintf("%v", a[1])] != nil
}
return strings.Contains(a[0], a[1])
case strings.Contains(rule, "<="):
a := strings.Split(formatted, " <= ")
return len(a) > 1 && a[0] <= a[1]
case strings.Contains(rule, ">="):
a := strings.Split(formatted, " >= ")
return len(a) > 1 && a[0] >= a[1]
case strings.Contains(rule, "<>"), strings.Contains(rule, "not like"):
if strings.Contains(rule, "<>") {
a := strings.Split(formatted, " <> ")
return len(a) > 1 && !strings.Contains(a[0], a[1]) && !strings.Contains(a[1], a[0])
}
a := strings.Split(formatted, " not like ")
return len(a) > 1 && !strings.Contains(a[0], a[1]) && !strings.Contains(a[1], a[0])
case strings.Contains(rule, "<"):
a := strings.Split(formatted, " < ")
return len(a) > 1 && a[0] < a[1]
case strings.Contains(rule, ">"):
a := strings.Split(formatted, " > ")
return len(a) > 1 && a[0] > a[1]
case strings.Contains(rule, "=="):
a := strings.Split(formatted, " == ")
return len(a) > 1 && a[0] == a[1]
case strings.Contains(rule, "!="):
a := strings.Split(formatted, " != ")
return len(a) > 1 && a[0] != a[1]
case strings.Contains(rule, "like"):
a := strings.Split(formatted, " like ")
return len(a) > 1 && (strings.Contains(a[0], a[1]) || strings.Contains(a[1], a[0]))
}
return false
}
func (r *DynamicResource) GetType() string {
return tools.DYNAMIC_RESOURCE.String()
}
func (abs *DynamicResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
var p pricing.PricedItemITF
var err error
for _, v := range []tools.DataType{
tools.COMPUTE_RESOURCE,
tools.DATA_RESOURCE,
tools.STORAGE_RESOURCE,
tools.PROCESSING_RESOURCE,
tools.WORKFLOW_RESOURCE,
} {
switch v {
case tools.COMPUTE_RESOURCE:
if p, err = ConvertToPricedResource[*ComputeResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
return p.(*PricedResource[*ProcessingResourcePricingProfile]), nil
}
case tools.DATA_RESOURCE:
if p, err = ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
return p.(*PricedResource[*DataResourcePricingProfile]), nil
}
case tools.STORAGE_RESOURCE:
if p, err = ConvertToPricedResource[*StorageResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
return p.(*PricedResource[*StorageResourcePricingProfile]), nil
}
case tools.PROCESSING_RESOURCE:
if p, err = ConvertToPricedResource[*ProcessingResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request); err == nil {
return p.(*PricedResource[*ProcessingResourcePricingProfile]), nil
}
}
}
return nil, errors.New("can't convert priced resource")
}
@@ -0,0 +1,223 @@
package resources
// exploitation_authorization.go — Autorisation d'Exploitation (AE)
//
// AEs are embedded inside AbstractResource (field ExploitationAuthorizations).
// They are NOT a separate MongoDB collection — each resource document carries
// its own AEs, just like it carries its Instances.
//
// # Visibility filtering
//
// When a resource is returned to a consumer peer the AE list is filtered:
// - AllowedPeerIDs empty → public AE, visible to all peers.
// - AllowedPeerIDs non-empty, contains requester → visible to that peer.
// - AllowedPeerIDs non-empty, doesn't contain requester → stripped from response.
//
// The resource owner always sees all of their own AEs unfiltered.
//
// # Enforcement
//
// oc-schedulerd's validateWorkflowIntegrity calls CheckWorkflowAE (defined in
// its own package to avoid circular imports) before launching any execution.
// Violations emit PEER_BEHAVIOR_EVENT(BehaviorFraud) against the consumer peer
// and cause the execution to be rejected.
import (
"encoding/json"
"fmt"
"slices"
"time"
"cloud.o-forge.io/core/oc-lib/tools"
)
// CouplingConstraint defines which resources must or must not co-exist in the
// same workflow when the protected resource is included.
type CouplingConstraint struct {
// RequiredResourceIDs — ALL of these resource UUIDs must appear in the
// workflow alongside the protected resource.
RequiredResourceIDs []string `json:"required_resource_ids,omitempty" bson:"required_resource_ids,omitempty"`
// ForbiddenResourceIDs — NONE of these resource UUIDs may appear in the
// workflow alongside the protected resource.
ForbiddenResourceIDs []string `json:"forbidden_resource_ids,omitempty" bson:"forbidden_resource_ids,omitempty"`
}
// ExploitationAuthorization (AE) is embedded in a resource and restricts how
// the resource may be used by other consumer peers.
//
// It is stored as part of the resource document (bson embedded), not as a
// separate collection. Create/update it by PATCHing the parent resource.
type ExploitationAuthorization struct {
// ID is a client-assigned UUID so individual AEs can be referenced.
ID string `json:"id" bson:"id"`
// Name is a human-readable label shown in the catalog detail view.
Name string `json:"name,omitempty" bson:"name,omitempty"`
// AllowedPeerIDs restricts which consumer peers may use the resource.
// An empty list means any peer is allowed.
AllowedPeerIDs []string `json:"allowed_peer_ids,omitempty" bson:"allowed_peer_ids,omitempty"`
// AllowedWorkflowIDs restricts which workflow IDs may include the resource.
// An empty list means any workflow is allowed.
AllowedWorkflowIDs []string `json:"allowed_workflow_ids,omitempty" bson:"allowed_workflow_ids,omitempty"`
// Coupling describes positive (required) and negative (forbidden) coupling.
// Nil means no coupling constraint.
Coupling *CouplingConstraint `json:"coupling,omitempty" bson:"coupling,omitempty"`
// ValidFrom / ValidUntil define the active window.
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
// IsRevoked allows instant revocation without deleting the AE from the resource.
IsRevoked bool `json:"is_revoked" bson:"is_revoked"`
}
// IsVisibleTo returns true when this AE should be included in the response to
// peerID. The resource owner (creatorID) always sees all AEs.
func (ae *ExploitationAuthorization) IsVisibleTo(peerID, creatorID string) bool {
if peerID == creatorID {
return true // owner sees everything
}
return len(ae.AllowedPeerIDs) == 0 || slices.Contains(ae.AllowedPeerIDs, peerID)
}
// CheckAE evaluates this AE against the execution context and returns any
// violations found. workflowResourceIDs is the set of all resource UUIDs in
// the workflow; resourceID is the UUID of the resource this AE belongs to.
func (ae *ExploitationAuthorization) CheckAE(
resourceID, workflowID, consumerPeerID string,
workflowResourceIDs map[string]struct{},
now time.Time,
) []AEViolation {
var vs []AEViolation
add := func(t AEViolationType, msg string) {
vs = append(vs, AEViolation{AEID: ae.ID, ResourceID: resourceID, Type: t, Message: msg})
}
if ae.IsRevoked {
add(AEViolationRevoked, fmt.Sprintf("AE %s for resource %s is revoked", ae.ID, resourceID))
return vs
}
if ae.ValidUntil != nil && now.After(*ae.ValidUntil) {
add(AEViolationExpired, fmt.Sprintf("AE %s for resource %s expired at %s",
ae.ID, resourceID, ae.ValidUntil.Format(time.RFC3339)))
return vs
}
if ae.ValidFrom != nil && now.Before(*ae.ValidFrom) {
add(AEViolationNotYetValid, fmt.Sprintf("AE %s for resource %s not valid until %s",
ae.ID, resourceID, ae.ValidFrom.Format(time.RFC3339)))
return vs
}
if consumerPeerID != "" && len(ae.AllowedPeerIDs) > 0 {
if !slices.Contains(ae.AllowedPeerIDs, consumerPeerID) {
add(AEViolationPeerNotAllowed, fmt.Sprintf(
"peer %s not allowed to use resource %s (AE %s)", consumerPeerID, resourceID, ae.ID))
}
}
if workflowID != "" && len(ae.AllowedWorkflowIDs) > 0 {
if !slices.Contains(ae.AllowedWorkflowIDs, workflowID) {
add(AEViolationWorkflowNotAllow, fmt.Sprintf(
"workflow %s not in allowed-workflow list for resource %s (AE %s)", workflowID, resourceID, ae.ID))
}
}
if ae.Coupling != nil {
for _, req := range ae.Coupling.RequiredResourceIDs {
if _, ok := workflowResourceIDs[req]; !ok {
add(AEViolationCouplingRequired, fmt.Sprintf(
"resource %s requires %s to be present (AE %s)", resourceID, req, ae.ID))
}
}
for _, forb := range ae.Coupling.ForbiddenResourceIDs {
if _, ok := workflowResourceIDs[forb]; ok {
add(AEViolationCouplingForbid, fmt.Sprintf(
"resource %s forbids co-use with %s (AE %s)", resourceID, forb, ae.ID))
}
}
}
return vs
}
// ── Violation types ───────────────────────────────────────────────────────────
type AEViolationType string
const (
AEViolationRevoked AEViolationType = "ae_revoked"
AEViolationExpired AEViolationType = "ae_expired"
AEViolationNotYetValid AEViolationType = "ae_not_yet_valid"
AEViolationPeerNotAllowed AEViolationType = "ae_peer_not_allowed"
AEViolationWorkflowNotAllow AEViolationType = "ae_workflow_not_allowed"
AEViolationCouplingRequired AEViolationType = "ae_coupling_required"
AEViolationCouplingForbid AEViolationType = "ae_coupling_forbidden"
)
// AEViolation describes a single constraint that was not satisfied.
type AEViolation struct {
AEID string
ResourceID string
Type AEViolationType
Message string
}
// ── NATS emit helper (uses tools only — no oclib circular import) ─────────────
// EmitAEBehaviorReport emits a PEER_BEHAVIOR_EVENT(BehaviorFraud) for each
// unique AE violation. Call this before rejecting the execution.
func EmitAEBehaviorReport(consumerPeerID string, violations []AEViolation) {
if consumerPeerID == "" || len(violations) == 0 {
return
}
seen := map[string]struct{}{}
for _, v := range violations {
key := v.AEID + ":" + v.ResourceID
if _, dup := seen[key]; dup {
continue
}
seen[key] = struct{}{}
report := tools.PeerBehaviorReport{
ReporterApp: "oc-scheduler",
TargetPeerID: consumerPeerID,
Severity: tools.BehaviorFraud,
Reason: fmt.Sprintf("AE violation (%s): %s", v.Type, v.Message),
Evidence: v.AEID,
At: time.Now().UTC(),
}
if b, err := json.Marshal(report); err == nil {
tools.NewNATSCaller().SetNATSPub(tools.PEER_BEHAVIOR_EVENT, tools.NATSResponse{
FromApp: "oc-scheduler",
Method: int(tools.PEER_BEHAVIOR_EVENT),
Payload: b,
})
}
}
}
// OriginType qualifies where a resource instance comes from.
type OriginType int
const (
// OriginPeer: instance offered by a known network peer (default).
OriginPeer OriginType = iota
// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
// No peer confirmation is needed; access is unrestricted.
OriginPublic
// OriginSelf: self-hosted instance with no third-party peer.
OriginSelf
)
// OriginMeta carries provenance information for a resource instance.
type OriginMeta struct {
Type OriginType `json:"origin_type" bson:"origin_type"`
Ref string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
Verified bool `json:"origin_verified" bson:"origin_verified"` // manually vetted by an OC admin
}
// IsPeerless MUST NOT be used for authorization decisions.
// Use ResourceInstance.IsPeerless() instead, which derives the property
// from structural invariants rather than this self-declared field.
//
// This method is kept only for display/logging purposes.
func (o OriginMeta) DeclaredPeerless() bool {
return o.Type != OriginPeer
}
+10 -2
View File
@@ -3,6 +3,7 @@ package resources
import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
@@ -19,22 +20,29 @@ type ResourceInterface interface {
ConvertToPricedResource(t tools.DataType, a *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, b *int, request *tools.APIRequest) (pricing.PricedItemITF, error)
GetType() string
ClearEnv() utils.DBObject
VerifyBuy()
SetAllowedInstances(request *tools.APIRequest, instance_id ...string) []ResourceInstanceITF
AddInstances(instance ResourceInstanceITF)
GetSelectedInstance(index *int) ResourceInstanceITF
StoreDraftDefault()
GetEnv() []models.Param
GetInputs() []models.Param
GetOutputs() []models.Param
}
type ResourceInstanceITF interface {
utils.DBObject
GetID() string
GetName() string
StoreDraftDefault()
ClearEnv()
GetOrigin() OriginMeta
IsPeerless() bool
FilterInstance(peerID string)
GetProfile(peerID string, partnershipIndex *int, buying *int, strategy *int) pricing.PricingProfileITF
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string)
ClearPeerGroups()
GetPartnerships() []ResourcePartnerITF
GetAverageDurationS() float64
UpdateAverageDuration(actualS float64)
}
+18
View File
@@ -12,13 +12,18 @@ type ResourceSet struct {
Computes []string `bson:"computes,omitempty" json:"computes,omitempty"`
Workflows []string `bson:"workflows,omitempty" json:"workflows,omitempty"`
NativeTool []string `bson:"native,omitempty" json:"native,omitempty"`
Services []string `bson:"services,omitempty" json:"services,omitempty"`
Dynamics []string `bson:"dynamics,omitempty" json:"dynamics,omitempty"`
// DynamicResources are stored inline — no DB collection, resolved at runtime via SetAllowedInstances.
DynamicResources []*DynamicResource `bson:"-" json:"dynamic_resources,omitempty"`
DataResources []*DataResource `bson:"-" json:"data_resources,omitempty"`
StorageResources []*StorageResource `bson:"-" json:"storage_resources,omitempty"`
ProcessingResources []*ProcessingResource `bson:"-" json:"processing_resources,omitempty"`
ComputeResources []*ComputeResource `bson:"-" json:"compute_resources,omitempty"`
WorkflowResources []*WorkflowResource `bson:"-" json:"workflow_resources,omitempty"`
NativeTools []*NativeTool `bson:"-" json:"native_tools,omitempty"`
ServiceResources []*ServiceResource `bson:"-" json:"service_resources,omitempty"`
}
func (r *ResourceSet) Clear() {
@@ -27,6 +32,8 @@ func (r *ResourceSet) Clear() {
r.ProcessingResources = nil
r.ComputeResources = nil
r.WorkflowResources = nil
r.ServiceResources = nil
r.DynamicResources = nil
}
func (r *ResourceSet) Fill(request *tools.APIRequest) {
@@ -37,6 +44,8 @@ func (r *ResourceSet) Fill(request *tools.APIRequest) {
(&StorageResource{}): r.Storages,
(&ProcessingResource{}): r.Processings,
(&WorkflowResource{}): r.Workflows,
(&ServiceResource{}): r.Services,
(&DynamicResource{}): r.Dynamics,
} {
for _, id := range v {
d, _, e := k.GetAccessor(request).LoadOne(id)
@@ -52,10 +61,17 @@ func (r *ResourceSet) Fill(request *tools.APIRequest) {
r.ProcessingResources = append(r.ProcessingResources, d.(*ProcessingResource))
case *WorkflowResource:
r.WorkflowResources = append(r.WorkflowResources, d.(*WorkflowResource))
case *ServiceResource:
r.ServiceResources = append(r.ServiceResources, d.(*ServiceResource))
case *DynamicResource:
r.DynamicResources = append(r.DynamicResources, d.(*DynamicResource))
}
}
}
}
for _, d := range r.DynamicResources {
d.SetAllowedInstances(request)
}
}
type ItemResource struct {
@@ -65,4 +81,6 @@ type ItemResource struct {
Compute *ComputeResource `bson:"compute,omitempty" json:"compute,omitempty"`
Workflow *WorkflowResource `bson:"workflow,omitempty" json:"workflow,omitempty"`
NativeTool *NativeTool `bson:"native_tools,omitempty" json:"native_tools,omitempty"`
Service *ServiceResource `bson:"service,omitempty" json:"service,omitempty"`
Dynamic *DynamicResource `bson:"dynamic,omitempty" json:"dynamic,omitempty"`
}
+9 -3
View File
@@ -23,7 +23,7 @@ func (d *NativeTool) SetName(name string) {
}
func (d *NativeTool) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*NativeTool](tools.NATIVE_TOOL, request, func() utils.DBObject { return &NativeTool{} })
return NewAccessor[*NativeTool](tools.NATIVE_TOOL, request)
}
func (r *NativeTool) AddInstances(instance ResourceInstanceITF) {
@@ -38,7 +38,13 @@ func (d *NativeTool) ClearEnv() utils.DBObject {
}
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */
// WorkflowResource has no instances, but still carries AEs that must be
// filtered before the resource is returned to a non-owner, non-admin peer.
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
if request != nil {
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
}
@@ -61,7 +67,7 @@ func InitNative() {
for _, kind := range []native_tools.NativeToolsEnum{native_tools.WORKFLOW_EVENT} {
newNative := &NativeTool{}
access := newNative.GetAccessor(&tools.APIRequest{Admin: true})
l, _, err := access.Search(nil, kind.String(), false)
l, _, err := access.Search(nil, kind.String(), false, 0, 10)
if err != nil || len(l) == 0 {
newNative.Name = kind.String()
newNative.Kind = int(kind)
+4
View File
@@ -47,6 +47,10 @@ func (abs *PricedResource[T]) GetID() string {
return abs.ResourceID
}
func (abs *PricedResource[T]) GetName() string {
return abs.Name
}
func (abs *PricedResource[T]) GetInstanceID() string {
return abs.InstanceID
}
+13 -17
View File
@@ -28,26 +28,26 @@ type ProcessingUsage struct {
*/
type ProcessingResource struct {
AbstractInstanciatedResource[*ProcessingInstance]
IsEvent bool `json:"is_event,omitempty" bson:"is_event,omitempty"`
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
IsService bool `json:"is_service,omitempty" bson:"is_service,omitempty"` // IsService is a flag that indicates if the processing is a service
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"` // Usage is the usage of the processing
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"`
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
License string `json:"license,omitempty" bson:"license,omitempty"`
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
// IsService marks a long-running processing that acts as a persistent service.
// Such processings do not require a Compute booking (they manage their own lifecycle).
IsService bool `json:"is_service" bson:"is_service" default:"false"`
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
// License is now in AbstractResource — kept here as alias for backward compat with existing DB docs.
// New code should use AbstractResource.License.
}
func (r *ProcessingResource) GetType() string {
return tools.PROCESSING_RESOURCE.String()
}
type ProcessingResourceAccess struct {
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
}
type ProcessingInstance struct {
ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]
Access *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
}
func NewProcessingInstance(name string, peerID string) ResourceInstanceITF {
@@ -67,7 +67,6 @@ type ProcessingResourcePartnership struct {
type PricedProcessingResource struct {
PricedResource[*ProcessingResourcePricingProfile]
IsService bool
}
func (r *PricedProcessingResource) ensurePricing() {
@@ -100,10 +99,7 @@ func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
a.BookingConfiguration = &BookingConfiguration{}
}
if a.BookingConfiguration.ExplicitBookingDurationS == 0 {
if a.IsService || a.BookingConfiguration.UsageStart == nil {
if a.IsService {
return -1
}
if a.BookingConfiguration.UsageStart == nil {
return (5 * time.Minute).Seconds()
}
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
@@ -112,7 +108,7 @@ func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
}
func (d *ProcessingResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request, func() utils.DBObject { return &ProcessingResource{} }) // Create a new instance of the accessor
return NewAccessor[*ProcessingResource](tools.PROCESSING_RESOURCE, request) // Create a new instance of the accessor
}
func (abs *ProcessingResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
@@ -9,6 +9,7 @@ import (
type PurchaseResource struct {
utils.AbstractObject
FromNano string `json:"from_nano,omitempty" bson:"priced_item,omitempty"`
DestPeerID string `json:"dest_peer_id" bson:"dest_peer_id"`
PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty" validate:"required"`
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
@@ -24,6 +25,34 @@ type PurchaseResource struct {
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
}
func (ri *PurchaseResource) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "dest_peer", "scheduler_peer":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.PEER)
case "execution":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW_EXECUTION)
case "resource":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW_RESOURCE)
ext[t] = append(ext[t], tools.DATA_RESOURCE)
ext[t] = append(ext[t], tools.COMPUTE_RESOURCE)
ext[t] = append(ext[t], tools.STORAGE_RESOURCE)
ext[t] = append(ext[t], tools.PROCESSING_RESOURCE)
ext[t] = append(ext[t], tools.SERVICE_RESOURCE)
}
}
return ext
}
func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
@@ -30,7 +30,7 @@ func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(id, a)
utils.GenericDelete(d, a)
return nil, 404, nil
}
return d, 200, nil
@@ -40,9 +40,13 @@ func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int,
func (a *PurchaseResourceMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(d.GetID(), a)
utils.GenericDelete(d, a)
return nil
}
return d
}
}
func (dca *PurchaseResourceMongoAccessor) ShouldVerifyAuth() bool {
return false // TEMP : by pass
}
+190 -45
View File
@@ -3,37 +3,126 @@ package resources
import (
"encoding/json"
"errors"
"fmt"
"slices"
"time"
"cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/biter777/countries"
"github.com/google/uuid"
)
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
return dbs.FiltersFromFlatMap(flatMap, target)
}
// AbstractResource is the struct containing all of the attributes commons to all ressources
type AbstractResource struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
PurchaseID string `json:"purchase_id,omitempty"` // is_buy precise if a resource is buy or not
Type string `json:"type,omitempty" bson:"type,omitempty"` // Type is the type of the resource
Logo string `json:"logo,omitempty" bson:"logo,omitempty"` // Logo is the logo of the resource
Description string `json:"description,omitempty" bson:"description,omitempty"` // Description is the description of the resource
ShortDescription string `json:"short_description,omitempty" bson:"short_description,omitempty"` // ShortDescription is the short description of the resource
Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource
UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"`
Type string `json:"type,omitempty" bson:"type,omitempty"` // Type is the type of the resource
Logo string `json:"logo,omitempty" bson:"logo,omitempty"` // Logo is the logo of the resource
Description string `json:"description,omitempty" bson:"description,omitempty"` // Description is the description of the resource
ShortDescription string `json:"short_description,omitempty" bson:"short_description,omitempty"` // ShortDescription is the short description of the resource
Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource
UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
AllowedBookingModes map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"`
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
// License is the usage licence of the resource (SPDX identifier or free-text).
// Displayed prominently in the catalog detail view.
License string `json:"license,omitempty" bson:"license,omitempty"`
// ExploitationAuthorizations (AEs) are coupling and peer-usage constraints
// issued by the resource owner. Stored embedded in the resource document,
// NOT in a separate collection.
// Visibility-filtered per requesting peer before any response is sent.
ExploitationAuthorizations []ExploitationAuthorization `json:"exploitation_authorizations,omitempty" bson:"exploitation_authorizations,omitempty"`
}
func (ri *AbstractResource) Extend(typ ...string) map[string][]tools.DataType {
dt := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "purchase":
if _, ok := dt[t]; !ok {
dt[t] = []tools.DataType{}
}
dt[t] = append(dt[t], tools.PURCHASE_RESOURCE)
}
}
return dt
}
func (abs *AbstractResource) VerifyBuy() {
p := &purchase_resource.PurchaseResource{}
access := p.GetAccessor(&tools.APIRequest{Admin: true})
purchase, _, _ := access.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: abs.GetID()}},
},
}, "", false, 0, 1)
if len(purchase) > 0 {
abs.PurchaseID = purchase[0].GetID()
}
}
func (abs *AbstractResource) GetEnv() []models.Param {
return abs.Env
}
func (abs *AbstractResource) GetInputs() []models.Param {
return abs.Inputs
}
func (abs *AbstractResource) GetOutputs() []models.Param {
return abs.Outputs
}
func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
return nil
}
// GetExploitationAuthorizations returns all AEs attached to this resource.
// Used by oc-schedulerd's CheckWorkflowAE via structural interface assertion.
func (r *AbstractResource) GetExploitationAuthorizations() []ExploitationAuthorization {
return r.ExploitationAuthorizations
}
// FilterExploitationAuthorizations removes AEs that are not visible to peerID.
// Must be called before serializing the resource for a consumer peer.
// The resource owner (CreatorID) always sees all AEs unfiltered.
func (r *AbstractResource) FilterExploitationAuthorizations(peerID string, isAdmin bool) {
if isAdmin {
return // admin or owner: no filtering
}
filtered := r.ExploitationAuthorizations[:0]
for _, ae := range r.ExploitationAuthorizations {
if ae.IsVisibleTo(peerID, r.CreatorID) {
filtered = append(filtered, ae)
}
}
r.ExploitationAuthorizations = filtered
}
func (ri *AbstractResource) ClearEnv() utils.DBObject {
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
return ri
}
func (r *AbstractResource) GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation {
if len(r.AllowedBookingModes) == 0 {
return map[booking.BookingMode]*pricing.PricingVariation{
@@ -52,15 +141,12 @@ func (r *AbstractResource) GetType() string {
}
func (r *AbstractResource) StoreDraftDefault() {
r.IsDraft = true
//r.IsDraft = true pour le moment on passe outre.
}
func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
return r.IsDraft, set
}
func (r *AbstractResource) CanDelete() bool {
return r.IsDraft // only draft bookings can be deleted
fmt.Println("IsDrafted", r.IsDraft, set.IsDrafted())
return r.IsDraft || set.IsDrafted(), set
}
type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
@@ -136,13 +222,6 @@ func ConvertToPricedResource[T pricing.PricingProfileITF](t tools.DataType,
}, nil
}
func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
for _, instance := range abs.Instances {
instance.ClearEnv()
}
return abs
}
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) ResourceInstanceITF {
if selected != nil && len(r.Instances) > *selected {
return r.Instances[*selected]
@@ -156,12 +235,15 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
// Filter AEs: only return AEs visible to the requesting peer.
if request != nil {
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
inst := []ResourceInstanceITF{}
for _, i := range abs.Instances {
inst = append(inst, i)
}
return inst
}
@@ -175,13 +257,20 @@ func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.AP
if len(instanceID) > 0 && !slices.Contains(instanceID, instance.GetID()) {
continue
}
// Structurally peerless instances (no creator, no partnerships, non-empty Ref)
// are freely accessible by any requester.
if instance.IsPeerless() {
instances = append(instances, instance)
continue
}
_, peerGroups := instance.GetPeerGroups()
for _, peers := range peerGroups {
if request == nil {
continue
}
if grps, ok := peers[request.PeerID]; ok || config.GetConfig().Whitelist {
if (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
_, allOK := peers["*"]
if grps, ok := peers[request.PeerID]; ok || allOK || config.GetConfig().Whitelist {
if allOK || (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance)
// TODO filter Partners + Profiles...
@@ -199,19 +288,14 @@ func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.AP
return instances
}
type GeoPoint struct {
Latitude float64 `json:"latitude,omitempty" bson:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty" bson:"longitude,omitempty"`
}
type ResourceInstance[T ResourcePartnerITF] struct {
utils.AbstractObject
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
LastUpdate time.Time `json:"last_update,omitempty" bson:"last_update,omitempty"`
Origin OriginMeta `json:"origin,omitempty" bson:"origin,omitempty"`
Location live.GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
@@ -231,10 +315,23 @@ func NewInstance[T ResourcePartnerITF](name string) *ResourceInstance[T] {
}
}
func (ri *ResourceInstance[T]) GetOrigin() OriginMeta {
return ri.Origin
}
// IsPeerless returns true when the instance has no owning peer and a non-empty
// registry reference. This is derived from structural invariants — NOT from the
// self-declared Origin.Type field — to prevent auth bypass via metadata manipulation:
//
// CreatorID == "" ∧ len(Partnerships) == 0 ∧ Origin.Ref != ""
func (ri *ResourceInstance[T]) IsPeerless() bool {
return ri.CreatorID == "" && len(ri.Partnerships) == 0 && ri.Origin.Ref != ""
}
func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
partnerships := []T{}
for _, p := range ri.Partnerships {
if p.GetPeerGroups()[peerID] != nil {
if p.GetPeerGroups()["*"] != nil || p.GetPeerGroups()[peerID] != nil {
p.FilterPartnership(peerID)
partnerships = append(partnerships, p)
}
@@ -242,13 +339,10 @@ func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
ri.Partnerships = partnerships
}
func (ri *ResourceInstance[T]) ClearEnv() {
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
}
func (ri *ResourceInstance[T]) GetProfile(peerID string, partnershipIndex *int, buyingIndex *int, strategyIndex *int) pricing.PricingProfileITF {
if ri.IsPeerless() {
return pricing.GetDefaultPricingProfile()
}
if partnershipIndex != nil && len(ri.Partnerships) > *partnershipIndex {
prts := ri.Partnerships[*partnershipIndex]
return prts.GetProfile(buyingIndex, strategyIndex)
@@ -262,6 +356,9 @@ func (ri *ResourceInstance[T]) GetProfile(peerID string, partnershipIndex *int,
}
func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
if ri.IsPeerless() {
return []pricing.PricingProfileITF{pricing.GetDefaultPricingProfile()}
}
pricings := []pricing.PricingProfileITF{}
for _, p := range ri.Partnerships {
pricings = append(pricings, p.GetPricingsProfiles(peerID, groups)...)
@@ -277,6 +374,10 @@ func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []strin
}
func (ri *ResourceInstance[T]) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) {
// Structurally peerless: universally accessible — wildcard on all peers.
if ri.IsPeerless() {
return []ResourcePartnerITF{}, []map[string][]string{{"*": {"*"}}}
}
groups := []map[string][]string{}
partners := []ResourcePartnerITF{}
for _, p := range ri.Partnerships {
@@ -317,6 +418,14 @@ func (ri *ResourceInstance[T]) UpdateAverageDuration(actualS float64) {
ri.AverageDurationSamples++
}
func (ri *ResourceInstance[T]) GetPartnerships() []ResourcePartnerITF {
rt := []ResourcePartnerITF{}
for _, p := range ri.Partnerships {
rt = append(rt, p)
}
return rt
}
type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"`
PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
@@ -325,11 +434,17 @@ type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
}
func (ri *ResourcePartnerShip[T]) FilterPartnership(peerID string) {
if ri.PeerGroups[peerID] == nil {
ri.PeerGroups = map[string][]string{}
} else {
if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil {
ri.PeerGroups = map[string][]string{
peerID: ri.PeerGroups[peerID],
"*": {"*"},
}
} else {
ri.PeerGroups = map[string][]string{}
if ri.PeerGroups["*"] != nil {
ri.PeerGroups["*"] = ri.PeerGroups["*"]
}
if ri.PeerGroups[peerID] != nil {
ri.PeerGroups[peerID] = ri.PeerGroups[peerID]
}
}
}
@@ -355,7 +470,15 @@ Une bill (facture) représente alors... l'emission d'une facture à un instant T
*/
func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
profiles := []pricing.PricingProfileITF{}
if ri.PeerGroups[peerID] == nil {
if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil {
return profiles
}
if ri.PeerGroups["*"] != nil {
for _, ri := range ri.PricingProfiles {
for _, i := range ri {
profiles = append(profiles, i)
}
}
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
@@ -387,6 +510,7 @@ func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string {
return rp.PeerGroups
}
return map[string][]string{
"*": {"*"},
pp.GetID(): {"*"},
}
}
@@ -432,6 +556,27 @@ func ToResource(
return nil, err
}
return &data, nil
case tools.SERVICE_RESOURCE.EnumIndex():
var data ServiceResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
}
return nil, errors.New("can't found any data resources matching")
}
type ResourceAccess struct {
Source *models.PathSource `json:"source,omitempty" bson:"source,omitempty"`
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
}
// HasSource returns true when the access is source-based (no embedded container).
func (a *ResourceAccess) HasSource() bool {
return a != nil && a.Container == nil && a.Source != nil
}
// HasContainer returns true when an explicit container image is provided.
func (a *ResourceAccess) HasContainer() bool {
return a != nil && a.Container != nil
}
+146 -22
View File
@@ -2,25 +2,69 @@ package resources
import (
"errors"
"fmt"
"slices"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type ResourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject
}
func sourceFromAccess(access *ResourceAccess) string {
if access == nil {
return ""
}
if access.Container != nil && access.Container.Source != "" {
return access.Container.Source
}
if access.Source != nil && access.Source.Source != "" {
return access.Source.Source
}
return ""
}
func upsertSourceParam(outputs []models.Param, source string) []models.Param {
for i, p := range outputs {
if p.Attr == "source" {
outputs[i].Value = source
return outputs
}
}
return append(outputs, models.Param{Attr: "source", Value: source, Readonly: true})
}
func applyAccessSourceOutput(data utils.DBObject) {
switch r := data.(type) {
case *ProcessingResource:
for _, inst := range r.Instances {
if src := sourceFromAccess(inst.Access); src != "" {
r.Outputs = upsertSourceParam(r.Outputs, src)
return
}
}
case *DataResource:
for _, inst := range r.Instances {
if src := sourceFromAccess(inst.Access); src != "" {
r.Outputs = upsertSourceParam(r.Outputs, src)
return
}
}
}
}
// New creates a new instance of the computeMongoAccessor
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *ResourceMongoAccessor[T] {
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest) *ResourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{
tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE,
tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE,
tools.DATA_RESOURCE, tools.NATIVE_TOOL,
tools.PROCESSING_RESOURCE, tools.SERVICE_RESOURCE,
tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE, tools.NATIVE_TOOL,
}, t) {
return nil
}
@@ -37,6 +81,8 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
return &StorageResource{}
case tools.PROCESSING_RESOURCE:
return &ProcessingResource{}
case tools.SERVICE_RESOURCE:
return &ServiceResource{}
case tools.WORKFLOW_RESOURCE:
return &WorkflowResource{}
case tools.DATA_RESOURCE:
@@ -47,7 +93,6 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
return nil
},
},
generateData: g,
}
}
@@ -55,9 +100,32 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
* Nothing special here, just the basic CRUD operations
*/
func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
data, code, err := dca.AbstractAccessor.LoadOne(id)
if err == nil {
data.(T).VerifyBuy()
data.(T).SetAllowedInstances(dca.Request)
return data, code, err
}
return data, code, err
}
func (dca *ResourceMongoAccessor[T]) UpdateOne(set map[string]interface{}, 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")
delete(set, "architecture")
delete(set, "infrastructure")
} else if dca.GetType() == tools.SERVICE_RESOURCE {
delete(set, "infrastructure")
} else if dca.GetType() == tools.STORAGE_RESOURCE {
delete(set, "storage_type")
}
if dca.GetType() == tools.PROCESSING_RESOURCE || dca.GetType() == tools.DATA_RESOURCE {
if merged, _, _, err := utils.ModelGenericUpdateOne(set, id, dca); err == nil {
applyAccessSourceOutput(merged)
if serialized := merged.Serialize(merged); serialized != nil {
set["outputs"] = serialized["outputs"]
}
}
}
return utils.GenericUpdateOne(set, id, dca)
}
@@ -67,39 +135,96 @@ func (dca *ResourceMongoAccessor[T]) ShouldVerifyAuth() bool {
}
func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
var i string
idsToUpdate := []string{}
var a utils.Accessor
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
r := data.(*ComputeResource)
if len(r.Instances) == 0 {
return nil, 404, errors.New("can't create a non existing computing units resource with no instances")
}
a = live.NewAccessor[*live.LiveDatacenter](tools.LIVE_DATACENTER, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID())
fmt.Println(res, r.Instances[0].GetID())
if res == nil {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
}
if !res.(*live.LiveDatacenter).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live computing units target is not compatible")
}
i = res.GetID()
idsToUpdate = res.(*live.LiveDatacenter).ResourcesID
} else if dca.GetType() == tools.SERVICE_RESOURCE {
r := data.(*ServiceResource)
if len(r.Instances) == 0 {
return nil, 404, errors.New("can't create a non existing service resource with no instances")
}
a = live.NewAccessor[*live.LiveService](tools.LIVE_SERVICE, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID())
if res == nil {
return nil, 404, errors.New("can't create a non existing service resource not reported onto service catalog")
}
if !res.(*live.LiveService).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live service target is not compatible")
}
i = res.GetID()
idsToUpdate = res.(*live.LiveService).ResourcesID
} else if dca.GetType() == tools.STORAGE_RESOURCE {
r := data.(*StorageResource)
if len(r.Instances) == 0 {
return nil, 404, errors.New("can't create a non existing storage resource with no instances")
}
a = live.NewAccessor[*live.LiveStorage](tools.LIVE_STORAGE, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID())
if res == nil {
return nil, 404, errors.New("can't create a non existing storage resource not reported onto storage catalog")
}
if !res.(*live.LiveStorage).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live storage target is not compatible")
}
i = res.GetID()
idsToUpdate = res.(*live.LiveStorage).ResourcesID
}
return utils.GenericStoreOne(data, dca)
applyAccessSourceOutput(data)
res, code, err := utils.GenericStoreOne(data, dca)
if res != nil && i != "" {
idsToUpdate = append(idsToUpdate, res.GetID())
a.UpdateOne(map[string]interface{}{
"resources_id": idsToUpdate,
}, i)
}
return res, code, err
}
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 (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa)
func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit)
}
func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
if filters == nil && search == "*" {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
fmt.Println("Search", d)
d.(T).VerifyBuy()
d.(T).SetAllowedInstances(wfa.Request)
fmt.Println("Search2", d)
return d
}, isDraft, wfa)
}, isDraft, wfa, offset, limit)
}
return utils.GenericSearch[T](filters, search, wfa.GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject {
d.(T).VerifyBuy()
d.(T).SetAllowedInstances(wfa.Request)
return d
}, isDraft, wfa)
}, isDraft, wfa, offset, limit)
}
func (a *ResourceMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
d.(T).VerifyBuy()
d.(T).SetAllowedInstances(a.Request)
return d
}
@@ -108,12 +233,11 @@ func (a *ResourceMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) ut
func (abs *ResourceMongoAccessor[T]) GetObjectFilters(search string) *dbs.Filters {
return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided
"abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractintanciatedresource.abstractresource.type": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractintanciatedresource.abstractresource.short_description": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractintanciatedresource.abstractresource.description": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractintanciatedresource.abstractresource.owners.name": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractintanciatedresource.abstractresource.abstractobject.creator_id": {{Operator: dbs.EQUAL.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.type": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.short_description": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.description": {{Operator: dbs.LIKE.String(), Value: search}},
"abstractinstanciatedresource.abstractresource.owners.name": {{Operator: dbs.LIKE.String(), Value: search}},
},
}
}
+198
View File
@@ -0,0 +1,198 @@
package resources
import (
"errors"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
type ServiceMode int
const (
DEPLOYMENT ServiceMode = iota // deploy the service, pay for uptime — duration unbounded
HOSTED // use an existing service, pay per call — duration per request
)
func (m ServiceMode) String() string {
return [...]string{"DEPLOYMENT", "HOSTED"}[m]
}
type ServiceUsage struct {
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"`
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"`
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"`
StorageGb float64 `bson:"storage,omitempty" json:"storage,omitempty"`
Hypothesis string `bson:"hypothesis,omitempty" json:"hypothesis,omitempty"`
ScalingModel string `bson:"scaling_model,omitempty" json:"scaling_model,omitempty"`
}
// ServiceResourceAccess describes how to reach the service once running.
// Populated for HOSTED instances (endpoint already known) and as a template for DEPLOYMENT.
type ServiceResourceAccess struct {
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"`
Protocol live.ServiceProtocol `json:"protocol" bson:"protocol" default:"0"`
EndpointPattern string `json:"endpoint_pattern,omitempty" bson:"endpoint_pattern,omitempty"`
HealthCheckPath string `json:"health_check_path,omitempty" bson:"health_check_path,omitempty"`
}
type ServiceResource struct {
AbstractInstanciatedResource[*ServiceInstance]
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
Usage *ServiceUsage `bson:"usage,omitempty" json:"usage,omitempty"`
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
License string `json:"license,omitempty" bson:"license,omitempty"`
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
}
func (r *ServiceResource) GetType() string {
return tools.SERVICE_RESOURCE.String()
}
func (d *ServiceResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ServiceResource](tools.SERVICE_RESOURCE, request)
}
func (abs *ServiceResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.SERVICE_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Service")
}
p, err := ConvertToPricedResource[*ServiceResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*ServiceResourcePricingProfile])
return &PricedServiceResource{PricedResource: *priced}, nil
}
type ServiceInstance struct {
ResourceInstance[*ServiceResourcePartnership]
Mode ServiceMode `json:"mode" bson:"mode" default:"0"`
Access *ServiceResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
MaxConcurrent int `json:"max_concurrent,omitempty" bson:"max_concurrent,omitempty"`
}
func (ri *ServiceInstance) IsPeerless() bool { return false }
func NewServiceInstance(name string, peerID string) ResourceInstanceITF {
return &ServiceInstance{
ResourceInstance: ResourceInstance[*ServiceResourcePartnership]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
type ServiceResourcePartnership struct {
ResourcePartnerShip[*ServiceResourcePricingProfile]
}
// ServiceResourcePricingProfile handles both service modes:
// - DEPLOYMENT: uptime billing via ExploitPricingProfile (pay while service is up)
// - HOSTED: per-call billing via AccessPricingProfile (pay per request)
type ServiceResourcePricingProfile struct {
Mode ServiceMode `json:"mode" bson:"mode"`
UptimePricing *pricing.ExploitPricingProfile[pricing.TimePricingStrategy] `json:"uptime_pricing,omitempty" bson:"uptime_pricing,omitempty"`
AccessPricing *pricing.AccessPricingProfile[pricing.TimePricingStrategy] `json:"access_pricing,omitempty" bson:"access_pricing,omitempty"`
}
func (p *ServiceResourcePricingProfile) ensure() {
if p.UptimePricing == nil {
p.UptimePricing = &pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{}
}
if p.AccessPricing == nil {
p.AccessPricing = &pricing.AccessPricingProfile[pricing.TimePricingStrategy]{}
}
}
func (p *ServiceResourcePricingProfile) IsPurchasable() bool {
p.ensure()
if p.Mode == DEPLOYMENT {
return p.UptimePricing.IsPurchasable()
}
return p.AccessPricing.IsPurchasable()
}
func (p *ServiceResourcePricingProfile) IsBooked() bool {
p.ensure()
if p.Mode == DEPLOYMENT {
return p.UptimePricing.IsBooked()
}
return p.AccessPricing.IsBooked()
}
func (p *ServiceResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
p.ensure()
if p.Mode == DEPLOYMENT {
return p.UptimePricing.GetPurchase()
}
return p.AccessPricing.GetPurchase()
}
func (p *ServiceResourcePricingProfile) GetOverrideStrategyValue() int {
return -1
}
func (p *ServiceResourcePricingProfile) GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variations []*pricing.PricingVariation, params ...string) (float64, error) {
p.ensure()
if p.Mode == DEPLOYMENT {
return p.UptimePricing.GetPriceHT(quantity, val, start, end, variations, params...)
}
return p.AccessPricing.GetPriceHT(quantity, val, start, end, variations, params...)
}
type PricedServiceResource struct {
PricedResource[*ServiceResourcePricingProfile]
}
func (r *PricedServiceResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ServiceResourcePricingProfile{}
}
}
func (r *PricedServiceResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedServiceResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedServiceResource) GetType() tools.DataType {
return tools.SERVICE_RESOURCE
}
func (r *PricedServiceResource) GetPriceHT() (float64, error) {
r.ensurePricing()
return r.PricedResource.GetPriceHT()
}
// GetExplicitDurationInS returns -1 for DEPLOYMENT (unbounded uptime).
// For HOSTED, returns the actual call window duration.
func (a *PricedServiceResource) GetExplicitDurationInS() float64 {
a.ensurePricing()
if a.SelectedPricing.Mode == DEPLOYMENT {
return -1
}
if a.BookingConfiguration == nil {
a.BookingConfiguration = &BookingConfiguration{}
}
if a.BookingConfiguration.ExplicitBookingDurationS != 0 {
return a.BookingConfiguration.ExplicitBookingDurationS
}
if a.BookingConfiguration.UsageStart == nil || a.BookingConfiguration.UsageEnd == nil {
return -1 // no deadline specified: open-ended
}
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
}
+26 -26
View File
@@ -12,6 +12,17 @@ import (
"github.com/google/uuid"
)
// EmbeddedStorageSelection records which storage capability was activated on a
// compute unit graph node, and which pricing options were selected for it.
// Key in WorkflowExecution.SelectedEmbeddedStorages is the compute graph node ID.
// A nil/absent entry means no storage was activated on that compute unit.
type EmbeddedStorageSelection struct {
StorageIndex int `json:"storage_index" bson:"storage_index"` // index in ComputeResourceInstance.AvailableStorages
PartnershipIndex int `json:"partnership_index" bson:"partnership_index"` // index in the storage's partnerships
BuyingIndex int `json:"buying_index" bson:"buying_index"`
StrategyIndex int `json:"strategy_index" bson:"strategy_index"`
}
/*
* StorageResource is a struct that represents a storage resource
* it defines the resource storage
@@ -23,7 +34,7 @@ type StorageResource struct {
}
func (d *StorageResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*StorageResource](tools.STORAGE_RESOURCE, request, func() utils.DBObject { return &StorageResource{} }) // Create a new instance of the accessor
return NewAccessor[*StorageResource](tools.STORAGE_RESOURCE, request) // Create a new instance of the accessor
}
func (r *StorageResource) GetType() string {
@@ -44,6 +55,15 @@ func (abs *StorageResource) ConvertToPricedResource(t tools.DataType, selectedIn
}, nil
}
func (ri *StorageResource) StoreDraftDefault() {
ri.AbstractObject.StoreDraftDefault()
ri.Env = append(ri.Env, models.Param{
Attr: "source",
Value: "[resource]instance.source",
Readonly: true,
})
}
type StorageResourceInstance struct {
ResourceInstance[*StorageResourcePartnership]
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
@@ -57,6 +77,10 @@ type StorageResourceInstance struct {
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
}
// IsPeerless is always false for storage instances: a storage resource is
// infrastructure owned by a peer and can never be declared peerless.
func (ri *StorageResourceInstance) IsPeerless() bool { return false }
func NewStorageResourceInstance(name string, peerID string) ResourceInstanceITF {
return &StorageResourceInstance{
ResourceInstance: ResourceInstance[*StorageResourcePartnership]{
@@ -68,30 +92,6 @@ func NewStorageResourceInstance(name string, peerID string) ResourceInstanceITF
}
}
func (ri *StorageResourceInstance) ClearEnv() {
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
}
func (ri *StorageResourceInstance) StoreDraftDefault() {
found := false
for _, p := range ri.ResourceInstance.Env {
if p.Attr == "source" {
found = true
break
}
}
if !found {
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
Attr: "source",
Value: ri.Source,
Readonly: true,
})
}
ri.ResourceInstance.StoreDraftDefault()
}
type StorageResourcePartnership struct {
ResourcePartnerShip[*StorageResourcePricingProfile]
MaxSizeGBAllowed float64 `json:"allowed_gb,omitempty" bson:"allowed_gb,omitempty"`
@@ -117,7 +117,7 @@ func (t PrivilegeStoragePricingStrategy) String() string {
type StorageResourcePricingStrategy int
const (
PER_DATA_STORED StorageResourcePricingStrategy = iota + 6
PER_DATA_STORED StorageResourcePricingStrategy = iota + 7
PER_TB_STORED
PER_GB_STORED
PER_MB_STORED
-18
View File
@@ -4,7 +4,6 @@ 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"
@@ -34,23 +33,6 @@ func TestDataResource_ConvertToPricedResource(t *testing.T) {
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
@@ -30,13 +30,6 @@ func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) {
input PricedProcessingResource
expected float64
}{
{
name: "Service without explicit duration",
input: PricedProcessingResource{
IsService: true,
},
expected: -1,
},
{
name: "Nil start time, non-service",
input: PricedProcessingResource{
+1 -3
View File
@@ -114,8 +114,6 @@ func (f *FakeResource) ConvertToPricedResource(t tools.DataType, a *int, b *int,
func (f *FakeResource) VerifyAuth(string, *tools.APIRequest) bool { return true }
func TestNewAccessor_ReturnsValid(t *testing.T) {
acc := resources.NewAccessor[*FakeResource](tools.COMPUTE_RESOURCE, &tools.APIRequest{}, func() utils.DBObject {
return &FakeResource{}
})
acc := resources.NewAccessor[*FakeResource](tools.COMPUTE_RESOURCE, &tools.APIRequest{})
assert.NotNil(t, acc)
}
-31
View File
@@ -3,7 +3,6 @@ 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"
@@ -37,36 +36,6 @@ func TestStorageResource_ConvertToPricedResource_InvalidType(t *testing.T) {
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
+8 -2
View File
@@ -16,7 +16,7 @@ type WorkflowResource struct {
}
func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request)
}
func (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) {
@@ -31,7 +31,13 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
}
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */
// WorkflowResource has no instances, but still carries AEs that must be
// filtered before the resource is returned to a non-owner, non-admin peer.
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
if request != nil {
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
}
+29 -4
View File
@@ -31,6 +31,7 @@ const (
*/
type AbstractObject struct {
UUID string `json:"id,omitempty" bson:"id,omitempty" validate:"required"`
NotInCatalog bool `json:"not_in_catalog" bson:"not_in_catalog" default:"false"`
Name string `json:"name,omitempty" bson:"name,omitempty" validate:"required"`
IsDraft bool `json:"is_draft" bson:"is_draft" default:"false"`
CreatorID string `json:"creator_id,omitempty" bson:"creator_id,omitempty"`
@@ -43,9 +44,29 @@ type AbstractObject struct {
Signature []byte `bson:"signature,omitempty" json:"signature,omitempty"`
}
func (ri *AbstractObject) Extend(typ ...string) map[string][]tools.DataType {
dt := map[string][]tools.DataType{}
for _, t := range typ {
switch t {
case "creator", "user_creator", "user_updater":
if _, ok := dt[t]; !ok {
dt[t] = []tools.DataType{}
}
dt[t] = append(dt[t], tools.PEER)
}
}
return dt
}
func (ri *AbstractObject) GetAccessor(request *tools.APIRequest) Accessor {
return nil
}
func (r *AbstractObject) SetNotInCatalog(ok bool) {
r.NotInCatalog = ok
}
func (r *AbstractObject) IsNotInCatalog() bool {
return r.NotInCatalog
}
func (r *AbstractObject) Unsign() {
r.Signature = nil
@@ -77,6 +98,10 @@ func (r *AbstractObject) DeepCopy() *AbstractObject {
return &obj
}
func (r *AbstractObject) SetDraft(draft bool) {
r.IsDraft = draft
}
func (r *AbstractObject) SetName(name string) {
r.Name = name
}
@@ -259,12 +284,12 @@ func (a *AbstractAccessor[T]) LoadOne(id string) (DBObject, int, error) {
}, a)
}
func (a *AbstractAccessor[T]) LoadAll(isDraft bool) ([]ShallowDBObject, int, error) {
return GenericLoadAll[T](a.GetExec(isDraft), isDraft, a)
func (a *AbstractAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error) {
return GenericLoadAll[T](a.GetExec(isDraft), isDraft, a, offset, limit)
}
func (a *AbstractAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]ShallowDBObject, int, error) {
return GenericSearch[T](filters, search, a.New().GetObjectFilters(search), a.GetExec(isDraft), isDraft, a)
func (a *AbstractAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error) {
return GenericSearch[T](filters, search, a.New().GetObjectFilters(search), a.GetExec(isDraft), isDraft, a, offset, limit)
}
func (a *AbstractAccessor[T]) GetExec(isDraft bool) func(DBObject) ShallowDBObject {
+60
View File
@@ -0,0 +1,60 @@
package utils
import (
"sync"
"cloud.o-forge.io/core/oc-lib/tools"
)
// ChangeEvent is fired whenever a DB object is created, updated or deleted
// within this process. Deleted=true means the object was removed; Object is
// the last known snapshot before deletion.
type ChangeEvent struct {
DataType tools.DataType
ID string
Object ShallowDBObject // nil only when the load after the write failed
Deleted bool
}
var (
changeBusMu sync.RWMutex
changeBus = map[tools.DataType][]chan ChangeEvent{}
)
// SubscribeChanges returns a channel that receives ChangeEvents for dt
// whenever an object of that type is written or deleted in this process.
// Call the returned cancel function to unsubscribe; after that the channel
// will no longer receive events (it is not closed — use a context to stop
// reading).
func SubscribeChanges(dt tools.DataType) (<-chan ChangeEvent, func()) {
ch := make(chan ChangeEvent, 32)
changeBusMu.Lock()
changeBus[dt] = append(changeBus[dt], ch)
changeBusMu.Unlock()
return ch, func() {
changeBusMu.Lock()
subs := changeBus[dt]
for i, c := range subs {
if c == ch {
changeBus[dt] = append(subs[:i], subs[i+1:]...)
break
}
}
changeBusMu.Unlock()
}
}
// NotifyChange broadcasts a ChangeEvent to all current subscribers for dt.
// Non-blocking: events are dropped for subscribers whose buffer is full.
func NotifyChange(dt tools.DataType, id string, obj ShallowDBObject, deleted bool) {
changeBusMu.RLock()
subs := changeBus[dt]
changeBusMu.RUnlock()
evt := ChangeEvent{DataType: dt, ID: id, Object: obj, Deleted: deleted}
for _, ch := range subs {
select {
case ch <- evt:
default:
}
}
}
+56 -18
View File
@@ -17,6 +17,9 @@ type Owner struct {
}
func VerifyAccess(a Accessor, id string) error {
if a == nil {
return errors.New("no accessor to verify access")
}
data, _, err := a.LoadOne(id)
if err != nil {
return err
@@ -51,7 +54,7 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
if a.ShouldVerifyAuth() && !data.VerifyAuth("store", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access : " + a.GetType().String())
}
if cursor, _, _ := a.Search(&f, "", data.IsDrafted()); len(cursor) > 0 {
if cursor, _, _ := a.Search(&f, "", data.IsDrafted(), 0, 10); len(cursor) > 0 {
return nil, 409, errors.New(a.GetType().String() + " with name " + data.GetName() + " already exists")
}
err := validate.Struct(data)
@@ -63,7 +66,11 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
a.GetLogger().Error().Msg("Could not store " + data.GetName() + " to db. Error: " + err.Error())
return nil, code, err
}
return a.LoadOne(id)
result, rcode, rerr := a.LoadOne(id)
if rerr == nil && result != nil {
go NotifyChange(a.GetType(), result.GetID(), result, false)
}
return result, rcode, rerr
}
// GenericLoadOne loads one object from the database (generic)
@@ -75,20 +82,22 @@ func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
if res == nil {
return res, code, errors.New("not found")
}
return GenericDelete(res, a)
}
func GenericDelete(res DBObject, a Accessor) (DBObject, int, error) {
if !res.CanDelete() {
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
}
if err != nil {
return nil, code, err
}
if a.ShouldVerifyAuth() && !res.VerifyAuth("delete", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access " + a.GetType().String())
}
_, code, err = mongo.MONGOService.DeleteOne(id, a.GetType().String())
_, code, err := mongo.MONGOService.DeleteOne(res.GetID(), a.GetType().String())
if err != nil {
a.GetLogger().Error().Msg("Could not delete " + id + " to db. Error: " + err.Error())
a.GetLogger().Error().Msg("Could not delete " + res.GetID() + " to db. Error: " + err.Error())
return nil, code, err
}
go NotifyChange(a.GetType(), res.GetID(), res, true)
return res, 200, nil
}
@@ -100,6 +109,9 @@ func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor)
obj := a.NewObj()
b, _ := json.Marshal(r)
json.Unmarshal(b, obj)
if change["is_draft"] == true {
obj.SetDraft(change["is_draft"] == true)
}
if !a.GetRequest().Admin {
var ok bool
ok, r = r.CanUpdate(obj)
@@ -116,18 +128,27 @@ func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor)
r.Sign()
}
loaded := r.Serialize(r) // get the loaded object
loaded := r.Serialize(r) // get the loaded object
for k, v := range change { // apply the changes, with a flatten method
loaded[k] = v
}
return r, loaded, 200, nil
newObj := a.NewObj()
b, err = json.Marshal(loaded)
if err != nil {
return nil, loaded, 400, nil
}
err = json.Unmarshal(b, newObj)
if err != nil {
return nil, loaded, 400, nil
}
return newObj, loaded, 200, nil
}
// GenericLoadOne loads one object from the database (generic)
// json expected in entry is a flatted object no need to respect the inheritance hierarchy
func GenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, int, error) {
obj, loaded, c, err := ModelGenericUpdateOne(change, id, a)
if err != nil {
return nil, c, err
}
@@ -136,7 +157,11 @@ func GenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBO
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
return nil, code, err
}
return a.LoadOne(id)
result, rcode, rerr := a.LoadOne(id)
if rerr == nil && result != nil {
go NotifyChange(a.GetType(), result.GetID(), result, false)
}
return result, rcode, rerr
}
func GenericLoadOne[T DBObject](id string, data T, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
@@ -147,7 +172,6 @@ func GenericLoadOne[T DBObject](id string, data T, f func(DBObject) (DBObject, i
if err = res_mongo.Decode(data); err != nil {
return nil, 400, err
}
if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
}
@@ -172,17 +196,27 @@ func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft
return objs, 200, nil
}
func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor) ([]ShallowDBObject, int, error) {
res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType().String())
func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) {
offset := int64(0)
limit := int64(0)
if len(opts) > 1 {
offset = opts[0]
}
res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType().String(), offset, limit)
return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
}
func GenericSearch[T DBObject](filters *dbs.Filters, search string, defaultFilters *dbs.Filters,
f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor) ([]ShallowDBObject, int, error) {
f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) {
if filters == nil && search != "" {
filters = defaultFilters
}
res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType().String())
offset := int64(0)
limit := int64(0)
if len(opts) > 1 {
offset = opts[0]
}
res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType().String(), offset, limit)
return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
}
@@ -194,7 +228,11 @@ func GenericRawUpdateOne(set DBObject, id string, a Accessor) (DBObject, int, er
a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
return nil, code, err
}
return a.LoadOne(id)
result, rcode, rerr := a.LoadOne(id)
if rerr == nil && result != nil {
go NotifyChange(a.GetType(), result.GetID(), result, false)
}
return result, rcode, rerr
}
func GetMySelf(wfa Accessor) (ShallowDBObject, error) {
@@ -202,7 +240,7 @@ func GetMySelf(wfa Accessor) (ShallowDBObject, error) {
And: map[string][]dbs.Filter{
"relation": {{Operator: dbs.EQUAL.String(), Value: 1}},
},
}, "", false)
}, "", false, 0, 1)
if len(datas) > 0 && datas[0] != nil {
return datas[0], nil
}
+7 -2
View File
@@ -8,6 +8,7 @@ import (
// ShallowDBObject is an interface that defines the basic methods shallowed version of a DBObject
type ShallowDBObject interface {
DBObject
GenerateID()
GetID() string
GetName() string
@@ -18,10 +19,14 @@ type ShallowDBObject interface {
// DBObject is an interface that defines the basic methods for a DBObject
type DBObject interface {
GenerateID()
Extend(typ ...string) map[string][]tools.DataType
SetNotInCatalog(bool)
IsNotInCatalog() bool
SetID(id string)
GetID() string
GetName() string
SetName(name string)
SetDraft(draft bool)
IsDrafted() bool
CanDelete() bool
StoreDraftDefault()
@@ -53,8 +58,8 @@ type Accessor interface {
DeleteOne(id string) (DBObject, int, error)
CopyOne(data DBObject) (DBObject, int, error)
StoreOne(data DBObject) (DBObject, int, error)
LoadAll(isDraft bool) ([]ShallowDBObject, int, error)
LoadAll(isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error)
UpdateOne(set map[string]interface{}, id string) (DBObject, int, error)
Search(filters *dbs.Filters, search string, isDraft bool) ([]ShallowDBObject, int, error)
Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error)
GetExec(isDraft bool) func(DBObject) ShallowDBObject
}
+54
View File
@@ -45,6 +45,10 @@ func (wf *Graph) IsProcessing(item GraphItem) bool {
return item.Processing != nil
}
func (wf *Graph) IsService(item GraphItem) bool {
return item.Service != nil
}
func (wf *Graph) IsNativeTool(item GraphItem) bool {
return item.NativeTool != nil
}
@@ -65,6 +69,10 @@ func (wf *Graph) IsWorkflow(item GraphItem) bool {
return item.Workflow != nil
}
func (wf *Graph) IsDynamic(item GraphItem) bool {
return item.Dynamic != nil
}
func (g *Graph) GetAverageTimeRelatedToProcessingActivity(processings []*resources.ProcessingResource, resource resources.ResourceInterface,
f func(GraphItem) resources.ResourceInterface, instance int, partnership int, buying int, strategy int, bookingMode int, request *tools.APIRequest) (float64, float64, error) {
oneIsInfinite := false
@@ -137,6 +145,50 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
return max, nil
}
// DataStorageLink represents a resolved Data→Storage pair found in the graph.
type DataStorageLink struct {
DataItemID string
StorageItemID string
}
// GetDataStorageLinks returns all links that connect a Data item to a Storage item.
// These links are mandatory when the Data instance has a Source configured:
// the workflow builder uses them to know where to download the data before
// any processing step that consumes that storage.
func (g *Graph) GetDataStorageLinks() []DataStorageLink {
var result []DataStorageLink
for _, link := range g.Links {
srcItem, srcOk := g.Items[link.Source.ID]
dstItem, dstOk := g.Items[link.Destination.ID]
if !srcOk || !dstOk {
continue
}
if g.IsData(srcItem) && g.IsStorage(dstItem) {
result = append(result, DataStorageLink{
DataItemID: link.Source.ID,
StorageItemID: link.Destination.ID,
})
} else if g.IsStorage(srcItem) && g.IsData(dstItem) {
result = append(result, DataStorageLink{
DataItemID: link.Destination.ID,
StorageItemID: link.Source.ID,
})
}
}
return result
}
// GetLinkedStorageForData returns the storage item IDs linked to a given Data item.
func (g *Graph) GetLinkedStorageForData(dataItemID string) []string {
var storageIDs []string
for _, dsl := range g.GetDataStorageLinks() {
if dsl.DataItemID == dataItemID {
storageIDs = append(storageIDs, dsl.StorageItemID)
}
}
return storageIDs
}
func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterface) {
if item, ok := g.Items[id]; ok {
if item.NativeTool != nil {
@@ -151,6 +203,8 @@ func (g *Graph) GetResource(id string) (tools.DataType, resources.ResourceInterf
return tools.PROCESSING_RESOURCE, item.Processing
} else if item.Storage != nil {
return tools.STORAGE_RESOURCE, item.Storage
} else if item.Service != nil {
return tools.SERVICE_RESOURCE, item.Service
}
}
return tools.INVALID, nil
+23 -15
View File
@@ -15,24 +15,32 @@ type GraphItem struct {
}
func (g *GraphItem) GetResource() (tools.DataType, resources.ResourceInterface) {
if g.Data != nil {
return tools.DATA_RESOURCE, g.Data
} else if g.Compute != nil {
return tools.COMPUTE_RESOURCE, g.Compute
} else if g.Workflow != nil {
return tools.WORKFLOW_RESOURCE, g.Workflow
} else if g.Processing != nil {
return tools.PROCESSING_RESOURCE, g.Processing
} else if g.Storage != nil {
return tools.STORAGE_RESOURCE, g.Storage
if g.ItemResource.Data != nil {
return tools.DATA_RESOURCE, g.ItemResource.Data
} else if g.ItemResource.Compute != nil {
return tools.COMPUTE_RESOURCE, g.ItemResource.Compute
} else if g.ItemResource.Workflow != nil {
return tools.WORKFLOW_RESOURCE, g.ItemResource.Workflow
} else if g.ItemResource.Processing != nil {
return tools.PROCESSING_RESOURCE, g.ItemResource.Processing
} else if g.ItemResource.Storage != nil {
return tools.STORAGE_RESOURCE, g.ItemResource.Storage
} else if g.ItemResource.NativeTool != nil {
return tools.NATIVE_TOOL, g.ItemResource.NativeTool
} else if g.ItemResource.Service != nil {
return tools.SERVICE_RESOURCE, g.ItemResource.Service
} else if g.ItemResource.Dynamic != nil {
return tools.DYNAMIC_RESOURCE, g.ItemResource.Dynamic
}
return tools.INVALID, nil
}
func (g *GraphItem) Clear() {
g.Data = nil
g.Compute = nil
g.Workflow = nil
g.Processing = nil
g.Storage = nil
g.ItemResource.Data = nil
g.ItemResource.Compute = nil
g.ItemResource.Workflow = nil
g.ItemResource.Processing = nil
g.ItemResource.Storage = nil
g.ItemResource.Service = nil
g.ItemResource.Dynamic = nil
}
+4 -4
View File
@@ -23,11 +23,11 @@ func (l *GraphLink) IsComputeLink(g Graph) (bool, string) {
if g.Items == nil {
return false, ""
}
if d, ok := g.Items[l.Source.ID]; ok && d.Compute != nil {
return true, d.Compute.UUID
if d, ok := g.Items[l.Source.ID]; ok && d.ItemResource.Compute != nil {
return true, d.ItemResource.Compute.UUID
}
if d, ok := g.Items[l.Destination.ID]; ok && d.Compute != nil {
return true, d.Compute.UUID
if d, ok := g.Items[l.Destination.ID]; ok && d.ItemResource.Compute != nil {
return true, d.ItemResource.Compute.UUID
}
return false, ""
}
+18 -18
View File
@@ -104,17 +104,17 @@ func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
func plantUMLPrefix(item graph.GraphItem) string {
switch {
case item.NativeTool != nil:
case item.ItemResource.NativeTool != nil:
return "e"
case item.Data != nil:
case item.ItemResource.Data != nil:
return "d"
case item.Processing != nil:
case item.ItemResource.Processing != nil:
return "p"
case item.Storage != nil:
case item.ItemResource.Storage != nil:
return "s"
case item.Compute != nil:
case item.ItemResource.Compute != nil:
return "c"
case item.Workflow != nil:
case item.ItemResource.Workflow != nil:
return "wf"
}
return "u"
@@ -123,24 +123,24 @@ func plantUMLPrefix(item graph.GraphItem) string {
// plantUMLItemLine builds the PlantUML declaration line for one graph item.
func plantUMLItemLine(varName string, item graph.GraphItem) string {
switch {
case item.NativeTool != nil:
case item.ItemResource.NativeTool != nil:
// WorkflowEvent has no instance and no configurable attributes.
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.NativeTool.GetName())
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.ItemResource.NativeTool.GetName())
case item.Data != nil:
return plantUMLResourceLine("Data", varName, item.Data)
case item.ItemResource.Data != nil:
return plantUMLResourceLine("Data", varName, item.ItemResource.Data)
case item.Processing != nil:
return plantUMLResourceLine("Processing", varName, item.Processing)
case item.ItemResource.Processing != nil:
return plantUMLResourceLine("Processing", varName, item.ItemResource.Processing)
case item.Storage != nil:
return plantUMLResourceLine("Storage", varName, item.Storage)
case item.ItemResource.Storage != nil:
return plantUMLResourceLine("Storage", varName, item.ItemResource.Storage)
case item.Compute != nil:
return plantUMLResourceLine("ComputeUnit", varName, item.Compute)
case item.ItemResource.Compute != nil:
return plantUMLResourceLine("ComputeUnit", varName, item.ItemResource.Compute)
case item.Workflow != nil:
return plantUMLResourceLine("Workflow", varName, item.Workflow)
case item.ItemResource.Workflow != nil:
return plantUMLResourceLine("Workflow", varName, item.ItemResource.Workflow)
}
return ""
}
+755 -57
View File
@@ -6,10 +6,12 @@ import (
"errors"
"fmt"
"mime/multipart"
"regexp"
"strconv"
"strings"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/booking/planner"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area"
@@ -43,12 +45,16 @@ func (c ConfigItem) Get(key string) *int {
type Workflow struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
resources.ResourceSet
Graph *graph.Graph `bson:"graph,omitempty" json:"graph,omitempty"` // Graph UI & logic representation of the workflow
ScheduleActive bool `json:"schedule_active" bson:"schedule_active"` // ScheduleActive is a flag that indicates if the schedule is active, if not the workflow is not scheduled and no execution or booking will be set
Graph *graph.Graph `bson:"graph,omitempty" json:"graph,omitempty"` // Graph UI & logic representation of the workflow
// Schedule *WorkflowSchedule `bson:"schedule,omitempty" json:"schedule,omitempty"` // Schedule is the schedule of the workflow
Shared []string `json:"shared,omitempty" bson:"shared,omitempty"` // Shared is the ID of the shared workflow // AbstractWorkflow contains the basic fields of a workflow
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Shared []string `json:"shared,omitempty" bson:"shared,omitempty"` // Shared is the ID of the shared workflow // AbstractWorkflow contains the basic fields of a workflow
Env map[string][]models.Param `json:"env" bson:"env"`
Inputs map[string][]models.Param `json:"inputs" bson:"inputs"`
Outputs map[string][]models.Param `json:"outputs" bson:"outputs"`
Args map[string][]string `json:"args" bson:"args"`
Exposes map[string][]models.Expose `bson:"exposes" json:"exposes"` // Expose is the execution
SelectedEmbeddedStorages map[string]*resources.EmbeddedStorageSelection `json:"selected_embedded_storages,omitempty"`
}
func (d *Workflow) GetAccessor(request *tools.APIRequest) utils.Accessor {
@@ -88,13 +94,24 @@ func (d *Workflow) GetResources(dt tools.DataType) []resources.ResourceInterface
itf = append(itf, d)
}
return itf
case tools.SERVICE_RESOURCE:
for _, d := range d.ServiceResources {
itf = append(itf, d)
}
return itf
case tools.DYNAMIC_RESOURCE:
for _, d := range d.DynamicResources {
itf = append(itf, d)
}
return itf
}
return itf
}
func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.APIRequest) (*Workflow, error) {
func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.APIRequest) (*Workflow, []string, error) {
if plantUML == nil {
return d, errors.New("no file available to export")
return d, nil, errors.New("no file available to export")
}
defer plantUML.Close()
@@ -104,12 +121,16 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A
d.Processings = []string{}
d.Computes = []string{}
d.Workflows = []string{}
d.Dynamics = []string{}
d.Services = []string{}
d.DataResources = []*resources.DataResource{}
d.StorageResources = []*resources.StorageResource{}
d.ProcessingResources = []*resources.ProcessingResource{}
d.ComputeResources = []*resources.ComputeResource{}
d.WorkflowResources = []*resources.WorkflowResource{}
d.DynamicResources = []*resources.DynamicResource{}
d.ServiceResources = []*resources.ServiceResource{}
d.Graph = graph.NewGraph()
resourceCatalog := map[string]func() resources.ResourceInterface{
@@ -141,6 +162,16 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A
},
}
},
"Service": func() resources.ResourceInterface {
return &resources.ServiceResource{
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ServiceInstance]{
Instances: []*resources.ServiceInstance{},
},
}
},
"Dynamic": func() resources.ResourceInterface {
return &resources.DynamicResource{}
},
// WorkflowEvent creates a NativeTool of Kind=WORKFLOW_EVENT directly,
// without DB lookup. It has no user-defined instance.
"WorkflowEvent": func() resources.ResourceInterface {
@@ -159,9 +190,11 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return d, err
return d, nil, err
}
var warnings []string
for i, line := range lines {
trimmed := strings.TrimSpace(line)
@@ -185,52 +218,53 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A
}
}
for n, new := range resourceCatalog {
if strings.Contains(line, n+"(") && !strings.Contains(line, "!procedure") && !strings.Contains(line, "!define") { // exclude macro declarations
newRes := new()
// Handle links outside the catalog loop: each link line must be processed
// exactly once (the catalog loop would otherwise call extractLink once per
// catalog entry, producing N×7 duplicate links in the graph).
if strings.Contains(line, "-->") {
if err := d.extractLink(parseLine, graphVarName, "-->", false); err != nil {
fmt.Println(err)
}
continue
}
if strings.Contains(line, "<--") {
if err := d.extractLink(parseLine, graphVarName, "<--", true); err != nil {
fmt.Println(err)
}
continue
}
if strings.Contains(line, "--") {
if err := d.extractLink(parseLine, graphVarName, "--", false); err != nil {
fmt.Println(err)
}
continue
}
for n, newFn := range resourceCatalog {
if strings.Contains(line, n+"(") && !strings.Contains(line, "!procedure") && !strings.Contains(line, "!define") {
newRes := newFn()
newRes.SetID(uuid.New().String())
varName, graphItem, err := d.extractResourcePlantUML(parseLine, newRes, n, request.PeerID)
varName, graphItem, warns, err := d.extractResourcePlantUML(parseLine, newRes, n, request.PeerID, request)
if err != nil {
return d, err
return d, warnings, err
}
warnings = append(warnings, warns...)
if graphItem != nil {
graphVarName[varName] = *graphItem
}
continue
} else if strings.Contains(line, "-->") {
err := d.extractLink(parseLine, graphVarName, "-->", false)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, "<--") {
err := d.extractLink(parseLine, graphVarName, "<--", true)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, "--") {
err := d.extractLink(parseLine, graphVarName, "--", false)
if err != nil {
fmt.Println(err)
continue
}
} else if strings.Contains(line, "-") {
err := d.extractLink(parseLine, graphVarName, "-", false)
if err != nil {
fmt.Println(err)
continue
}
break
}
}
}
d.generateResource(d.GetResources(tools.DATA_RESOURCE), request)
d.generateResource(d.GetResources(tools.PROCESSING_RESOURCE), request)
d.generateResource(d.GetResources(tools.SERVICE_RESOURCE), request)
d.generateResource(d.GetResources(tools.STORAGE_RESOURCE), request)
d.generateResource(d.GetResources(tools.COMPUTE_RESOURCE), request)
d.generateResource(d.GetResources(tools.WORKFLOW_RESOURCE), request)
d.generateResource(d.GetResources(tools.DYNAMIC_RESOURCE), request)
d.Graph.Items = graphVarName
return d, nil
return d, warnings, nil
}
func (d *Workflow) generateResource(datas []resources.ResourceInterface, request *tools.APIRequest) error {
@@ -322,14 +356,37 @@ func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphI
if len(splitted) < 2 {
return errors.New("links elements not found")
}
// Source: trim surrounding whitespace.
srcVar := strings.TrimSpace(splitted[0])
// Destination: trim whitespace then stop at the first space or apostrophe
// (the rest may be a trailing comment produced by ToPlantUML look-ahead).
dstTokens := strings.FieldsFunc(strings.TrimSpace(splitted[1]), func(r rune) bool {
return r == ' ' || r == '\t' || r == '\''
})
if len(dstTokens) == 0 {
return errors.New("link destination var name not found")
}
dstVar := dstTokens[0]
srcItem, srcOk := graphVarName[srcVar]
dstItem, dstOk := graphVarName[dstVar]
if !srcOk || srcItem.ID == "" {
return fmt.Errorf("link source %q not declared", srcVar)
}
if !dstOk || dstItem.ID == "" {
return fmt.Errorf("link destination %q not declared", dstVar)
}
link := &graph.GraphLink{
Source: graph.Position{
ID: graphVarName[splitted[0]].ID,
ID: srcItem.ID,
X: 0,
Y: 0,
},
Destination: graph.Position{
ID: graphVarName[splitted[1]].ID,
ID: dstItem.ID,
X: 0,
Y: 0,
},
@@ -348,35 +405,46 @@ func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphI
return nil
}
func (d *Workflow) extractResourcePlantUML(line string, resource resources.ResourceInterface, dataName string, peerID string) (string, *graph.GraphItem, error) {
func (d *Workflow) extractResourcePlantUML(line string, resource resources.ResourceInterface, dataName string, peerID string, request *tools.APIRequest) (string, *graph.GraphItem, []string, error) {
splittedFunc := strings.Split(line, "(")
if len(splittedFunc) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no func")
return "", nil, nil, errors.New("Can't deserialize Object, there's no func")
}
splittedParams := strings.Split(splittedFunc[1], ",")
if len(splittedParams) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no params")
return "", nil, nil, errors.New("Can't deserialize Object, there's no params")
}
varName := splittedParams[0]
splitted := strings.Split(splittedParams[1], "\"")
if len(splitted) <= 1 {
return "", nil, errors.New("Can't deserialize Object, there's no name")
return "", nil, nil, errors.New("Can't deserialize Object, there's no name")
}
resource.SetName(strings.ReplaceAll(splitted[1], "\\n", " "))
name := strings.ReplaceAll(splitted[1], "\\n", " ")
// Resources with instances get a default one seeded from the parent resource,
// then overridden by any explicit comment attributes.
// Event (NativeTool) has no instance: getNewInstance returns nil and is skipped.
instance := d.getNewInstance(dataName, splitted[1], peerID)
// Extract comment text (if present) for metadata parsing.
comment := ""
if parts := strings.Split(line, "'"); len(parts) > 1 {
comment = strings.ReplaceAll(parts[1], "'", "")
}
var warns []string
// Try to resolve an existing catalog resource (by id, then by name).
if existing, warn := d.resolveExistingResource(resource, dataName, name, comment, request); existing != nil {
warns = append(warns, warn)
item := d.addExistingGraphItem(dataName, existing)
return varName, item, warns, nil
}
// No existing resource — create new.
resource.SetName(name)
instance := d.getNewInstance(dataName, name, peerID)
if instance != nil {
if b, err := json.Marshal(resource); err == nil {
json.Unmarshal(b, instance)
}
splittedComments := strings.Split(line, "'")
if len(splittedComments) > 1 {
comment := strings.ReplaceAll(splittedComments[1], "'", "")
if comment != "" {
json.Unmarshal(parseHumanFriendlyAttrs(comment), instance)
}
resource.AddInstances(instance)
@@ -387,7 +455,91 @@ func (d *Workflow) extractResourcePlantUML(line string, resource resources.Resou
d.Graph.Items[item.ID] = *item
}
return varName, item, nil
return varName, item, warns, nil
}
// resolveExistingResource tries to find an existing catalog resource matching
// the given name or the id embedded in the PlantUML comment.
// Returns (resource, warning message) or (nil, "") if none found.
func (d *Workflow) resolveExistingResource(
proto resources.ResourceInterface,
dataName, name, comment string,
request *tools.APIRequest,
) (resources.ResourceInterface, string) {
accessor := proto.GetAccessor(request)
// 1. Try lookup by id from comment ("id: <uuid>").
if comment != "" {
attrs := map[string]any{}
json.Unmarshal(parseHumanFriendlyAttrs(comment), &attrs)
if id, ok := attrs["id"].(string); ok && id != "" {
if dbObj, _, err := accessor.LoadOne(id); err == nil && dbObj != nil {
if ri, ok := dbObj.(resources.ResourceInterface); ok {
return ri, fmt.Sprintf(`[import warning] %s "%s": existing resource retrieved by id %s`, dataName, name, id)
}
}
}
}
// 2. Try search by exact name.
filter := &dbs.Filters{
Or: map[string][]dbs.Filter{
"abstractobject.name": {{Operator: dbs.EQUAL.String(), Value: name}},
},
}
if results, _, err := accessor.Search(filter, "", false, 0, 10); err == nil {
for _, r := range results {
if r.GetName() == name {
if dbObj, _, err2 := accessor.LoadOne(r.GetID()); err2 == nil && dbObj != nil {
if ri, ok := dbObj.(resources.ResourceInterface); ok {
return ri, fmt.Sprintf(`[import warning] %s "%s": existing resource found by name and retrieved instead of creating a new one`, dataName, name)
}
}
}
}
}
return nil, ""
}
// addExistingGraphItem registers an already-existing resource in the workflow's
// ID lists and graph, without adding it to the resource lists (so generateResource
// won't try to store it again).
func (d *Workflow) addExistingGraphItem(dataName string, resource resources.ResourceInterface) *graph.GraphItem {
graphItem := &graph.GraphItem{
ID: uuid.New().String(),
ItemResource: &resources.ItemResource{},
}
switch dataName {
case "Data":
d.Datas = append(d.Datas, resource.GetID())
if r, ok := resource.(*resources.DataResource); ok {
graphItem.Data = r
}
case "Processing":
d.Processings = append(d.Processings, resource.GetID())
if r, ok := resource.(*resources.ProcessingResource); ok {
graphItem.Processing = r
}
case "Service":
d.Services = append(d.Services, resource.GetID())
if r, ok := resource.(*resources.ServiceResource); ok {
graphItem.Service = r
}
case "Storage":
d.Storages = append(d.Storages, resource.GetID())
if r, ok := resource.(*resources.StorageResource); ok {
graphItem.Storage = r
}
case "ComputeUnit":
d.Computes = append(d.Computes, resource.GetID())
if r, ok := resource.(*resources.ComputeResource); ok {
graphItem.Compute = r
}
default:
return nil
}
return graphItem
}
func (d *Workflow) getNewGraphItem(dataName string, resource resources.ResourceInterface) *graph.GraphItem {
@@ -407,6 +559,14 @@ func (d *Workflow) getNewGraphItem(dataName string, resource resources.ResourceI
d.Processings = append(d.Processings, resource.GetID())
d.ProcessingResources = append(d.ProcessingResources, resource.(*resources.ProcessingResource))
graphItem.Processing = resource.(*resources.ProcessingResource)
case "Service":
d.Services = append(d.Services, resource.GetID())
d.ServiceResources = append(d.ServiceResources, resource.(*resources.ServiceResource))
graphItem.Service = resource.(*resources.ServiceResource)
case "Dynamic":
d.Dynamics = append(d.Dynamics, resource.GetID())
d.DynamicResources = append(d.DynamicResources, resource.(*resources.DynamicResource))
graphItem.Dynamic = resource.(*resources.DynamicResource)
case "WorkflowEvent":
// The resource is already a *NativeTool with Kind=WORKFLOW_EVENT set by the
// catalog factory. We use it directly without any DB lookup.
@@ -438,6 +598,8 @@ func (d *Workflow) getNewInstance(dataName string, name string, peerID string) r
return resources.NewStorageResourceInstance(name, peerID)
case "ComputeUnit":
return resources.NewComputeResourceInstance(name, peerID)
case "Service":
return resources.NewServiceInstance(name, peerID)
default:
return nil
}
@@ -618,13 +780,14 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
if start.Before(now) {
start = now
}
// PLANNED: honour the caller's start date as-is.
// PLANNED: honour the caller's start date as-is.
}
priceds := map[tools.DataType]map[string]pricing.PricedItemITF{}
var err error
// 2. Plan processings first so we can derive the total workflow duration.
// 2. Plan processings and services first so we can derive the total workflow duration.
// Services in DEPLOYMENT mode return duration=-1 (open-ended); HOSTED mode returns a bounded call window.
ps, priceds, err := plan[*resources.ProcessingResource](tools.PROCESSING_RESOURCE, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request, wf.Graph.IsProcessing,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
d, err := wf.Graph.GetAverageTimeProcessingBeforeStart(0, res.GetID(),
@@ -641,6 +804,24 @@ func (wf *Workflow) Planify(start time.Time, end *time.Time, instances ConfigIte
if err != nil {
return false, 0, priceds, nil, err
}
if _, priceds, err = plan[*resources.ServiceResource](tools.SERVICE_RESOURCE, instances, partnerships, buyings, strategies, bookingMode, wf, priceds, request, wf.Graph.IsService,
func(res resources.ResourceInterface, priced pricing.PricedItemITF) (time.Time, float64, error) {
d, err := wf.Graph.GetAverageTimeProcessingBeforeStart(0, res.GetID(),
*instances.Get(res.GetID()), *partnerships.Get(res.GetID()), *buyings.Get(res.GetID()), *strategies.Get(res.GetID()),
bookingMode, request)
if err != nil {
return start, 0, err
}
return start.Add(time.Duration(d) * time.Second), priced.GetExplicitDurationInS(), nil
}, func(started time.Time, duration float64) (*time.Time, error) {
if duration < 0 {
return nil, nil // DEPLOYMENT mode: open-ended
}
s := started.Add(time.Duration(duration) * time.Second)
return &s, nil
}); err != nil {
return false, 0, priceds, nil, err
}
// Total workflow duration used as the booking window for compute/storage.
// Returns -1 if any processing is a service (open-ended).
@@ -790,7 +971,9 @@ func (w *Workflow) GetItemsByResources() map[tools.DataType]map[string][]string
tools.DATA_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsData) },
tools.COMPUTE_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsCompute) },
tools.PROCESSING_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsProcessing) },
tools.SERVICE_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsService) },
tools.WORKFLOW_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsWorkflow) },
tools.DYNAMIC_RESOURCE: func() []graph.GraphItem { return w.GetGraphItems(w.Graph.IsDynamic) },
}
for dt, meth := range dtMethodMap {
@@ -848,3 +1031,518 @@ func plan[T resources.ResourceInterface](
}
return resources, priceds, nil
}
// ── Integrity validation ─────────────────────────────────────────────────────
// Arrow direction constants matching the flutter_flow_chart ArrowDirection enum
// (index order: forward=0, backward=1, bidirectionnal=2).
const (
arrowDirectionBackward int64 = 1
)
// ViolationSeverity distinguishes blocking errors from non-blocking warnings.
type ViolationSeverity int
const (
SeverityError ViolationSeverity = iota // Blocks scheduling — must be fixed.
SeverityWarning // Reported but non-blocking.
)
// ViolationType identifies the category of the violation.
// Mirrors the TopologyErrorType / TopologyWarningType enums in oc-front.
type ViolationType string
const (
// Errors — block scheduling
ViolationVariableNotFound ViolationType = "variable_not_found"
ViolationMissingComputeUnit ViolationType = "missing_compute_unit"
ViolationCycle ViolationType = "cycle"
ViolationMissingDataStorage ViolationType = "missing_data_storage"
ViolationRequiredOutputMissing ViolationType = "required_output_missing"
// Warnings — non-blocking, reported for UX
ViolationInvertedArrow ViolationType = "inverted_arrow"
ViolationIsolatedProcessing ViolationType = "isolated_processing"
ViolationStorageNotLinkedToProcessing ViolationType = "storage_not_linked_to_processing"
)
// IntegrityViolation describes a single structural or semantic problem
// found in the workflow graph.
type IntegrityViolation struct {
Severity ViolationSeverity
Type ViolationType
ItemIDs []string // graph item IDs involved in the violation
Message string
}
func (v IntegrityViolation) IsError() bool { return v.Severity == SeverityError }
func (v IntegrityViolation) IsWarning() bool { return v.Severity == SeverityWarning }
// ValidateIntegrity checks the structural and semantic integrity of the workflow
// graph. It must be called by both oc-front (UX enforcement) and oc-schedulerd
// (sovereign enforcement, regardless of submission source — the front can be
// bypassed via direct API calls).
//
// Errors (block scheduling):
// 1. Variable not found — an arg references $varName not defined in env/inputs.
// 2. Missing compute — a Processing/non-HOSTED Service has no Compute linked.
// 3. Cycle — the processing DAG contains a directed cycle.
// 4. Missing data storage — a Data with Source has no Storage linked.
//
// Warnings (non-blocking):
// 5. Inverted arrow — a backward link between two processing nodes.
// 6. Isolated processing — a processing node with no processing neighbours.
// 7. Storage not linked to processing — a storage node orphaned from any processing.
func (w *Workflow) ValidateIntegrity() []IntegrityViolation {
var violations []IntegrityViolation
violations = append(violations, w.validateVariables()...)
violations = append(violations, w.validateRequiredInputs()...)
violations = append(violations, w.validateComputeLinks()...)
violations = append(violations, w.detectCycles()...)
violations = append(violations, w.validateDataStorageLinks()...)
violations = append(violations, w.detectInvertedArrows()...)
violations = append(violations, w.detectIsolatedProcessings()...)
violations = append(violations, w.detectOrphanedStorages()...)
return violations
}
// HasCriticalViolations returns true when ValidateIntegrity found at least one Error.
// oc-schedulerd uses this to reject a workflow without inspecting each violation.
func (w *Workflow) HasCriticalViolations() bool {
for _, v := range w.ValidateIntegrity() {
if v.IsError() {
return true
}
}
return false
}
// itemName returns a human-readable name for a graph item (falls back to itemID).
func (w *Workflow) itemName(itemID string) string {
item, ok := w.Graph.Items[itemID]
if !ok {
return itemID
}
_, res := item.GetResource()
if res != nil {
return res.GetName()
}
return itemID
}
// validateVariables checks that every $varName reference inside w.Args is
// defined in the corresponding element's env or inputs — mirroring
// WorkflowFactory.validateArgs() in oc-front.
var varRefPattern = regexp.MustCompile(`\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?`)
func (w *Workflow) validateVariables() []IntegrityViolation {
var violations []IntegrityViolation
for itemID, argList := range w.Args {
if len(argList) == 0 {
continue
}
available := map[string]struct{}{}
for _, p := range w.Env[itemID] {
if p.Name != "" {
available[p.Name] = struct{}{}
}
}
for _, p := range w.Inputs[itemID] {
if p.Name != "" {
available[p.Name] = struct{}{}
}
}
name := w.itemName(itemID)
for _, arg := range argList {
for _, m := range varRefPattern.FindAllStringSubmatch(arg, -1) {
varName := m[1]
if _, ok := available[varName]; !ok {
violations = append(violations, IntegrityViolation{
Severity: SeverityError,
Type: ViolationVariableNotFound,
ItemIDs: []string{itemID},
Message: fmt.Sprintf(`"%s": arg "%s" → variable $%s is not defined in env or inputs`, name, arg, varName),
})
}
}
}
}
return violations
}
// validateComputeLinks checks that every Processing node (and every non-HOSTED
// Service node) has at least one Compute linked — mirroring the computeErrors
// block in oc-front's checkTopology().
func (w *Workflow) validateComputeLinks() []IntegrityViolation {
var violations []IntegrityViolation
for id, item := range w.Graph.Items {
needsCompute := false
var name string
switch {
case w.Graph.IsProcessing(item) && item.Processing != nil:
// IsService processings are long-running services and don't need a Compute booking.
if item.Processing.IsService {
continue
}
needsCompute = true
name = item.Processing.GetName()
case w.Graph.IsService(item) && item.Service != nil:
// HOSTED services use an existing endpoint — no Compute booking needed.
inst := item.Service.GetSelectedInstance(nil)
if inst != nil {
if si, ok := inst.(*resources.ServiceInstance); ok && si.Mode == resources.HOSTED {
continue
}
}
needsCompute = true
name = item.Service.GetName()
}
if !needsCompute {
continue
}
hasCompute := false
for _, link := range w.Graph.Links {
var otherID string
if link.Source.ID == id {
otherID = link.Destination.ID
} else if link.Destination.ID == id {
otherID = link.Source.ID
} else {
continue
}
if other, ok := w.Graph.Items[otherID]; ok && w.Graph.IsCompute(other) {
hasCompute = true
break
}
}
if !hasCompute {
violations = append(violations, IntegrityViolation{
Severity: SeverityError,
Type: ViolationMissingComputeUnit,
ItemIDs: []string{id},
Message: fmt.Sprintf(`"%s" has no compute unit linked`, name),
})
}
}
return violations
}
// detectCycles runs DFS colouring on the processing→processing directed graph
// and reports any back-edge as a cycle error — mirroring dfsCycle() in oc-front.
func (w *Workflow) detectCycles() []IntegrityViolation {
var violations []IntegrityViolation
// Collect processing + service + event node IDs (execution flux nodes).
procIDs := map[string]struct{}{}
for id, item := range w.Graph.Items {
if w.Graph.IsProcessing(item) || w.Graph.IsService(item) || w.Graph.IsNativeTool(item) {
procIDs[id] = struct{}{}
}
}
// Build directed successors honoring ArrowDirection.
successors := map[string][]string{}
for id := range procIDs {
successors[id] = []string{}
}
for _, link := range w.Graph.Links {
src, dst := link.Source.ID, link.Destination.ID
_, srcIsProc := procIDs[src]
_, dstIsProc := procIDs[dst]
if !srcIsProc || !dstIsProc {
continue
}
dir := int64(0)
if link.Style != nil {
dir = link.Style.ArrowDirection
}
if dir == arrowDirectionBackward {
// Visual arrow reversed: dst runs before src.
successors[dst] = append(successors[dst], src)
} else {
successors[src] = append(successors[src], dst)
}
}
// DFS colouring: 0=white, 1=grey (in stack), 2=black (done).
color := map[string]int{}
reported := map[string]struct{}{}
var dfs func(u string)
dfs = func(u string) {
color[u] = 1
for _, v := range successors[u] {
if color[v] == 1 {
key := u + "→" + v
if _, seen := reported[key]; !seen {
reported[key] = struct{}{}
violations = append(violations, IntegrityViolation{
Severity: SeverityError,
Type: ViolationCycle,
ItemIDs: []string{u, v},
Message: fmt.Sprintf(`Infinite loop: "%s" → "%s" creates a cycle that would block execution indefinitely`,
w.itemName(u), w.itemName(v)),
})
}
} else if color[v] == 0 {
dfs(v)
}
}
color[u] = 2
}
for id := range procIDs {
if color[id] == 0 {
dfs(id)
}
}
return violations
}
// validateDataStorageLinks checks that every Data item with a non-empty Source
// has at least one Storage linked — the builder needs this to inject the
// download step (curl or NATS/Minio protocol).
func (w *Workflow) validateDataStorageLinks() []IntegrityViolation {
var violations []IntegrityViolation
dataStorageLinks := w.Graph.GetDataStorageLinks()
linkedStorage := map[string]struct{}{}
for _, dsl := range dataStorageLinks {
linkedStorage[dsl.DataItemID] = struct{}{}
}
for id, item := range w.Graph.Items {
if !w.Graph.IsData(item) || item.Data == nil {
continue
}
hasSource := false
for _, inst := range item.Data.Instances {
if inst.Access.HasSource() {
hasSource = true
break
}
}
if !hasSource {
continue
}
if _, ok := linkedStorage[id]; !ok {
violations = append(violations, IntegrityViolation{
Severity: SeverityError,
Type: ViolationMissingDataStorage,
ItemIDs: []string{id},
Message: fmt.Sprintf(`data "%s" has a source but no Storage linked`, item.Data.GetName()),
})
}
}
return violations
}
// validateRequiredInputs checks that for each processing node with a required
// input, every immediate predecessor outputs a parameter with that name.
// Mirrors the requiredOutputMissing check in oc-front's checkTopology().
func (w *Workflow) validateRequiredInputs() []IntegrityViolation {
var violations []IntegrityViolation
procIDs := map[string]struct{}{}
for id, item := range w.Graph.Items {
if w.Graph.IsProcessing(item) || w.Graph.IsService(item) || w.Graph.IsNativeTool(item) {
procIDs[id] = struct{}{}
}
}
// Build direct predecessors map.
predecessors := map[string][]string{}
for id := range procIDs {
predecessors[id] = []string{}
}
for _, link := range w.Graph.Links {
src, dst := link.Source.ID, link.Destination.ID
_, srcIsProc := procIDs[src]
_, dstIsProc := procIDs[dst]
if !srcIsProc || !dstIsProc {
continue
}
dir := int64(0)
if link.Style != nil {
dir = link.Style.ArrowDirection
}
if dir == arrowDirectionBackward {
predecessors[src] = append(predecessors[src], dst)
} else {
predecessors[dst] = append(predecessors[dst], src)
}
}
for id, reqInputs := range w.Inputs {
if _, isProc := procIDs[id]; !isProc {
continue
}
for _, inp := range reqInputs {
if !inp.Required || inp.Name == "" {
continue
}
for _, predID := range predecessors[id] {
if !w.nodeHasOutput(predID, inp.Name) {
violations = append(violations, IntegrityViolation{
Severity: SeverityError,
Type: ViolationRequiredOutputMissing,
ItemIDs: []string{id, predID},
Message: fmt.Sprintf(
`"%s" requires input "%s" but "%s" does not output it`,
w.itemName(id), inp.Name, w.itemName(predID),
),
})
}
}
}
}
return violations
}
// nodeHasOutput returns true if the given node outputs a parameter named name,
// either via workflow-level outputs or its resource's own outputs.
func (w *Workflow) nodeHasOutput(nodeID, name string) bool {
for _, p := range w.Outputs[nodeID] {
if p.Name == name {
return true
}
}
item, ok := w.Graph.Items[nodeID]
if !ok {
return false
}
var res resources.ResourceInterface
switch {
case item.Processing != nil:
res = item.Processing
case item.Service != nil:
res = item.Service
}
if res != nil {
for _, p := range res.GetOutputs() {
if p.Name == name {
return true
}
}
}
return false
}
// detectInvertedArrows warns when a link between two processing nodes uses a
// backward arrow direction — mirroring the invertedArrow warning in oc-front.
func (w *Workflow) detectInvertedArrows() []IntegrityViolation {
var violations []IntegrityViolation
for _, link := range w.Graph.Links {
if link.Style == nil || link.Style.ArrowDirection != arrowDirectionBackward {
continue
}
srcItem, srcOK := w.Graph.Items[link.Source.ID]
dstItem, dstOK := w.Graph.Items[link.Destination.ID]
if !srcOK || !dstOK {
continue
}
if (w.Graph.IsProcessing(srcItem) || w.Graph.IsService(srcItem)) &&
(w.Graph.IsProcessing(dstItem) || w.Graph.IsService(dstItem)) {
violations = append(violations, IntegrityViolation{
Severity: SeverityWarning,
Type: ViolationInvertedArrow,
ItemIDs: []string{link.Source.ID, link.Destination.ID},
Message: fmt.Sprintf(`Reversed arrow between "%s" & "%s": "%s" will execute before "%s" unexpectedly`,
w.itemName(link.Destination.ID), w.itemName(link.Source.ID),
w.itemName(link.Destination.ID), w.itemName(link.Source.ID)),
})
}
}
return violations
}
// detectIsolatedProcessings warns when a processing node has no link to another
// processing node — it will execute synchronously with the workflow's first elements.
func (w *Workflow) detectIsolatedProcessings() []IntegrityViolation {
var violations []IntegrityViolation
procIDs := map[string]struct{}{}
for id, item := range w.Graph.Items {
if w.Graph.IsProcessing(item) || w.Graph.IsService(item) || w.Graph.IsNativeTool(item) {
procIDs[id] = struct{}{}
}
}
for id := range procIDs {
hasProcNeighbour := false
for _, link := range w.Graph.Links {
var otherID string
if link.Source.ID == id {
otherID = link.Destination.ID
} else if link.Destination.ID == id {
otherID = link.Source.ID
} else {
continue
}
if _, ok := procIDs[otherID]; ok {
hasProcNeighbour = true
break
}
}
if !hasProcNeighbour {
violations = append(violations, IntegrityViolation{
Severity: SeverityWarning,
Type: ViolationIsolatedProcessing,
ItemIDs: []string{id},
Message: fmt.Sprintf(`"%s" is isolated (no connection with another processing) — will execute synchronously with the workflow's first element(s)`,
w.itemName(id)),
})
}
}
return violations
}
// detectOrphanedStorages warns when a storage node is not linked to any
// processing node — it contributes no data flow to the workflow.
func (w *Workflow) detectOrphanedStorages() []IntegrityViolation {
var violations []IntegrityViolation
for id, item := range w.Graph.Items {
if !w.Graph.IsStorage(item) {
continue
}
linkedTopics := map[string]struct{}{}
for _, link := range w.Graph.Links {
var otherID string
if link.Source.ID == id {
otherID = link.Destination.ID
} else if link.Destination.ID == id {
otherID = link.Source.ID
} else {
continue
}
if other, ok := w.Graph.Items[otherID]; ok {
switch {
case w.Graph.IsProcessing(other):
linkedTopics["processing"] = struct{}{}
case w.Graph.IsCompute(other):
linkedTopics["compute"] = struct{}{}
case w.Graph.IsData(other):
linkedTopics["data"] = struct{}{}
case w.Graph.IsService(other):
linkedTopics["service"] = struct{}{}
}
}
}
if _, ok := linkedTopics["processing"]; ok {
continue
}
name := w.itemName(id)
var msg string
if len(linkedTopics) == 0 {
msg = fmt.Sprintf(`"%s" is isolated (not linked to anything)`, name)
} else {
topics := make([]string, 0, len(linkedTopics))
for t := range linkedTopics {
topics = append(topics, t)
}
msg = fmt.Sprintf(`"%s" is not linked to any processing (only linked to: %s)`, name, strings.Join(topics, ", "))
}
violations = append(violations, IntegrityViolation{
Severity: SeverityWarning,
Type: ViolationStorageNotLinkedToProcessing,
ItemIDs: []string{id},
Message: msg,
})
}
return violations
}
+12 -8
View File
@@ -150,7 +150,7 @@ func (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: workflow.Name + "_workspace"}},
},
}
resource, _, err := a.workspaceAccessor.Search(filters, "", workflow.IsDraft)
resource, _, err := a.workspaceAccessor.Search(filters, "", workflow.IsDraft, 0, 10)
if delete { // if delete is set to true, delete the workspace
for _, r := range resource {
a.workspaceAccessor.DeleteOne(r.GetID())
@@ -192,9 +192,9 @@ func (a *workflowMongoAccessor) LoadOne(id string) (utils.DBObject, int, error)
}, a)
}
func (a *workflowMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *workflowMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Workflow](filters, search, a.New().GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject { return a.verifyResource(d) }, isDraft, a)
func(d utils.DBObject) utils.ShallowDBObject { return a.verifyResource(d) }, isDraft, a, offset, limit)
}
func (a *workflowMongoAccessor) verifyResource(obj utils.DBObject) utils.DBObject {
@@ -210,15 +210,19 @@ func (a *workflowMongoAccessor) verifyResource(obj utils.DBObject) utils.DBObjec
var access utils.Accessor
switch t {
case tools.COMPUTE_RESOURCE:
access = resources.NewAccessor[*resources.ComputeResource](t, a.GetRequest(), func() utils.DBObject { return &resources.ComputeResource{} })
access = resources.NewAccessor[*resources.ComputeResource](t, a.GetRequest())
case tools.PROCESSING_RESOURCE:
access = resources.NewAccessor[*resources.ProcessingResource](t, a.GetRequest(), func() utils.DBObject { return &resources.ProcessingResource{} })
access = resources.NewAccessor[*resources.ProcessingResource](t, a.GetRequest())
case tools.STORAGE_RESOURCE:
access = resources.NewAccessor[*resources.StorageResource](t, a.GetRequest(), func() utils.DBObject { return &resources.StorageResource{} })
access = resources.NewAccessor[*resources.StorageResource](t, a.GetRequest())
case tools.WORKFLOW_RESOURCE:
access = resources.NewAccessor[*resources.WorkflowResource](t, a.GetRequest(), func() utils.DBObject { return &resources.WorkflowResource{} })
access = resources.NewAccessor[*resources.WorkflowResource](t, a.GetRequest())
case tools.DATA_RESOURCE:
access = resources.NewAccessor[*resources.DataResource](t, a.GetRequest(), func() utils.DBObject { return &resources.DataResource{} })
access = resources.NewAccessor[*resources.DataResource](t, a.GetRequest())
case tools.NATIVE_TOOL:
access = resources.NewAccessor[*resources.NativeTool](t, a.GetRequest())
case tools.SERVICE_RESOURCE:
access = resources.NewAccessor[*resources.ServiceResource](t, a.GetRequest())
default:
wf.Graph.Clear(resource.GetID())
}
@@ -0,0 +1,168 @@
package workflow_execution
import (
"slices"
"time"
workflowgraph "cloud.o-forge.io/core/oc-lib/models/workflow/graph"
)
// ExecutionStepState is the runtime state of a single step in the execution graph.
type ExecutionStepState string
const (
StepWaiting ExecutionStepState = "waiting"
StepRunning ExecutionStepState = "running"
StepSuccess ExecutionStepState = "success"
StepFailure ExecutionStepState = "failure"
)
// ExecutionGraphItem is the summarized view of one node in the workflow execution graph.
//
// - Name : human-readable label (resource name or item ID as fallback)
// - StartDate : set when the step transitions to StepRunning
// - EndDate : set when the step transitions to StepSuccess or StepFailure
// - State : current lifecycle state of the step
// - Deps : itemIDs that must reach StepSuccess before this step can start
// - WhenRunning : itemIDs (resources) that become active while this step is running
// (e.g. the compute node executing it, the storage it reads/writes)
type ExecutionGraphItem struct {
Name string `json:"name" bson:"name"`
StartDate *time.Time `json:"start_date,omitempty" bson:"start_date,omitempty"`
EndDate *time.Time `json:"end_date,omitempty" bson:"end_date,omitempty"`
State ExecutionStepState `json:"state" bson:"state"`
Deps []string `json:"deps,omitempty" bson:"deps,omitempty"`
WhenRunning []string `json:"when_running,omitempty" bson:"when_running,omitempty"`
}
// ExecutionGraph is a flat, scheduler-friendly summary of a workflow execution graph.
// The map key is the workflow graph item ID.
type ExecutionGraph map[string]ExecutionGraphItem
// BuildExecutionGraph derives an initial ExecutionGraph (all steps in StepWaiting)
// from a workflow graph. It infers:
// - Deps : predecessor item IDs based on link direction
// - WhenRunning : sibling item IDs connected to a step by a link
// (i.e. resources that are co-active when the step runs)
func BuildExecutionGraph(g *workflowgraph.Graph) ExecutionGraph {
if g == nil {
return ExecutionGraph{}
}
// deps[dst] = list of src item IDs that dst depends on
deps := map[string][]string{}
// whenRunning[id] = list of item IDs active while id is running
whenRunning := map[string][]string{}
for _, link := range g.Links {
src := link.Source.ID
dst := link.Destination.ID
if src == "" || dst == "" {
continue
}
srcItem, srcOk := g.Items[src]
dstItem, dstOk := g.Items[dst]
if !srcOk || !dstOk {
continue
}
// Steps (logical nodes that sequence execution): Data, Processing, Workflow, NativeTool.
// Resources (infrastructure co-active while a step runs): Compute, Storage.
srcIsStep := srcItem.ItemResource.Data != nil || srcItem.ItemResource.Processing != nil || srcItem.ItemResource.Workflow != nil || srcItem.ItemResource.NativeTool != nil
dstIsStep := dstItem.ItemResource.Data != nil || dstItem.ItemResource.Processing != nil || dstItem.ItemResource.Workflow != nil || dstItem.ItemResource.NativeTool != nil
srcIsResource := srcItem.ItemResource.Compute != nil || srcItem.ItemResource.Storage != nil
dstIsResource := dstItem.ItemResource.Compute != nil || dstItem.ItemResource.Storage != nil
switch {
case srcIsStep && dstIsStep:
// Sequential dependency: dst must wait for src to succeed.
deps[dst] = appendUnique(deps[dst], src)
case srcIsStep && dstIsResource:
// src activates dst (compute/storage) while running.
whenRunning[src] = appendUnique(whenRunning[src], dst)
case srcIsResource && dstIsStep:
// dst uses src (compute/storage) while running.
whenRunning[dst] = appendUnique(whenRunning[dst], src)
}
}
eg := ExecutionGraph{}
for id, item := range g.Items {
name := id
_, r := item.GetResource()
if r != nil && r.GetName() != "" {
name = r.GetName()
}
eg[id] = ExecutionGraphItem{
Name: name,
State: StepWaiting,
Deps: deps[id],
WhenRunning: whenRunning[id],
}
}
return eg
}
// MarkRunning transitions the step to StepRunning and records the start time.
// It is a no-op if the step is already beyond StepRunning.
func (eg ExecutionGraph) MarkRunning(itemID string, at time.Time) {
item, ok := eg[itemID]
if !ok || item.State == StepSuccess || item.State == StepFailure {
return
}
item.State = StepRunning
item.StartDate = &at
eg[itemID] = item
}
// MarkDone transitions the step to StepSuccess or StepFailure and records the end time.
func (eg ExecutionGraph) MarkDone(itemID string, success bool, at time.Time) {
item, ok := eg[itemID]
if !ok {
return
}
if success {
item.State = StepSuccess
} else {
item.State = StepFailure
}
item.EndDate = &at
eg[itemID] = item
}
// Depssatisfied returns true when all deps of the given item have reached StepSuccess.
func (eg ExecutionGraph) Depssatisfied(itemID string) bool {
item, ok := eg[itemID]
if !ok {
return false
}
for _, dep := range item.Deps {
depItem, depOk := eg[dep]
if !depOk || depItem.State != StepSuccess {
return false
}
}
return true
}
// ReadyToRun returns the IDs of all steps that are still waiting and whose deps
// are fully satisfied. Useful for the scheduler to decide what to start next.
func (eg ExecutionGraph) ReadyToRun() []string {
ready := []string{}
for id, item := range eg {
if item.State == StepWaiting && eg.Depssatisfied(id) {
ready = append(ready, id)
}
}
return ready
}
func appendUnique(slice []string, val string) []string {
if slices.Contains(slice, val) {
return slice
}
return append(slice, val)
}
@@ -9,6 +9,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"
"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"
@@ -17,6 +18,17 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
// BookingState tracks the reservation and completion status of a single booking
// within a workflow execution.
// - IsBooked: true while the resource is actively reserved (set on WORKFLOW_STARTED_EVENT,
// cleared on WORKFLOW_STEP_DONE_EVENT / WORKFLOW_DONE_EVENT).
// - IsDone: true once the booking has been confirmed by the remote peer (CONSIDERS_EVENT)
// or completed (WORKFLOW_STEP_DONE_EVENT / WORKFLOW_DONE_EVENT).
type BookingState struct {
IsBooked bool `json:"is_booked" bson:"is_booked"`
IsDone bool `json:"is_done" bson:"is_done"`
}
/*
* WorkflowExecution is a struct that represents a list of workflow executions
* Warning: No user can write (del, post, put) a workflow execution, it is only used by the system
@@ -33,13 +45,43 @@ type WorkflowExecution struct {
State enum.BookingStatus `json:"state" bson:"state" default:"0"` // TEMPORARY TODO DEFAULT 1 -> 0 State is the state of the workflow
WorkflowID string `json:"workflow_id" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
BookingsState map[string]bool `json:"bookings_state" bson:"bookings_state,omitempty"` // WorkflowID is the ID of the workflow
PurchasesState map[string]bool `json:"purchases_state" bson:"purchases_state,omitempty"` // WorkflowID is the ID of the workflow
BookingsState map[string]BookingState `json:"bookings_state" bson:"bookings_state,omitempty"` // booking_id → reservation+completion status
PurchasesState map[string]bool `json:"purchases_state" bson:"purchases_state,omitempty"` // purchase_id → confirmed
// Graph is a lightweight, real-time summary of the workflow execution graph.
// Keyed by workflow graph item ID; updated by oc-scheduler on each step-done event.
// Consumed by oc-front to render the live execution panel via websocket updates.
Graph ExecutionGraph `json:"graph,omitempty" bson:"graph,omitempty"`
SelectedInstances workflow.ConfigItem `json:"selected_instances"`
SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
SelectedBuyings workflow.ConfigItem `json:"selected_buyings"`
SelectedStrategies workflow.ConfigItem `json:"selected_strategies"`
SelectedPaymentMode workflow.ConfigItem `json:"selected_payment_mode"`
// SelectedBillingStrategy est la fréquence de facturation globale choisie par l'utilisateur
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
// Propagée depuis WorkflowSchedule.SelectedBillingStrategy par GenerateExecutions().
SelectedBillingStrategy pricing.BillingStrategy `json:"selected_billing_strategy" bson:"selected_billing_strategy"`
// SelectedEmbeddedStorages records which storage capability was activated on
// each compute unit graph node (key = compute graph node ID).
// Populated by oc-scheduler, consumed by oc-monitord's argo builder.
SelectedEmbeddedStorages map[string]*resources.EmbeddedStorageSelection `json:"selected_embedded_storages,omitempty" bson:"selected_embedded_storages,omitempty"`
}
func (ri *WorkflowExecution) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "workflow":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.PEER)
}
}
return ext
}
func (r *WorkflowExecution) StoreDraftDefault() {
@@ -78,7 +120,7 @@ func (ws *WorkflowExecution) PurgeDraft(request *tools.APIRequest) error {
{Operator: dbs.GTE.String(), Value: primitive.NewDateTimeFromTime(ws.ExecDate)},
},
},
}, "", ws.IsDraft)
}, "", ws.IsDraft, 0, 10000)
if code != 200 || err != nil {
return err
}
@@ -130,6 +172,7 @@ 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])...)
purchases = append(purchases, d.buyEach(bs, executionsID, wfID, tools.SERVICE_RESOURCE, priceds[tools.SERVICE_RESOURCE])...)
d.PurchasesState = map[string]bool{}
for _, p := range purchases {
d.PurchasesState[p.GetID()] = false
@@ -159,7 +202,11 @@ func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID str
var m map[string]interface{}
b, _ := json.Marshal(priced)
json.Unmarshal(b, &m)
end := start.Add(time.Duration(priced.GetExplicitDurationInS()) * time.Second)
var endDate *time.Time
if durS := priced.GetExplicitDurationInS(); durS > 0 {
e := start.Add(time.Duration(durS) * time.Second)
endDate = &e
}
bookingItem := &purchase_resource.PurchaseResource{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
@@ -173,7 +220,7 @@ func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID str
ResourceID: priced.GetID(),
InstanceID: priced.GetInstanceID(),
ResourceType: dt,
EndDate: &end,
EndDate: endDate,
}
items = append(items, bookingItem)
d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = append(
@@ -185,13 +232,14 @@ func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID str
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.SERVICE_RESOURCE, priceds[tools.SERVICE_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])...)
for _, p := range booking {
if d.BookingsState == nil {
d.BookingsState = map[string]bool{}
d.BookingsState = map[string]BookingState{}
}
d.BookingsState[p.GetID()] = false
d.BookingsState[p.GetID()] = BookingState{}
}
return booking
}
@@ -229,10 +277,24 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
var m map[string]interface{}
b, _ := json.Marshal(priced)
json.Unmarshal(b, &m)
name := priced.GetName()
if len(executionsID) > 8 {
name += " " + executionsID[:8]
}
// Résout le mode de paiement spécifique à cette ressource depuis SelectedPaymentMode.
// SelectedPaymentMode est un ConfigItem (map[string]int) dont les clés sont les IDs
// de nœuds du graph ; on tente itemID puis l'ID de la ressource comme fallback.
paymentType := pricing.PAY_ONCE
if v, ok := d.SelectedPaymentMode[itemID]; ok {
paymentType = pricing.PaymentType(v)
} else if v, ok := d.SelectedPaymentMode[priced.GetID()]; ok {
paymentType = pricing.PaymentType(v)
}
bookingItem := &booking.Booking{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: d.GetName() + "_" + executionsID + "_" + wfID,
Name: name + " " + start.Format("2006-01-02 15:04"),
IsDraft: true,
},
PricedItem: m,
@@ -242,10 +304,13 @@ func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.
InstanceID: priced.GetInstanceID(),
ResourceType: dt,
DestPeerID: priced.GetCreatorID(),
Peerless: priced.GetCreatorID() == "",
WorkflowID: wfID,
ExecutionID: d.GetID(),
ExpectedStartDate: start,
ExpectedEndDate: endDate,
BillingStrategy: d.SelectedBillingStrategy,
PaymentType: paymentType,
}
items = append(items, bookingItem)
d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append(
+9 -9
View File
@@ -49,7 +49,7 @@ func (a *workspaceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, erro
// UpdateOne updates a workspace in the database, given its ID, it automatically share to peers if the workspace is shared
func (a *workspaceMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
if set["active"] == true { // If the workspace is active, deactivate all the other workspaces
/*if set["active"] == true { // If the workspace is active, deactivate all the other workspaces
res, _, err := a.LoadAll(true)
if err == nil {
for _, r := range res {
@@ -59,7 +59,7 @@ func (a *workspaceMongoAccessor) UpdateOne(set map[string]interface{}, id string
}
}
}
}
}*/
res, code, err := utils.GenericUpdateOne(set, id, a)
if code == 200 && res != nil {
a.share(res.(*Workspace), tools.PUT, a.GetCaller())
@@ -76,8 +76,8 @@ func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject,
},
}
// filters *dbs.Filters, word string, isDraft bool
res, _, err := a.Search(filters, "", true) // Search for the workspace
if err == nil && len(res) > 0 { // If the workspace already exists, return an error
res, _, err := a.Search(filters, "", true, 0, 10) // Search for the workspace
if err == nil && len(res) > 0 { // If the workspace already exists, return an error
return nil, 409, errors.New("a workspace with the same name already exists")
}
// reset the resources
@@ -87,24 +87,24 @@ func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject,
}
func (a *workspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Workspace](id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
d.(*Workspace).Fill(a.GetRequest())
return d, 200, nil
}, a)
}
func (a *workspaceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *workspaceMongoAccessor) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Workspace](func(d utils.DBObject) utils.ShallowDBObject {
d.(*Workspace).Fill(a.GetRequest())
return d
}, isDraft, a)
}, isDraft, a, offset, limit)
}
func (a *workspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (a *workspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Workspace](filters, search, (&Workspace{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject {
d.(*Workspace).Fill(a.GetRequest())
return d
}, isDraft, a)
}, isDraft, a, offset, limit)
}
/*
+15 -5
View File
@@ -60,16 +60,16 @@ func (s State) String() string {
type API struct{}
func (a *API) Discovered(infos []*beego.ControllerInfo) {
func (a *API) Discovered(infos []*beego.ControllerInfo, extra ...map[string][]string) {
respondToDiscovery := func(resp NATSResponse) {
var m map[string]interface{}
json.Unmarshal(resp.Payload, &m)
if len(m) == 0 {
a.SubscribeRouter(infos)
a.SubscribeRouter(infos, extra...)
}
}
a.ListenRouter(respondToDiscovery)
a.SubscribeRouter(infos)
a.SubscribeRouter(infos, extra...)
}
// GetState returns the state of the API
@@ -99,11 +99,12 @@ func (a *API) ListenRouter(exec func(msg NATSResponse)) {
})
}
func (a *API) SubscribeRouter(infos []*beego.ControllerInfo) {
func (a *API) SubscribeRouter(infos []*beego.ControllerInfo, extra ...map[string][]string) {
nats := NewNATSCaller()
appPrefix := "/" + strings.ReplaceAll(config.GetAppName(), "oc-", "")
discovery := map[string][]string{}
for _, info := range infos {
path := strings.ReplaceAll(info.GetPattern(), "/oc/", "/"+strings.ReplaceAll(config.GetAppName(), "oc-", ""))
path := strings.ReplaceAll(info.GetPattern(), "/oc/", appPrefix+"/")
for k, v := range info.GetMethod() {
if discovery[path] == nil {
discovery[path] = []string{}
@@ -115,6 +116,15 @@ func (a *API) SubscribeRouter(infos []*beego.ControllerInfo) {
}
}
}
for _, extraRoutes := range extra {
for rawPath, methods := range extraRoutes {
path := strings.ReplaceAll(rawPath, "/oc/", appPrefix+"/")
if discovery[path] == nil {
discovery[path] = []string{}
}
discovery[path] = append(discovery[path], methods...)
}
}
b, _ := json.Marshal(discovery)
go nats.SetNATSPub(DISCOVERY, NATSResponse{
+67 -3
View File
@@ -32,6 +32,14 @@ const (
BILL
NATIVE_TOOL
EXECUTION_VERIFICATION
ALLOWED_IMAGE
SERVICE_RESOURCE
DYNAMIC_RESOURCE
LIVE_SERVICE
PAYMENT
REFUND
DISCOUNT
SUBSCRIPTION
)
var NOAPI = func() string {
@@ -88,6 +96,14 @@ var InnerDefaultAPI = [...]func() string{
NOAPI,
CATALOGAPI,
SCHEDULERAPI,
DATACENTERAPI,
CATALOGAPI,
CATALOGAPI,
DATACENTERAPI,
NOAPI,
NOAPI,
NOAPI,
NOAPI,
}
// Bind the standard data name to the data type
@@ -114,6 +130,14 @@ var Str = [...]string{
"bill",
"native_tool",
"execution_verification",
"allowed_image",
"service_resource",
"dynamic_resource",
"live_service",
"payment",
"refund",
"discount",
"subscription",
}
func FromString(comp string) int {
@@ -149,7 +173,8 @@ func DataTypeList() []DataType {
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE,
WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY,
ORDER, PURCHASE_RESOURCE,
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL}
LIVE_DATACENTER, LIVE_STORAGE, BILL, NATIVE_TOOL, EXECUTION_VERIFICATION, ALLOWED_IMAGE, SERVICE_RESOURCE, DYNAMIC_RESOURCE, LIVE_SERVICE,
PAYMENT, REFUND, DISCOUNT, SUBSCRIPTION}
}
type PropalgationMessage struct {
@@ -171,8 +196,18 @@ const (
PB_CONSIDERS
PB_ADMIRALTY_CONFIG
PB_MINIO_CONFIG
PB_PVC_CONFIG
PB_CLOSE_SEARCH
NONE
PB_OBSERVE
PB_OBSERVE_CLOSE
// PB_PROPAGATE is used by oc-discovery to broadcast a peer's online/offline
// state to other oc-discovery nodes in the federation via PROPALGATION_EVENT.
PB_PROPAGATE
// PB_SOURCE_PRESIGN is sent by oc-datacenter to request a pre-signed Minio URL
// for a private source resource (isReachable=false, Phase 4).
// oc-discovery routes it to the resource owner peer via ProtocolSourcePresignResource.
PB_SOURCE_PRESIGN
)
func GetActionString(ss string) PubSubAction {
@@ -199,14 +234,43 @@ func GetActionString(ss string) PubSubAction {
return PB_MINIO_CONFIG
case "close_search":
return PB_CLOSE_SEARCH
case "observe":
return PB_OBSERVE
case "observe_close":
return PB_OBSERVE_CLOSE
case "propagate":
return PB_PROPAGATE
case "source_presign":
return PB_SOURCE_PRESIGN
default:
return NONE
}
}
var path = []string{"search", "search_response", "create", "update", "delete", "planner", "close_planner",
"considers", "admiralty_config", "minio_config", "close_search"}
// path aligns with PubSubAction iota values for String().
var path = []string{
"search", // 0 PB_SEARCH
"search_response", // 1 PB_SEARCH_RESPONSE
"create", // 2 PB_CREATE
"update", // 3 PB_UPDATE
"delete", // 4 PB_DELETE
"planner", // 5 PB_PLANNER
"close_planner", // 6 PB_CLOSE_PLANNER
"considers", // 7 PB_CONSIDERS
"admiralty_config", // 8 PB_ADMIRALTY_CONFIG
"minio_config", // 9 PB_MINIO_CONFIG
"pvc_config", // 10 PB_PVC_CONFIG
"close_search", // 11 PB_CLOSE_SEARCH
"none", // 12 NONE
"observe", // 13 PB_OBSERVE
"observe_close", // 14 PB_OBSERVE_CLOSE
"propagate", // 15 PB_PROPAGATE
"source_presign", // 16 PB_SOURCE_PRESIGN
}
func (m PubSubAction) String() string {
if int(m) >= len(path) {
return "unknown"
}
return strings.ToUpper(path[m])
}
+85 -5
View File
@@ -14,6 +14,7 @@ import (
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -97,8 +98,8 @@ func (k *KubernetesService) CreateNamespace(ctx context.Context, ns string) erro
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ns,
Labels: map[string]string{
"multicluster-scheduler": "enabled",
Annotations: map[string]string{
"multicluster.admiralty.io/elect": "",
},
},
}
@@ -199,9 +200,9 @@ func (k *KubernetesService) ProvisionExecutionNamespace(ctx context.Context, ns
}
role := "argo-role"
if err := k.CreateRole(ctx, ns, role,
[][]string{{"coordination.k8s.io"}, {""}, {""}},
[][]string{{"leases"}, {"secrets"}, {"pods"}},
[][]string{{"get", "create", "update"}, {"get"}, {"patch"}},
[][]string{{"coordination.k8s.io"}, {""}, {""}, {"multicluster.admiralty.io"}, {"argoproj.io"}},
[][]string{{"leases"}, {"secrets"}, {"pods"}, {"podchaperons"}, {"workflowtaskresults"}},
[][]string{{"get", "create", "update"}, {"get"}, {"patch"}, {"get", "list", "watch", "create", "update", "patch", "delete"}, {"create", "patch"}},
); err != nil {
return err
}
@@ -598,6 +599,85 @@ func (k *KubernetesService) CreateSecret(context context.Context, minioId string
return nil
}
// DeleteSecretsByLabel deletes all Secrets in the given namespace matching labelSelector.
// Used by oc-datacenter to clean up ephemeral source-presigned Secrets after workflow completion.
func (k *KubernetesService) DeleteSecretsByLabel(ctx context.Context, namespace, labelSelector string) error {
return k.Set.CoreV1().Secrets(namespace).DeleteCollection(
ctx,
metav1.DeleteOptions{},
metav1.ListOptions{LabelSelector: labelSelector},
)
}
// CreatePVC creates a static PersistentVolume + PersistentVolumeClaim in the given namespace.
// Static provisioning (no StorageClass) avoids the WaitForFirstConsumer deadlock
// with Admiralty virtual nodes — the PVC binds immediately.
func (k *KubernetesService) CreatePVC(ctx context.Context, name, namespace, storageSize string) error {
storageClassName := ""
pv := &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1.PersistentVolumeSpec{
Capacity: v1.ResourceList{
v1.ResourceStorage: resource.MustParse(storageSize),
},
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
StorageClassName: storageClassName,
PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
PersistentVolumeSource: v1.PersistentVolumeSource{
HostPath: &v1.HostPathVolumeSource{
Path: "/var/lib/oc-storage/" + name,
Type: func() *v1.HostPathType { t := v1.HostPathDirectoryOrCreate; return &t }(),
},
},
ClaimRef: &v1.ObjectReference{
Namespace: namespace,
Name: name,
},
},
}
_, err := k.Set.CoreV1().PersistentVolumes().Create(ctx, pv, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("CreatePV %s: %w", name, err)
}
pvc := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
StorageClassName: &storageClassName,
VolumeName: name,
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse(storageSize),
},
},
},
}
_, err = k.Set.CoreV1().PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("CreatePVC %s/%s: %w", namespace, name, err)
}
return nil
}
// DeletePVC deletes a PersistentVolumeClaim and its associated PersistentVolume.
func (k *KubernetesService) DeletePVC(ctx context.Context, name, namespace string) error {
err := k.Set.CoreV1().PersistentVolumeClaims(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("DeletePVC %s/%s: %w", namespace, name, err)
}
err = k.Set.CoreV1().PersistentVolumes().Delete(ctx, name, metav1.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("DeletePV %s: %w", name, err)
}
return nil
}
// ============== ADMIRALTY ==============
// Returns a concatenation of the peerId and namespace in order for
// kubernetes ressources to have a unique name, under 63 characters
+30 -3
View File
@@ -29,8 +29,10 @@ type NATSMethod int
var meths = []string{"remove execution", "create execution", "planner execution", "discovery",
"workflow event", "argo kube event", "create resource", "remove resource",
"propalgation event", "search event", "confirm event",
"considers event", "admiralty config event", "minio config event",
"considers event", "admiralty config event", "minio config event", "pvc config event",
"workflow started event", "workflow step done event", "workflow done event",
"peer behavior event", "peer observe response event", "peer observe event",
"source presign event",
}
const (
@@ -53,6 +55,7 @@ const (
CONSIDERS_EVENT
ADMIRALTY_CONFIG_EVENT
MINIO_CONFIG_EVENT
PVC_CONFIG_EVENT
// Workflow lifecycle events emitted by oc-monitord.
// oc-scheduler listens to STARTED and DONE to maintain WorkflowExecution state.
@@ -60,6 +63,28 @@ const (
WORKFLOW_STARTED_EVENT
WORKFLOW_STEP_DONE_EVENT
WORKFLOW_DONE_EVENT
// PEER_BEHAVIOR_EVENT is emitted by any trusted service (oc-scheduler,
// oc-datacenter, …) when a peer exhibits suspicious or fraudulent behavior.
// oc-discovery consumes it to update the peer's trust score and auto-blacklist
// below threshold.
PEER_BEHAVIOR_EVENT
// PEER_OBSERVE_RESPONSE_EVENT is emitted by oc-discovery each time it receives
// a heartbeat from an observed remote peer. oc-peer listens to this event to
// update the WS connectivity state for its clients.
PEER_OBSERVE_RESPONSE_EVENT
// PEER_OBSERVE_EVENT is emitted by oc-peer to request oc-discovery to start
// or stop observing a remote peer. Payload contains the target peer_id and a
// boolean close flag.
PEER_OBSERVE_EVENT
// SOURCE_PRESIGN_EVENT is emitted by oc-discovery on the resource-owner peer
// when it receives a PB_SOURCE_PRESIGN request routed via libp2p.
// oc-datacenter listens to it to generate a pre-signed Minio URL and reply
// via PB_CONSIDERS (Phase 4 — isReachable=false).
SOURCE_PRESIGN_EVENT
)
func (n NATSMethod) String() string {
@@ -70,8 +95,10 @@ func (n NATSMethod) String() string {
func NameToMethod(name string) NATSMethod {
for _, v := range [...]NATSMethod{REMOVE_EXECUTION, CREATE_EXECUTION, PLANNER_EXECUTION, DISCOVERY, WORKFLOW_EVENT, ARGO_KUBE_EVENT,
CREATE_RESOURCE, REMOVE_RESOURCE, PROPALGATION_EVENT, SEARCH_EVENT, CONFIRM_EVENT,
CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT,
WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT} {
CONSIDERS_EVENT, ADMIRALTY_CONFIG_EVENT, MINIO_CONFIG_EVENT, PVC_CONFIG_EVENT,
WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT,
PEER_BEHAVIOR_EVENT, PEER_OBSERVE_RESPONSE_EVENT, PEER_OBSERVE_EVENT,
SOURCE_PRESIGN_EVENT} {
if strings.Contains(strings.ToLower(v.String()), strings.ToLower(name)) {
return v
}
+49
View File
@@ -0,0 +1,49 @@
package tools
import "time"
// BehaviorSeverity qualifies the gravity of a peer misbehavior.
type BehaviorSeverity int
const (
// BehaviorWarn: minor inconsistency — slight trust penalty.
BehaviorWarn BehaviorSeverity = iota
// BehaviorFraud: deliberate data manipulation (e.g. fake peerless Ref,
// invalid booking) — significant trust penalty.
BehaviorFraud
// BehaviorCritical: severe abuse (secret exfiltration, data corruption,
// system-level attack) — heavy penalty, near-immediate blacklist.
BehaviorCritical
)
// scorePenalties maps each severity to a trust-score deduction (out of 100).
var scorePenalties = map[BehaviorSeverity]float64{
BehaviorWarn: 5,
BehaviorFraud: 20,
BehaviorCritical: 40,
}
// Penalty returns the trust-score deduction for this severity.
func (s BehaviorSeverity) Penalty() float64 {
if p, ok := scorePenalties[s]; ok {
return p
}
return 5
}
// PeerBehaviorReport is the payload carried by PEER_BEHAVIOR_EVENT.
// Any trusted service can emit it; oc-discovery is the sole consumer.
type PeerBehaviorReport struct {
// ReporterApp identifies the emitting service (e.g. "oc-scheduler", "oc-datacenter").
ReporterApp string `json:"reporter_app"`
// TargetPeerID is the MongoDB DID (_id) of the offending peer.
TargetPeerID string `json:"target_peer_id"`
// Severity drives how much the trust score drops.
Severity BehaviorSeverity `json:"severity"`
// Reason is a human-readable description shown in the blacklist warning.
Reason string `json:"reason"`
// Evidence is an optional reference (booking ID, resource Ref, …).
Evidence string `json:"evidence,omitempty"`
// At is the timestamp of the observed misbehavior.
At time.Time `json:"at"`
}
+5 -1
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"strings"
"sync"
)
// HTTP Method Enum defines the different methods that can be used to interact with the HTTP server
@@ -57,7 +58,8 @@ type HTTPCallerITF interface {
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
Mu sync.RWMutex
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
@@ -217,6 +219,8 @@ func (caller *HTTPCaller) CallForm(method string, url string, subpath string,
}
func (caller *HTTPCaller) StoreResp(resp *http.Response) error {
caller.Mu.Lock()
defer caller.Mu.Unlock()
caller.LastResults = make(map[string]interface{})
caller.LastResults["header"] = resp.Header
caller.LastResults["code"] = resp.StatusCode