389 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
mr 5bdd2554a7 Infinite loop debug 2026-03-23 09:03:07 +01:00
mr ea2a98d84a ShouldVerifyAuthdisable on admin request 2026-03-23 08:11:24 +01:00
mr b82b955045 CanUpdate 2026-03-21 15:08:01 +01:00
mr 88d2e52628 Correct 2026-03-20 16:14:07 +01:00
mr 9f861e5b8d Set up 2026-03-20 15:41:33 +01:00
mr e4506f3b42 longest trace 2026-03-20 15:21:48 +01:00
mr 75d08aae7c time longest 2026-03-20 15:09:52 +01:00
mr b288085f32 if 100% kick 2026-03-20 14:57:01 +01:00
mr bd3e81be0c CHECK log 2026-03-20 14:51:08 +01:00
mr fafa1186c2 out * 1 hour 2026-03-20 14:42:48 +01:00
mr 471eaff94c missing instanceID 2026-03-20 14:38:52 +01:00
mr c9fcabac6e debug time 2026-03-20 14:32:46 +01:00
mr 478e68e6d4 Workout Time Scheduling 2026-03-20 14:20:26 +01:00
mr 5619010838 correct time loc 2026-03-20 14:01:14 +01:00
mr f1a9214ac7 Check trigger strange 2026-03-20 13:41:12 +01:00
mr e6eb516f39 ensurePricing 2026-03-20 13:28:35 +01:00
mr 1508cc3611 PricedItem evolved 2026-03-20 13:07:06 +01:00
mr 2abc035ec0 planner trace 2026-03-20 12:09:28 +01:00
mr c34b8c6703 correction planner 2026-03-20 11:33:59 +01:00
mr a62fbc6c7a Workflow lifecycle events + resource instance duration tracking
- Add WorkflowLifecycleEvent + StepMetric to tools/workflow_lifecycle.go
- Add WORKFLOW_STARTED_EVENT, WORKFLOW_STEP_DONE_EVENT, WORKFLOW_DONE_EVENT NATS methods
- ResourceInstance.UpdateAverageDuration for AverageDurationS running average
- Support Steps recap in WORKFLOW_DONE_EVENT for catch-up by oc-scheduler/oc-catalog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:30:30 +01:00
mr 6e28dce02c provisionning 2026-03-19 15:52:55 +01:00
mr fe3b185b60 err trace 2026-03-19 12:05:33 +01:00
mr 6641d38d9d DBAbstract 2026-03-19 11:32:51 +01:00
mr 93ad8db9a8 decoded CA 2026-03-19 11:17:14 +01:00
mr 4eb53917b8 Log 2026-03-19 10:50:00 +01:00
mr c7884f5cde NewKubernetesService decoded 2026-03-19 09:05:42 +01:00
mr 5fca0480af suppress check error on get 2026-03-19 08:44:25 +01:00
mr 28b5b7d39f Provisionning Ns + TearDown Ns 2026-03-19 08:18:18 +01:00
mr 5b7edb53a9 OcLib 2026-03-19 07:56:47 +01:00
mr 5976795d44 New Channel to Clarify Movement 2026-03-18 15:38:22 +01:00
mr 3d22ff40fb PB -> ADMIRALTY + MINIO 2026-03-18 14:58:21 +01:00
mr 889656a95e argo kube event remains 2026-03-18 14:52:07 +01:00
mr c66fbc809e argo event 2026-03-18 14:46:35 +01:00
mr 1a37a1b4aa loki adjust 2026-03-18 10:28:31 +01:00
mr d4ac398cdb plantuml debug 2026-03-18 09:41:09 +01:00
mr 4eb112bee3 Debug 2026-03-18 09:17:22 +01:00
mr d1214fe622 adjust Export 2026-03-18 09:10:58 +01:00
mr 6a907236fa export 2026-03-18 08:40:39 +01:00
mr 85314baac3 PlantUML doc & Human Readable commentary 2026-03-18 08:30:02 +01:00
mr cec8033ddc by pass restriction 2026-03-17 16:46:40 +01:00
mr d0645f5ca7 publishing is only allowed is it can be monitored and be accessible temp disable 2026-03-17 16:42:03 +01:00
mr c39bc52312 setup draft as live 2026-03-17 16:35:35 +01:00
mr 0a87343e3e Copy 2026-03-17 16:24:42 +01:00
mr 96beaade24 access 2026-03-17 16:15:12 +01:00
mr 5753450965 oclib setup 2026-03-17 16:09:39 +01:00
mr 7f8d697e4c \n replaceAll 2026-03-17 15:49:27 +01:00
mr 94837f8d24 kicks out Required not Required 2026-03-17 15:31:25 +01:00
mr e758144b46 forgot 2026-03-17 15:25:54 +01:00
mr 72be3118b7 NATSMethod 2026-03-17 14:59:27 +01:00
mr 67778e1e47 err 2026-03-17 14:54:13 +01:00
mr 562dfb18c1 graphItem 2026-03-17 14:37:06 +01:00
mr 2a2dd96870 graphVarName 2026-03-17 14:32:39 +01:00
mr 333476e2c5 Setup 2026-03-17 14:26:11 +01:00
mr 0fd2513278 Setup 2026-03-17 14:17:49 +01:00
mr e79101f58d oc-lib 2026-03-17 14:03:19 +01:00
mr b3dbc7687e setup 2026-03-17 13:52:43 +01:00
mr 8fd4f5faef items 2026-03-17 13:36:36 +01:00
mr f7012e285f setup 2026-03-17 13:29:54 +01:00
mr 088b45b2cf Set up 2026-03-17 13:19:51 +01:00
mr 1ac735cef1 Stop rebuild id 2026-03-17 10:04:40 +01:00
mr 65237f0d1f implement 2026-03-17 09:32:02 +01:00
mr 9b2f945176 forgot about sessionID use ExecutionsID 2026-03-17 09:01:47 +01:00
mr b110cbc260 error on usage start 2026-03-16 15:59:19 +01:00
mr a4d81cbb67 sec 2026-03-16 13:16:50 +01:00
mr 9bf2c566e9 test 2026-03-16 12:48:21 +01:00
mr 6d8efd137a After 2026-03-16 12:32:39 +01:00
mr 40a986af41 order per session 2026-03-16 11:47:51 +01:00
mr 4a076ba237 SchedulingSessionID 2026-03-16 11:45:58 +01:00
mr deb819c5af After check 2026-03-16 11:41:05 +01:00
mr 55a039bd66 follow date 2026-03-16 11:40:00 +01:00
mr a86e78841b base draft 2026-03-16 10:59:31 +01:00
mr 48f034316b booking strange state 2026-03-16 10:49:39 +01:00
mr 9e5102893f not found del 2026-03-16 09:28:48 +01:00
mr 465b91fd6e Draft booking flow 2026-03-16 08:52:31 +01:00
mr 12ba346427 bookingstate 2026-03-13 14:32:05 +01:00
mr 2cdf15d722 default to subscription 2026-03-13 14:25:04 +01:00
mr aeebd8b5b2 pricing strategy defaul is subscription 2026-03-13 14:03:18 +01:00
mr e355af2bac pricing.PERMANENT 2026-03-13 13:55:40 +01:00
mr a335c905b3 oclib PB_CLOSE_SEARCH 2026-03-12 15:11:50 +01:00
mr a30173921f restricted update 2026-03-12 11:56:33 +01:00
mr e28b79ac0d missing payload 2026-03-12 11:45:24 +01:00
mr 9645e71b54 Acces execution verification for manual verif 2026-03-12 11:40:17 +01:00
mr 9f514a133e add verification flow 2026-03-12 11:37:45 +01:00
mr f5e1991324 add input + payload 2026-03-12 09:33:10 +01:00
mr d7a8f2adaa Interface{} 2026-03-12 09:25:44 +01:00
mr 7d9addf760 enrich workflow event manual validation 2026-03-12 09:19:10 +01:00
mr 2c9c42dd51 Add Groups 2026-03-12 08:36:34 +01:00
mr 97bfb0582a peer not found 2026-03-11 09:40:29 +01:00
mr 933b7147e9 execution pllaner 2026-03-11 08:25:18 +01:00
mr e03a0d3dd0 OR 2026-03-04 15:57:47 +01:00
mr 340f2a6301 OR missing 2026-03-04 15:39:17 +01:00
mr a426bdf655 isDraft 2026-03-04 13:54:43 +01:00
mr 2bfcfb5736 New 2026-03-04 13:51:43 +01:00
mr 5d18512f67 models 2026-03-04 13:43:14 +01:00
mr 66ee4156e2 is_draft 2026-03-04 13:37:27 +01:00
mr f1eaf497aa resource as draft for update 2026-03-04 13:31:05 +01:00
mr b47b51126a by pass temp 2026-03-04 13:15:01 +01:00
mr 473dc62660 decode 2026-03-04 13:02:53 +01:00
mr 334de8ca2e err = res_mongo.Decode(&data); err != nil 2026-03-04 12:43:47 +01:00
mr ae7e297622 loadone catch error 2026-03-04 12:40:06 +01:00
mr 3e0f369850 prospect 2026-03-04 12:34:11 +01:00
mr 6217618e6c a.Type 2026-03-04 12:22:54 +01:00
mr f033182382 Apply 2026-03-04 12:18:13 +01:00
mr 542b0b73ab getmyself 2026-03-02 16:24:14 +01:00
mr 44812309db Update try 2026-03-02 15:46:05 +01:00
mr cb3771c17a reverse VARs 2026-02-26 10:12:17 +01:00
mr f4e2d8057d crypto 2026-02-26 09:57:54 +01:00
mr 959fce48ef try -> crypto adjust 2026-02-26 09:48:51 +01:00
mr ce8ef70516 offical pb for remote config 2026-02-24 14:08:21 +01:00
mr d18b031a29 WorkflowResource 2026-02-24 13:29:00 +01:00
mr 0f6aa1fe78 ARGO_KUBE_EVENT 2026-02-24 13:00:19 +01:00
mr a9ebad78f3 kube 2026-02-24 10:36:10 +01:00
mr 54aef164ba kubernetes lib 2026-02-24 10:29:28 +01:00
mr ff830065ec set up 2026-02-23 17:26:37 +01:00
mr e039fa56b6 execution_id 2026-02-23 15:56:40 +01:00
mr e10bb55455 p.SetID(uuid.NewString()) 2026-02-23 15:50:10 +01:00
mr f28e2c3620 State in WF 2026-02-23 15:41:48 +01:00
mr b08bbf51dd priced.GetInstanceID() 2026-02-23 15:22:48 +01:00
mr 5d32b4646a Add InstanceID 2026-02-23 15:18:27 +01:00
mr 25e4e67111 missing Instance hit per Purchase +Booking 2026-02-23 15:08:27 +01:00
mr 19b0f10e71 adjust self 2026-02-23 14:07:39 +01:00
mr 12c506e9a7 Native Schedule 2026-02-23 10:23:55 +01:00
mr 2871353635 close planner 2026-02-23 09:30:03 +01:00
mr 59923ac5c1 Missing action 2026-02-23 09:25:38 +01:00
mr da8b8ec397 Planner 2026-02-23 09:20:00 +01:00
mr 9afbbb5c82 Planner Improve 2026-02-23 08:38:43 +01:00
mr 9662ac6d67 Add New 2026-02-19 09:43:44 +01:00
mr 0b41e2505e Nats Native Behaviors + Peer is Stateless 2026-02-18 14:25:56 +01:00
mr fa5c3a3c60 Adjust + Test 2026-02-18 12:24:19 +01:00
mr 842e09f22f by pass pricing profile need 2026-02-17 10:02:44 +01:00
mr 403913d8cf new oclib match 2026-02-12 13:39:52 +01:00
mr 7e8546bbea New dynamic URL match 2026-02-12 13:20:06 +01:00
mr 1895b7ac8a prospect 2026-02-10 09:37:39 +01:00
mr 3bcf0da56a inspect oc-auth 2026-02-10 09:12:02 +01:00
mr b9c9b66780 Sign Resource 2026-02-09 12:37:03 +01:00
mr b767afb301 signature to byte 2026-02-09 10:55:36 +01:00
mr bafeee0d05 GetSignature() string 2026-02-09 10:50:10 +01:00
mr c2aa2fedaa Verify Resource integrity base struct 2026-02-09 10:03:40 +01:00
mr b9e7ce20b6 store 2026-02-05 15:30:23 +01:00
mr 543d28e5d2 relation duplicate key 2026-02-05 15:17:47 +01:00
mr 04d25c7ef1 as _id 2026-02-05 15:14:35 +01:00
mr c8e98bea61 id as _id 2026-02-05 15:11:03 +01:00
mr 2cf960e4f7 mongo adjust 2026-02-05 15:06:45 +01:00
mr e32ba1e37a test 2026-02-05 14:54:41 +01:00
mr 5798e16263 StoreOne 2026-02-05 14:25:26 +01:00
mr 342451db25 publishing log 2026-02-05 14:16:30 +01:00
mr 425cd2a9ba NATS_SERVER 2026-02-05 14:10:48 +01:00
mr 9e30133628 beego.Run() 2026-02-05 14:04:17 +01:00
mr d9f646aac2 security inject appName 2026-02-04 09:38:45 +01:00
mr ef916fe2d9 InitAPI 2026-02-03 16:05:31 +01:00
mr 4258f6b580 missing import 2026-02-03 16:01:23 +01:00
mr 06ad584b6a missing import 2026-02-03 16:01:15 +01:00
mr c650f08b8e add setup 2026-02-03 15:57:12 +01:00
mr c90b55f312 Beego set up 2026-02-03 15:56:39 +01:00
mr 4f28b9b589 loop on discovery 2026-02-03 09:37:53 +01:00
mr 30e6c9a618 bug on enums 2026-02-03 08:44:47 +01:00
mr 186ba3e689 address 2026-01-29 13:20:33 +01:00
mr c1519f6b26 update peer model 2026-01-29 13:12:15 +01:00
mr 97cf629e27 moove user from propalgation to Nats Response 2026-01-28 17:27:02 +01:00
mr c0d89ea9e1 utils to transform byte to resource 2026-01-28 17:04:40 +01:00
mr 7ccd7fe16b CATALOG_SEARCH_EVENT 2026-01-28 17:00:59 +01:00
mr 886f9d15ba include datatype to propalgationMessage 2026-01-28 16:51:27 +01:00
mr d26789d64e Propalgation Mess 2026-01-28 16:44:47 +01:00
mr 7911cf29de Update NATS channel allowed 2026-01-28 16:29:19 +01:00
mr 743f4a6ff7 share pubsub action 2026-01-28 16:22:42 +01:00
mr d2c5d20318 Lightest AdminRequest 2026-01-28 15:52:48 +01:00
mr e3fe49c239 Structured NATS 2026-01-28 15:49:21 +01:00
mr 1c9d7b63c0 NATSResponse 2026-01-28 15:08:07 +01:00
mr d098d253d8 NATSMethod 2026-01-28 15:06:32 +01:00
mr ecb734187a NATSResponse 2026-01-28 15:05:48 +01:00
mr 3c052bf165 Peer Accessor modify 2026-01-27 15:37:28 +01:00
mr 9af8d15672 DISCOVERY 2026-01-27 12:49:51 +01:00
mr 4d57767005 better implem obj hierarchy 2026-01-27 11:14:28 +01:00
mr 9b4f9e420a Mongo Improvment for filtering 2026-01-27 10:59:38 +01:00
mr d772a703da adjust useless line 2026-01-27 09:50:07 +01:00
mr c69069449f chan name fail 2026-01-27 09:37:51 +01:00
mr 643beacd4b Configuration NATS 2026-01-27 09:35:47 +01:00
mr 802786daa7 Peer Manipulation 2026-01-26 16:29:09 +01:00
mr b35d4e2b36 url 2026-01-26 14:34:48 +01:00
mr 1f93493965 relation 2026-01-26 14:32:56 +01:00
mr 055e6c70cd filter peer 2026-01-26 13:00:55 +01:00
mr 85a8857938 oclib -> Generate Peer ID 2026-01-26 12:34:04 +01:00
mr bc94f2b188 Adjust Mongo 2026-01-26 10:36:15 +01:00
mr 6b12aa1713 silence InitNative 2026-01-23 10:03:29 +01:00
mr f3d7c65b18 partner 2026-01-23 07:51:15 +01:00
mr d06c9e9337 GetRelationPath 2026-01-23 07:48:04 +01:00
mr 0308b4ea10 add func 2026-01-23 07:45:36 +01:00
mr b71b1e741d adjust peer element for partnership 2026-01-23 07:38:20 +01:00
mr 8f5f3e331d PENDING status 2026-01-22 16:11:54 +01:00
mr 2d1d76767c keep only verify 2026-01-22 15:58:38 +01:00
mr 00bcca379f Peer Evolve 2026-01-22 15:55:27 +01:00
mr b987286759 NATS URL 2026-01-22 14:18:02 +01:00
mr c72954d2f7 no needed group 2026-01-22 13:58:21 +01:00
mr bcfd43e140 group.Group 2026-01-22 13:45:19 +01:00
mr a4512e4da6 add Groups 2026-01-22 13:38:55 +01:00
mr 1c3b9218f7 correction on bookig flow 2026-01-15 13:27:57 +01:00
mr 7c5d5c491f Better peer filtering 2026-01-15 12:26:56 +01:00
mr 76eb167c5b peer improvment 2026-01-15 12:15:04 +01:00
mr fa5b754333 add Selected in Workflow Exec 2026-01-14 13:57:49 +01:00
mr 0e378dc19c add Selected in Workflow Exec 2026-01-14 13:55:32 +01:00
mr 5cdfc28d2f delete scheduler 2026-01-13 16:53:25 +01:00
mr 6d745fe922 add event base intelligency 2026-01-13 16:04:31 +01:00
mr c35b06e0bc add NATS EVENT 2026-01-12 15:40:37 +01:00
mr be770ec763 multipart file over os.file 2026-01-12 14:26:29 +01:00
mr 27f295f17e Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib
Merged Main OCLIB
2026-01-12 12:00:32 +01:00
mr 33b8d2799a update import plantUML 2026-01-12 11:59:05 +01:00
root 188b758f7a Ajouter .gitattributes 2025-11-01 16:39:25 +01:00
pb f4b0cf5683 changed the value used as the key in the related map, using the 'node id' was erasing some relations when a a processing is linked to two storages 2025-08-08 16:15:53 +02:00
pb e7a71188a3 changed the value used as the key in the related map, using the 'node id' was erasing some relations when a a processing is linked to two storages 2025-08-08 16:05:36 +02:00
pb 40a61387b9 added the UUID of the peer when an error appears 2025-08-05 13:39:21 +02:00
pb cc939451fd added the UUID of the peer when an error appears 2025-08-05 13:25:47 +02:00
pb 76e9b2562e added the enum to reach the /minio/secet route 2025-08-05 11:56:27 +02:00
pb cc3091d401 added the function to load one ressource for each ressource type 2025-07-31 15:53:05 +02:00
pb 3ddbf1a967 added comments 2025-07-30 18:28:19 +02:00
pb be2a1cc114 corrction of the non initialized map 2025-07-30 18:21:09 +02:00
pb a093369dc5 corrction of the non initialized map 2025-07-30 18:15:55 +02:00
pb 76d83878eb added a method to workflow which allows to retrieve items by resource type and resource id 2025-07-30 18:01:23 +02:00
pb e735f78e58 added a new method to create kubernetes names with the same naming 2025-07-15 14:58:19 +02:00
pb 98a2359c9d added a tweak to PeerItemOrder GetPrice in order to not crash on nil Purchase 2025-07-10 11:47:54 +02:00
pb 83e590d4e1 added error handling 2025-07-09 17:42:37 +02:00
pb 4e3ff9aa08 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib 2025-07-09 16:54:37 +02:00
pb 424d523c5e added the bson tag for DestPeerId which was causing error when deserializing from mongo results 2025-07-09 16:50:41 +02:00
mr 346275e12c test 2025-07-08 13:59:55 +02:00
mr 6ab774cc43 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main
test
2025-07-08 13:42:28 +02:00
mr 2748b59221 test booking 2025-07-08 13:42:13 +02:00
pb 34f01e9740 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib 2025-07-08 12:02:34 +02:00
pb dcdc6ff1d9 added more info in GenericStoreOne error generated by validate() 2025-07-08 12:02:18 +02:00
pb 365b924e4b Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib 2025-07-07 16:30:58 +02:00
pb e7e56d1859 commented the PricingProfile check for IsBooked 2025-07-07 16:30:29 +02:00
mr 443546027b Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main 2025-07-04 10:44:59 +02:00
mr 1c4f3f756f test 2025-07-04 10:44:14 +02:00
pb 3971d5ca5d added the value to reach the minio route on datacenter 2025-06-30 14:06:03 +02:00
pb e95d1aa53b implemented StoreOne for orders 2025-06-27 17:33:39 +02:00
pb 1ab2bd2153 commented the condition that prevents the booking if pricing is not selected to allow dev while the feature is not implemented 2025-06-27 17:10:32 +02:00
pb d35ad440fa removed the 'validate:required' from PricedItem in Booking because we don't have implemented the error handling nor the feature yet 2025-06-27 16:21:54 +02:00
mr d58dc56024 corrected live accessor 2025-06-26 16:20:41 +02:00
mr 34b7cdcf06 corrected live accessor 2025-06-26 15:59:21 +02:00
mr af0d7807bc corrected live accessor 2025-06-26 15:57:49 +02:00
mr e600fedcab access to live storage 2025-06-24 12:22:27 +02:00
mr 147c7bc3a1 access to live storage 2025-06-24 11:58:52 +02:00
mr 3fdf5c3ebf Live Structure 2025-06-24 11:32:07 +02:00
mr cd177bd779 Live Structure 2025-06-24 11:29:04 +02:00
mr 2c8dcbe93d wait for NATS 2025-06-24 08:49:53 +02:00
mr e84d262f38 add sets up 2025-06-20 12:11:13 +02:00
mr 29b192211d add sets up 2025-06-20 12:10:36 +02:00
mr 583ca2fbac add sets up 2025-06-20 10:50:01 +02:00
mr 82d25b0bee add sets up 2025-06-20 10:48:08 +02:00
mr 181b3249b8 add sets up 2025-06-20 10:47:33 +02:00
mr 8b38249df7 add sets up 2025-06-20 09:27:55 +02:00
mr 01af8237db add sets up 2025-06-20 09:22:09 +02:00
mr 2f4884c655 add sets up 2025-06-20 09:07:53 +02:00
mr c9ee2a1d24 add sets up 2025-06-20 09:05:44 +02:00
mr 8d5ba6a5e4 add sets up 2025-06-20 08:34:33 +02:00
mr d3cfe019e3 add sets up 2025-06-20 08:10:52 +02:00
mr 4c2ecd3f41 add sets up 2025-06-20 07:53:32 +02:00
mr d8ccdec501 add sets up 2025-06-20 07:51:32 +02:00
mr 938f9f1326 set up 2025-06-19 08:11:11 +02:00
mr 29bc21735d setup 2025-06-18 07:58:40 +02:00
mr ec7a7e4746 draft of compute units catalog 2025-06-17 16:42:21 +02:00
mr 0b0952b28c draft of compute units catalog 2025-06-17 16:14:44 +02:00
mr 9e52663261 draft of compute units catalog 2025-06-17 15:35:02 +02:00
mr 8f2adb76e4 add draft compute units 2025-06-17 15:06:33 +02:00
mr 0d6c329477 draft of compute units catalog 2025-06-17 14:57:36 +02:00
mr 1c751f7253 add draft compute units 2025-06-17 14:51:41 +02:00
mr 2264d22c69 draft of compute units catalog 2025-06-17 14:21:37 +02:00
mr 9fe72ea96e draft tests 2025-06-16 13:48:32 +02:00
mr 48299810e0 draft test 2025-06-16 11:24:39 +02:00
mr 2a0ab8e549 access to workflow IsDeps func 2025-06-12 10:47:38 +02:00
mr 23a9d648d2 Merge branch 'main' of https://cloud.o-forge.io/core/oc-lib into main
merge oclib
2025-06-12 10:42:38 +02:00
mr a3029fa3f9 refactor addition in oclib for better use 2025-06-12 10:42:05 +02:00
pb 387785b40c reimported logs without import cycle 2025-06-04 10:33:00 +02:00
pb 03dea55131 added another error log when the status is dead 2025-06-03 10:00:47 +02:00
pb 7b8aa989f6 added a log when an API is not reachable 2025-06-02 18:22:08 +02:00
pb 6ab6383144 corrected an use of the original http caller instead of the deep copy 2025-05-27 18:07:00 +02:00
pb 690d60f9d6 corrected the getBooking function parameters 2025-05-27 16:13:46 +02:00
pb da0de80afd Booking check and booking post have been transformed in goroutine to improve performance when booking several execution with cron expressions 2025-05-27 15:38:24 +02:00
pb cd7ae788b1 didn't put the blocking loop in the right place for post booking 2025-05-27 12:06:10 +02:00
pb 0d96cc53bf transformed the loop that posted the booking on oc-datacenter to a threaded operation where each call is done in a goroutine 2025-05-27 11:58:55 +02:00
pb 66fc3c5b35 added the passing of the request.Caller's URL to the deep copy 2025-05-27 11:34:44 +02:00
pb 5ab3eb8a38 forgot to pass the mutex as pointer and unlock it 2025-05-27 11:17:10 +02:00
pb fec23b4acd modified HTTP caller to have a DeepCopy() method in order to parallelize calls without race conditions 2025-05-27 11:08:35 +02:00
pb 901622fee0 logging on the booking uuid before the post booking 2025-05-27 09:51:41 +02:00
pb 527e622774 correct the error channel 2025-05-26 19:21:28 +02:00
pb 7223b79fe8 correct the error channel 2025-05-26 19:16:39 +02:00
pb 1ade41aeae moved the code that execute the booking into a separated function so that it can be launched as goroutine and parallelize get booking$ 2025-05-26 19:05:17 +02:00
pb 58dc579255 added debug logging 2025-05-26 18:30:56 +02:00
pb 370dac201b In CheckBooking mooved the loop on bookings outside of the loop of execs, which seems to repeat the Peer execution on booking an exponential number of time 2025-05-26 17:55:45 +02:00
pb 2a763006db counting round in exec 2025-05-26 17:41:26 +02:00
pb 522c66653b Added logging for debug 2025-05-26 17:22:09 +02:00
pb b57f050b81 increased the limit of returns by Mongo find() 2025-05-22 14:41:38 +02:00
122 changed files with 12649 additions and 2063 deletions
+3
View File
@@ -0,0 +1,3 @@
# Force Go as the main language
*.go linguist-detectable=true
* linguist-language=Go
+190
View File
@@ -0,0 +1,190 @@
# Rapport d'audit Éléments inutilisés et problèmes identifiés
> Généré le 2026-02-18 branche `feature/event`
---
## 1. Bugs critiques corrigés dans cette session
| Fichier | Ligne | Description | Statut |
|---------|-------|-------------|--------|
| `entrypoint.go` | 652, 664, 676, 688 | `fmt.Errorf(res.Err)` format string non-constant (erreur de build) | Corrigé |
| `models/utils/abstracts.go` | 136 | `VerifyAuth` déréférençait `request.Admin` avant de vérifier `request != nil` | Corrigé |
| `models/utils/abstracts.go` | 68-78 | `DeepCopy()` faisait `Unmarshal` dans un pointeur nil retournait toujours `nil` | Corrigé |
| `models/resources/resource.go` | 176 | `instances = append(instances)` argument manquant, l'instance n'était jamais ajoutée | Corrigé |
| `models/resources/priced_resource.go` | 63-69 | Code mort après `return true` dans `IsBooked()` | Corrigé |
| `tools/remote_caller.go` | 118 | `CallDelete` vérifiait `req.Body == nil` (toujours vrai pour DELETE), court-circuitant la lecture de la réponse | Corrigé |
---
## 2. Debug prints à supprimer (fmt.Println en production)
Ces appels `fmt.Println` polluent stdout et peuvent exposer des informations sensibles.
| Fichier | Lignes | Contenu |
|---------|--------|---------|
| `models/bill/bill.go` | ~197 | `fmt.Println(err)` |
| `models/collaborative_area/collaborative_area_mongo_accessor.go` | ~95, 109, 118, 123 | Debug sur `res`, `sharedWorkspace.AllowedPeersGroup`, `canFound`, `peerskey` |
| `models/peer/peer_cache.go` | ~44, 55 | URL et `"Launching peer execution on..."` |
| `models/resources/storage.go` | ~196 | `fmt.Println("GetPriceHT", ...)` |
| `models/workflow/workflow.go` | ~158, 164, 170, 176 | 4× `fmt.Println(err)` |
| `tools/nats_caller.go` | ~110, 117, 122, 126 | 4× `fmt.Println()` divers |
| `tools/remote_caller.go` | 227 | `fmt.Println("Error reading the body...")` (devrait utiliser le logger) |
| `dbs/dbs.go` | 47 | `fmt.Println("Recovered. Error:\n", r, debug.Stack())` |
> **Note :** `priced_resource.go` et `data.go` corrigés dans cette session.
---
## 3. Code commenté significatif
### 3.1 Validation de pricing désactivée (workflow)
**Fichier :** `models/workflow/workflow.go` ~lignes 631-634
```go
// Should be commented once the Pricing selection feature has been implemented
// if priced.SelectPricing() == nil {
// return resources, priceds, errors.New("no pricings are selected... can't proceed")
// }
```
Une vérification de sécurité critique est désactivée. Sans elle, des ressources sans pricing peuvent être traitées silencieusement.
### 3.2 PAY_PER_USE stratégie supprimée mais traces restantes
**Fichier :** `models/common/pricing/pricing_strategy.go` lignes 47, 61-63
```go
// PAY_PER_USE // per request. ( unpredictible )
/*case PAY_PER_USE:
return bs, true*/
```
La constante `PAY_PER_USE` a été supprimée mais les commentaires laissés créent de la confusion.
### 3.3 Vérification d'autorisation peer désactivée
**Fichier :** `models/resources/resource.go` lignes 98-104
```go
/*if ok, _ := utils.IsMySelf(request.PeerID, ...); ok {*/
profile = pricing.GetDefaultPricingProfile()
/*} else {
return nil, errors.New("no pricing profile found")
}*/
```
Le profil par défaut est retourné sans vérifier si le pair est bien `myself`. Sécurité à revoir.
---
## 4. Logique erronée non corrigée (à traiter)
### 4.1 IsTimeStrategy logique inversée
**Fichier :** `models/common/pricing/pricing_strategy.go` ligne 88
```go
func IsTimeStrategy(i int) bool {
return len(TimePricingStrategyList()) < i // BUG: devrait être ">"
}
```
La condition est inversée. Retourne `true` pour des valeurs hors de la liste. Fonction actuellement non utilisée (voir §5).
### 4.2 IsBillingStrategyAllowed case SUBSCRIPTION sans retour
**Fichier :** `models/common/pricing/pricing_strategy.go` lignes 54-65
```go
case SUBSCRIPTION:
/*case PAY_PER_USE:
return bs, true*/
// Aucun return ici → tombe dans le default
```
Le cas `SUBSCRIPTION` ne retourne rien explicitement, ce qui est trompeur.
---
## 5. Éléments inutilisés
### 5.1 Fonction jamais appelée
| Symbole | Fichier | Ligne |
|---------|---------|-------|
| `IsTimeStrategy(i int) bool` | `models/common/pricing/pricing_strategy.go` | 88 |
De plus, cette fonction a une logique erronée (voir §4.1).
### 5.2 Variable singleton inutilisée
| Symbole | Fichier | Ligne |
|---------|---------|-------|
| `HTTPCallerInstance` | `tools/remote_caller.go` | 57 |
Déclarée comme singleton mais jamais utilisée de nouvelles instances sont créées via `NewHTTPCaller()`.
---
## 6. Tests supprimés (couverture perdue)
Les fichiers suivants ont été supprimés sur la branche `feature/event` et la couverture correspondante n'est plus assurée :
| Fichier supprimé | Modèles non couverts |
|------------------|----------------------|
| `models/peer/tests/peer_cache_test.go` | `PeerCache` logique d'exécution distribuée |
| `models/peer/tests/peer_test.go` | `Peer` modèle et accesseur |
| `models/utils/tests/abstracts_test.go` | `AbstractObject` méthodes de base |
| `models/utils/tests/common_test.go` | `GenericStoreOne`, `GenericDeleteOne`, etc. |
| `models/workflow_execution/tests/workflow_test.go` | `WorkflowExecution` modèle et accesseur |
> `models/order/tests/order_test.go` existe mais ne contient **aucune fonction de test**.
---
## 7. Fautes d'orthographe dans les identifiants publics
Ces typos sont dans des noms exportés (API publique) les corriger est un **breaking change**.
### 7.1 `Instanciated` `Instantiated`
Apparaît 50+ fois dans les types exportés centraux :
- `AbstractInstanciatedResource[T]` (resource.go, compute.go, data.go, storage.go, processing.go, workflow.go)
- `AbstractInstanciatedResource.Instances`
- Tests : `resources.AbstractInstanciatedResource[*MockInstance]{...}`
### 7.2 `ressource` `resource` (dans les messages d'erreur)
**Fichier :** `entrypoint.go` messages dans `LoadOneStorage`, `LoadOneComputing`, etc.
```go
"Error while loading storage ressource " + storageId // "ressource" est du français
```
### 7.3 `GARANTED` `GUARANTEED`
**Fichiers :** `models/common/pricing/pricing_profile.go`, `models/resources/storage.go`
```go
GARANTED_ON_DELAY // pricing_profile.go:72
GARANTED // pricing_profile.go:73
GARANTED_ON_DELAY_STORAGE // storage.go:106
GARANTED_STORAGE // storage.go:107
```
### 7.4 `CREATE_EXECTUTION` `CREATE_EXECUTION`
**Fichier :** `tools/nats_caller.go` ligne 34
```go
CREATE_EXECTUTION // faute de frappe dans la constante enum
```
### 7.5 `PROPALGATION` `PROPAGATION`
**Fichier :** `tools/nats_caller.go` lignes 29, 45, 56
```go
"propalgation event" // et PROPALGATION_EVENT
```
---
## 8. Incohérences de nommage mineures
| Fichier | Problème |
|---------|----------|
| `models/resources/interfaces.go:19` | Paramètre `instance_id` en snake_case dans une signature Go (devrait être `instanceID`) |
| `entrypoint.go:505` | Message de panique dans `CopyOne` dit `"Panic recovered in UpdateOne"` |
| `tools/remote_caller.go:110` | Commentaire `// CallPut calls the DELETE method` (copie-colle incorrect) |
---
## 9. Résumé
| Catégorie | Nombre | Priorité |
|-----------|--------|----------|
| Bugs critiques corrigés | 6 | Fait |
| Debug `fmt.Println` restants | 15+ | 🔴 Haute |
| Code commenté important | 3 | 🟠 Moyenne |
| Logique erronée (non corrigée) | 2 | 🟠 Moyenne |
| Éléments inutilisés | 2 | 🟡 Faible |
| Tests supprimés (couverture perdue) | 5 fichiers | 🟠 Moyenne |
| Typos dans API publique | 5 types | 🟡 Faible (breaking change) |
| Incohérences mineures | 3 | 🟢 Très faible |
+40 -9
View File
@@ -9,14 +9,28 @@ import "sync"
// ===================================================
type Config struct {
NATSUrl string
MongoUrl string
MongoDatabase string
Host string
Port string
LokiUrl string
LogLevel string
Whitelist bool
IsApi bool
IsNano bool
APIPort int
NATSUrl string
MongoUrl string
MongoDatabase string
Host string
Port string
LokiUrl string
LogLevel string
Whitelist bool
PrivateKeyPath string
PublicKeyPath string
InternalCatalogAPI string
InternalSharedAPI string
InternalWorkflowAPI string
InternalWorkspaceAPI string
InternalPeerAPI string
InternalDatacenterAPI string
InternalSchedulerAPI string
}
func (c Config) GetUrl() string {
@@ -37,12 +51,29 @@ func GetConfig() *Config {
return instance
}
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *Config {
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
GetConfig().LokiUrl = lokiUrl
GetConfig().LogLevel = logLevel
GetConfig().Whitelist = true
GetConfig().APIPort = port
GetConfig().PrivateKeyPath = pkPath
GetConfig().PublicKeyPath = ppPath
GetConfig().InternalCatalogAPI = internalCatalogAPI
GetConfig().InternalSharedAPI = internalSharedAPI
GetConfig().InternalWorkflowAPI = internalWorkflowAPI
GetConfig().InternalWorkspaceAPI = internalWorkspaceAPI
GetConfig().InternalPeerAPI = internalPeerAPI
GetConfig().InternalDatacenterAPI = internalDatacenterAPI
GetConfig().InternalSchedulerAPI = internalSchedulerAPI
return GetConfig()
}
+3 -4
View File
@@ -23,12 +23,11 @@ import (
* The configuration loader will give priority to the local file over the default file
*/
func GetConfLoader() *onion.Onion {
func GetConfLoader(appName string) *onion.Onion {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
AppName := GetAppName()
EnvPrefix := "OC_"
defaultConfigFile := "/etc/oc/" + AppName[3:] + ".json"
localConfigFile := "./" + AppName[3:] + ".json"
defaultConfigFile := "/etc/oc/" + appName[3:] + ".json"
localConfigFile := "./" + appName[3:] + ".json"
var configFile string
var o *onion.Onion
l3 := GetEnvVarLayer(EnvPrefix)
+232 -90
View File
@@ -2,6 +2,9 @@ package dbs
import (
"fmt"
"reflect"
"regexp"
"runtime/debug"
"strings"
"go.mongodb.org/mongo-driver/bson"
@@ -19,6 +22,8 @@ const (
GT
EQUAL
NOT
ELEMMATCH
OR
)
var str = [...]string{
@@ -31,113 +36,44 @@ var str = [...]string{
"gt",
"equal",
"not",
"elemMatch",
"or",
}
func (m Operator) String() string {
return str[m]
}
func (m Operator) ToMongoEOperator(k string, value interface{}) bson.E {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
}
}()
defaultValue := bson.E{Key: k, Value: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
switch m {
case LIKE:
return bson.E{Key: k, Value: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
case EXISTS:
return bson.E{Key: k, Value: bson.M{"$exists": ToValueOperator(StringToOperator(m.String()), value)}}
case IN:
return bson.E{Key: k, Value: bson.M{"$in": ToValueOperator(StringToOperator(m.String()), value)}}
case GTE:
return bson.E{Key: k, Value: bson.M{"$gte": ToValueOperator(StringToOperator(m.String()), value)}}
case GT:
return bson.E{Key: k, Value: bson.M{"$gt": ToValueOperator(StringToOperator(m.String()), value)}}
case LTE:
return bson.E{Key: k, Value: bson.M{"$lte": ToValueOperator(StringToOperator(m.String()), value)}}
case LT:
return bson.E{Key: k, Value: bson.M{"$lt": ToValueOperator(StringToOperator(m.String()), value)}}
case EQUAL:
return bson.E{Key: k, Value: value}
case NOT:
v := value.(Filters)
orList := bson.A{}
andList := bson.A{}
f := bson.D{}
for k, filter := range v.Or {
for _, ff := range filter {
orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range v.And {
for _, ff := range filter {
andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
return bson.E{Key: "$not", Value: f}
default:
return defaultValue
}
}
func (m Operator) ToMongoOperator(k string, value interface{}) bson.M {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
fmt.Println("Recovered. Error:\n", r, debug.Stack())
}
}()
defaultValue := bson.M{k: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
switch m {
case LIKE:
return bson.M{k: bson.M{"$regex": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case EXISTS:
return bson.M{k: bson.M{"$exists": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case IN:
return bson.M{k: bson.M{"$in": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case GTE:
return bson.M{k: bson.M{"$gte": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case GT:
return bson.M{k: bson.M{"$gt": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case LTE:
return bson.M{k: bson.M{"$lte": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case LT:
return bson.M{k: bson.M{"$lt": ToValueOperator(StringToOperator(m.String()), value)}}
return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case ELEMMATCH:
return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
case EQUAL:
return bson.M{k: value}
case NOT:
v := value.(Filters)
orList := bson.A{}
andList := bson.A{}
f := bson.D{}
for k, filter := range v.Or {
for _, ff := range filter {
orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range v.And {
for _, ff := range filter {
andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
return bson.M{"$not": f}
return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value, false)}
case OR:
return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value, true)}
default:
return defaultValue
}
@@ -152,13 +88,55 @@ func StringToOperator(s string) Operator {
return LIKE
}
func ToValueOperator(operator Operator, value interface{}) interface{} {
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
value = ""
func GetBson(filters *Filters) bson.D {
f := bson.D{}
orList := bson.A{}
andList := bson.A{}
if filters != nil {
for k, filter := range filters.Or {
for _, ff := range filter {
orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range filters.And {
for _, ff := range filter {
andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{Key: "$or", Value: orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{Key: "$and", Value: andList}}
}
}
if operator == LIKE {
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
return f
}
func (m Operator) ToValueOperator(operator Operator, value interface{}, or bool) interface{} {
switch value := value.(type) {
case *Filters:
bson := GetBson(value)
if or {
for _, b := range bson {
if b.Key == "$or" {
return b.Value
}
}
} else {
return bson
}
default:
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
value = ""
}
if operator == LIKE {
return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
}
}
return value
}
@@ -172,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 {
+19 -32
View File
@@ -247,10 +247,7 @@ func (m *MongoDB) StoreOne(obj interface{}, id string, collection_name string) (
if err := m.createClient(mngoConfig.GetUrl(), false); err != nil {
return "", 503, err
}
var doc map[string]interface{}
b, _ := bson.Marshal(obj)
bson.Unmarshal(b, &doc)
doc["_id"] = id
doc := map[string]interface{}{"_id": id}
targetDBCollection := CollectionMap[collection_name]
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel()
@@ -261,7 +258,7 @@ func (m *MongoDB) StoreOne(obj interface{}, id string, collection_name string) (
return "", 409, err
}
return id, 200, nil
return m.UpdateOne(obj, id, collection_name)
}
func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResult, int, error) {
@@ -270,6 +267,9 @@ func (m *MongoDB) LoadOne(id string, collection_name string) (*mongo.SingleResul
}
filter := bson.M{"_id": id}
targetDBCollection := CollectionMap[collection_name]
if targetDBCollection == nil {
return nil, 503, errors.New("collection " + collection_name + " not initialized")
}
MngoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel()
@@ -282,37 +282,21 @@ 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(100)
targetDBCollection := CollectionMap[collection_name]
orList := bson.A{}
andList := bson.A{}
f := bson.D{}
if filters != nil {
for k, filter := range filters.Or {
for _, ff := range filter {
orList = append(orList, dbs.StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
for k, filter := range filters.And {
for _, ff := range filter {
andList = append(andList, dbs.StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
}
}
if len(orList) > 0 && len(andList) == 0 {
f = bson.D{{"$or", orList}}
} else {
if len(orList) > 0 {
andList = append(andList, bson.M{"$or": orList})
}
f = bson.D{{"$and", andList}}
}
if targetDBCollection == nil {
return nil, 503, errors.New("collection " + collection_name + " not initialized")
}
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(
@@ -347,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
}
@@ -355,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
+2 -2
View File
@@ -194,7 +194,7 @@ AccessPricingProfile ^-- ProcessingResourcePricingProfile
ExploitPricingProfile ^-- ComputeResourcePricingProfile
ExploitPricingProfile ^-- StorageResourcePricingProfile
interface PricingProfileITF {
GetPrice(quantity float64, val float64, start date, end date, request) float64
GetPriceHT(quantity float64, val float64, start date, end date, request) float64
IsPurchased() bool
}
class AccessPricingProfile {
@@ -319,7 +319,7 @@ Workflow "1 " --* "many " ExploitResourceSet
class Workflow {}
interface PricedItemITF {
getPrice(request) float64, error
GetPriceHT(request) float64, error
}
@enduml
+214
View File
@@ -0,0 +1,214 @@
# PlantUML Format de commentaire human-readable
Ce document décrit la syntaxe des commentaires attachés aux ressources et aux liens
dans les fichiers PlantUML importés par OpenCloud.
---
## Syntaxe générale
```plantuml
TypeRessource(varName, "Nom affiché") ' clé: valeur, clé.sous_clé: valeur
```
### Règles de parsing
| Règle | Détail |
|---|---|
| Séparateur de paires | `,` |
| Séparateur clé/valeur | premier `:` de la paire (les URLs `http://...` sont gérées) |
| Sous-objets | notation pointée `access.container.image: nginx` |
| Types | auto-inférés : `bool` > `float64` > `string` |
| Fallback | JSON brut si le commentaire commence par `{` (compatibilité ascendante) |
### Comportement à l'import
Chaque ressource reçoit automatiquement une **instance par défaut**, seedée avec les
attributs de la ressource parente. Le commentaire vient ensuite **surcharger** uniquement
les champs explicitement renseignés.
> **Exception :** `WorkflowEvent` n'a pas d'instance (voir section dédiée).
---
## Ressources disponibles
### `Data(var, "nom")` Données
Ressource de données. Les attributs qualifient le modèle de données **et** son instance
(source d'accès).
| Clé | Type | Description |
|---|---|---|
| `type` | string | Type de données (`raster`, `vector`, `tabular`) |
| `quality` | string | Niveau de qualité |
| `open_data` | bool | Données en accès libre |
| `static` | bool | Données statiques (pas de mise à jour) |
| `personal_data` | bool | Contient des données personnelles |
| `anonymized_personal_data` | bool | Données personnelles anonymisées |
| `size` | float64 | Taille en GB |
| `access_protocol` | string | Protocole d'accès (`http`, `s3`, `ftp`) |
| `country` | string | Code pays ISO (`FR`, `DE`) |
| `location.latitude` | float64 | Latitude géographique |
| `location.longitude` | float64 | Longitude géographique |
| `source` | string | URL / endpoint d'accès à la donnée |
```plantuml
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
```
---
### `Processing(var, "nom")` Traitement
Ressource de traitement (algorithme, conteneur, service). Les attributs qualifient
le modèle de traitement **et** sa configuration d'exécution.
| Clé | Type | Description |
|---|---|---|
| `infrastructure` | int | Infrastructure cible : `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
| `is_service` | bool | Traitement persistant (service long-running) |
| `open_source` | bool | Code source ouvert |
| `license` | string | Licence (`MIT`, `Apache-2.0`, `GPL-3.0`) |
| `maturity` | string | Maturité (`prototype`, `beta`, `production`) |
| `access_protocol` | string | Protocole d'accès |
| `country` | string | Code pays ISO |
| `location.latitude` | float64 | Latitude |
| `location.longitude` | float64 | Longitude |
| `access.container.image` | string | Image du conteneur |
| `access.container.command` | string | Commande de démarrage |
| `access.container.args` | string | Arguments de la commande |
```plantuml
Processing(p1, "NDVI Calc") ' infrastructure: 0, open_source: true, license: MIT, maturity: production, access.container.image: myrepo/ndvi:1.2
```
---
### `Storage(var, "nom")` Stockage
Ressource de stockage. Produit une instance live (`LiveStorage`) à l'import.
| Clé | Type | Description |
|---|---|---|
| `storage_type` | int | Type de stockage (enum) |
| `source` | string | URL / endpoint du stockage |
| `path` | string | Chemin ou bucket dans le stockage |
| `local` | bool | Stockage local |
| `security_level` | string | Niveau de sécurité |
| `size` | float64 | Taille allouée en GB |
| `encryption` | bool | Chiffrement activé |
| `redundancy` | string | Politique de redondance |
| `throughput` | string | Débit cible |
| `access_protocol` | string | Protocole (`s3`, `nfs`, `smb`) |
| `country` | string | Code pays ISO |
| `location.latitude` | float64 | Latitude |
| `location.longitude` | float64 | Longitude |
```plantuml
Storage(s1, "Minio OVH") ' source: http://minio.example.com:9000, path: /bucket/data, access_protocol: s3, encryption: true, size: 500, country: FR
```
---
### `ComputeUnit(var, "nom")` Unité de calcul
Ressource de calcul (datacenter, cluster). Produit une instance live (`LiveDatacenter`)
à l'import.
| Clé | Type | Description |
|---|---|---|
| `architecture` | string | Architecture CPU (`x86_64`, `arm64`) |
| `infrastructure` | int | `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
| `source` | string | URL de l'API du datacenter |
| `security_level` | string | Niveau de sécurité |
| `annual_co2_emissions` | float64 | Émissions CO annuelles (kg) |
| `access_protocol` | string | Protocole d'accès |
| `country` | string | Code pays ISO |
| `location.latitude` | float64 | Latitude |
| `location.longitude` | float64 | Longitude |
```plantuml
ComputeUnit(c1, "Datacenter Rennes") ' source: https://api.dc-rennes.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68, security_level: high
```
---
### `WorkflowEvent(var, "nom")` Événement déclencheur de workflow
Crée directement un `NativeTool` de type `WORKFLOW_EVENT` (Kind = 0).
Représente le point de départ d'un workflow.
> **Pas d'instance. Pas de commentaire.**
> Le nom du `NativeTool` est forcé à `WORKFLOW_EVENT` à l'import.
```plantuml
WorkflowEvent(e1, "Start")
```
---
## Liens
Les commentaires sur les liens qualifient la connexion entre deux ressources
(typiquement entre un traitement et un stockage).
### Syntaxe
```plantuml
source --> destination ' clé: valeur
source <-- destination ' clé: valeur
source -- destination ' clé: valeur (non directionnel)
```
### Attributs disponibles
| Clé | Type | Description |
|---|---|---|
| `storage_link_infos.write` | bool | `true` = écriture, `false` = lecture |
| `storage_link_infos.source` | string | Chemin source dans le lien |
| `storage_link_infos.destination` | string | Chemin destination dans le lien |
| `storage_link_infos.filename` | string | Nom du fichier échangé |
```plantuml
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: output.tif
d1 --> p1
```
---
## Exemple complet
```plantuml
@startuml
!include opencloud.puml
WorkflowEvent(e1, "Start")
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
Processing(p1, "NDVI") ' infrastructure: 0, open_source: true, license: MIT, access.container.image: myrepo/ndvi:1.2
Storage(s1, "Minio résultats") ' source: http://minio.example.com:9000, path: /results, access_protocol: s3, encryption: true, size: 500, country: FR
ComputeUnit(c1, "DC Rennes") ' source: https://api.dc.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68
e1 --> p1
d1 --> p1
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: ndvi.tif
s1 --> c1
@enduml
```
---
## Récapitulatif des types de ressources
| Mot-clé PlantUML | Type Go | Instance | Live | Commentaire |
|---|---|---|---|---|
| `Data` | `DataResource` | `DataInstance` | non | oui |
| `Processing` | `ProcessingResource` | `ProcessingInstance` | non | oui |
| `Storage` | `StorageResource` | `StorageResourceInstance` | oui `LiveStorage` | oui |
| `ComputeUnit` | `ComputeResource` | `ComputeResourceInstance` | oui `LiveDatacenter` | oui |
| `WorkflowEvent` | `NativeTool` (Kind=WORKFLOW_EVENT) | aucune | non | non |
+452 -134
View File
@@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"net/http"
"os"
"slices"
"strings"
"runtime/debug"
@@ -15,11 +17,19 @@ 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"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
@@ -27,7 +37,10 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
"github.com/beego/beego/v2/server/web/filter/cors"
"github.com/google/uuid"
"github.com/goraz/onion"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/rs/zerolog"
)
@@ -37,25 +50,73 @@ type LibDataEnum int
// init accessible constant to retrieve data from the database
const (
INVALID LibDataEnum = iota
DATA_RESOURCE = tools.DATA_RESOURCE
PROCESSING_RESOURCE = tools.PROCESSING_RESOURCE
STORAGE_RESOURCE = tools.STORAGE_RESOURCE
COMPUTE_RESOURCE = tools.COMPUTE_RESOURCE
WORKFLOW_RESOURCE = tools.WORKFLOW_RESOURCE
WORKFLOW = tools.WORKFLOW
WORKSPACE = tools.WORKSPACE
WORKFLOW_EXECUTION = tools.WORKFLOW_EXECUTION
PEER = tools.PEER
COLLABORATIVE_AREA = tools.COLLABORATIVE_AREA
RULE = tools.RULE
BOOKING = tools.BOOKING
ORDER = tools.ORDER
INVALID LibDataEnum = iota
DATA_RESOURCE = tools.DATA_RESOURCE
PROCESSING_RESOURCE = tools.PROCESSING_RESOURCE
STORAGE_RESOURCE = tools.STORAGE_RESOURCE
COMPUTE_RESOURCE = tools.COMPUTE_RESOURCE
WORKFLOW_RESOURCE = tools.WORKFLOW_RESOURCE
WORKFLOW = tools.WORKFLOW
WORKSPACE = tools.WORKSPACE
WORKFLOW_EXECUTION = tools.WORKFLOW_EXECUTION
PEER = tools.PEER
COLLABORATIVE_AREA = tools.COLLABORATIVE_AREA
RULE = tools.RULE
BOOKING = tools.BOOKING
ORDER = tools.ORDER
LIVE_DATACENTER = tools.LIVE_DATACENTER
LIVE_STORAGE = tools.LIVE_STORAGE
PURCHASE_RESOURCE = tools.PURCHASE_RESOURCE
NATIVE_TOOL = tools.NATIVE_TOOL
EXECUTION_VERIFICATION = tools.EXECUTION_VERIFICATION
ALLOWED_IMAGE = tools.ALLOWED_IMAGE
SERVICE_RESOURCE = tools.SERVICE_RESOURCE
LIVE_SERVICE = tools.LIVE_SERVICE
BILL = tools.BILL
PAYMENT = tools.PAYMENT
REFUND = tools.REFUND
DISCOUNT = tools.DISCOUNT
SUBSCRIPTION = tools.SUBSCRIPTION
)
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
return dbs.FiltersFromFlatMap(flatMap, target)
}
func GetMySelf() (*peer.Peer, error) {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
if pp == nil {
return nil, errors.New("peer not found")
}
return pp.(*peer.Peer), err
}
func IsMySelf(peerID string) (bool, string) {
return utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{Admin: true}))
}
func GenerateNodeID() (string, error) {
folderStatic := "/var/lib/opencloud-node"
if _, err := os.Stat(folderStatic); err == nil {
os.MkdirAll(folderStatic, 0644)
}
folderStatic += "/node_id"
if _, err := os.Stat(folderStatic); os.IsNotExist(err) {
hostname, err := os.Hostname()
if err != nil {
return "", err
}
id := uuid.NewSHA1(uuid.NameSpaceOID, []byte("oc-"+hostname))
err = os.WriteFile(folderStatic, []byte(id.String()), 0644)
return id.String(), err
}
data, err := os.ReadFile(folderStatic)
return string(data), err
}
// will turn into standards api hostnames
func (d LibDataEnum) API() string {
return tools.DefaultAPI[d]
return tools.Str[d]
}
// will turn into standards name
@@ -97,25 +158,42 @@ type LibData struct {
}
func InitDaemon(appName string) {
beego.BConfig.AppName = appName
config.SetAppName(appName) // set the app name to the logger to define the main log chan
// create a temporary console logger for init
logs.SetLogger(logs.CreateLogger("main"))
// Load the right config file
o := GetConfLoader()
o := GetConfLoader(appName)
// 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"),
o.GetStringDefault("LOKI_URL", ""),
o.GetStringDefault("LOG_LEVEL", "info"),
o.GetIntDefault("API_PORT", 8080),
o.GetStringDefault("PUBLIC_KEY_PATH", "./pem/public.pem"),
o.GetStringDefault("PRIVATE_KEY_PATH", "./pem/private.pem"),
o.GetStringDefault("INTERNAL_CATALOG_API", "oc-catalog"),
o.GetStringDefault("INTERNAL_SHARED_API", "oc-shared"),
o.GetStringDefault("INTERNAL_WORKFLOW_API", "oc-workflow"),
o.GetStringDefault("INTERNAL_WORKSPACE_API", "oc-workspace"),
o.GetStringDefault("INTERNAL_PEER_API", "oc-peer"),
o.GetStringDefault("INTERNAL_DATACENTER_API", "oc-datacenter"),
o.GetStringDefault("INTERNAL_SCHEDULER_API", "oc-scheduler"),
)
// 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 {
@@ -135,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 ")
@@ -144,27 +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 Init(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)
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...)
}
}
//
@@ -186,8 +341,12 @@ func GetLogger() zerolog.Logger {
* @param logLevel string
* @return *Config
*/
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *config.Config {
cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel)
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(isNano, isApi, mongoUrl, database, natsUrl, lokiUrl, logLevel, port, pkpath, pppath, internalCatalogAPI, internalSharedAPI, internalWorkflowAPI,
internalWorkspaceAPI, internalPeerAPI, internalDatacenterAPI, internalSchedulerAPI)
defer func() {
if r := recover(); r != nil {
tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
@@ -221,72 +380,37 @@ func GetConfig() *config.Config {
* The configuration loader will give priority to the local file over the default file
*/
func GetConfLoader() *onion.Onion {
return config.GetConfLoader()
func GetConfLoader(appName string) *onion.Onion {
return config.GetConfLoader(appName)
}
type Request struct {
collection LibDataEnum
user string
peerID string
groups []string
caller *tools.HTTPCaller
Collection LibDataEnum
User string
PeerID string
Groups []string
Caller *tools.HTTPCaller
admin bool
}
func NewRequest(collection LibDataEnum, user string, peerID string, groups []string, caller *tools.HTTPCaller) *Request {
return &Request{collection: collection, user: user, peerID: peerID, groups: groups, caller: caller}
return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller}
}
func ToScheduler(m interface{}) (n *workflow_execution.WorkflowSchedule) {
defer func() {
if r := recover(); r != nil {
return
}
}()
return m.(*workflow_execution.WorkflowSchedule)
}
func (r *Request) Schedule(wfID string, scheduler *workflow_execution.WorkflowSchedule) (*workflow_execution.WorkflowSchedule, error) {
ws, _, _, err := scheduler.Schedules(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
if err != nil {
return nil, err
func NewRequestInfoAdmin(collection LibDataEnum, user string, groups []string, caller *tools.HTTPCaller) *Request {
p, err := GetMySelf()
peerID := ""
if p != nil && err == nil {
peerID = p.GetID()
}
fmt.Println("BAM", ws)
return ws, nil
return &Request{Collection: collection, User: user, PeerID: peerID, Groups: groups, Caller: caller, admin: true}
}
func (r *Request) CheckBooking(wfID string, start string, end string, durationInS float64, cron string) bool {
ok, _, _, _, err := workflow_execution.NewScheduler(start, end, durationInS, cron).CheckBooking(wfID, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
})
if err != nil {
fmt.Println(err)
return false
}
return ok
}
func (r *Request) DraftOrder(scheduler *workflow_execution.WorkflowSchedule) (*order.Order, error) {
o := &order.Order{}
if err := o.DraftOrder(scheduler, &tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}); err != nil {
return nil, err
}
return o, nil
func NewRequestAdmin(collection LibDataEnum, caller *tools.HTTPCaller) *Request {
return &Request{Collection: collection, Caller: caller, admin: true}
}
/*
func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.WorkflowSchedule) error {
return o.Pay(scheduler, &tools.APIRequest{
Caller: r.caller,
@@ -294,8 +418,9 @@ func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.Wo
PeerID: r.peerID,
Groups: r.groups,
})
return nil
}
*/
/*
* Search will search for the data in the database
* @param filters *dbs.Filters
@@ -304,19 +429,20 @@ 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)))
data = LibDataShallow{Data: nil, Code: 500, Err: "Panic recovered in LoadAll : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}).Search(filters, word, isDraft)
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).Search(filters, word, isDraft, offset, limit)
if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
return
@@ -331,19 +457,20 @@ 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())))
data = LibDataShallow{Data: nil, Code: 500, Err: "Panic recovered in LoadAll : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}).LoadAll(isDraft)
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).LoadAll(isDraft, offset, limit)
if err != nil {
data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
return
@@ -366,11 +493,12 @@ func (r *Request) LoadOne(id string) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in LoadOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).LoadOne(id)
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -395,13 +523,14 @@ func (r *Request) UpdateOne(set map[string]interface{}, id string) (data LibData
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
model := models.Model(r.collection.EnumIndex())
model := models.Model(r.Collection.EnumIndex())
d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
}).UpdateOne(model.Deserialize(set, model), id)
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).UpdateOne(set, id)
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
return
@@ -424,11 +553,12 @@ func (r *Request) DeleteOne(id string) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in DeleteOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
d, code, err := models.Model(r.Collection.EnumIndex()).GetAccessor(&tools.APIRequest{
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).DeleteOne(id)
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -452,12 +582,13 @@ func (r *Request) StoreOne(object map[string]interface{}) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in StoreOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
model := models.Model(r.collection.EnumIndex())
model := models.Model(r.Collection.EnumIndex())
d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).StoreOne(model.Deserialize(object, model))
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -481,12 +612,13 @@ func (r *Request) CopyOne(object map[string]interface{}) (data LibData) {
data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
}
}()
model := models.Model(r.collection.EnumIndex())
model := models.Model(r.Collection.EnumIndex())
d, code, err := model.GetAccessor(&tools.APIRequest{
Caller: r.caller,
Username: r.user,
PeerID: r.peerID,
Groups: r.groups,
Caller: r.Caller,
Username: r.User,
PeerID: r.PeerID,
Groups: r.Groups,
Admin: r.admin,
}).CopyOne(model.Deserialize(object, model))
if err != nil {
data = LibData{Data: d, Code: code, Err: err.Error()}
@@ -576,3 +708,189 @@ func (l *LibData) ToOrder() *order.Order {
}
return nil
}
func (l *LibData) ToLiveDatacenter() *live.LiveDatacenter {
if l.Data.GetAccessor(nil).GetType() == tools.LIVE_DATACENTER {
return l.Data.(*live.LiveDatacenter)
}
return nil
}
func (l *LibData) ToLiveStorage() *live.LiveStorage {
if l.Data.GetAccessor(nil).GetType() == tools.LIVE_STORAGE {
return l.Data.(*live.LiveStorage)
}
return nil
}
func (l *LibData) ToBookings() *booking.Booking {
if l.Data.GetAccessor(nil).GetType() == tools.BOOKING {
return l.Data.(*booking.Booking)
}
return nil
}
func (l *LibData) ToPurchasedResource() *purchase_resource.PurchaseResource {
if l.Data.GetAccessor(nil).GetType() == tools.PURCHASE_RESOURCE {
return l.Data.(*purchase_resource.PurchaseResource)
}
return nil
}
func (l *LibData) ToBill() *billing.Bill {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.BILL {
return l.Data.(*billing.Bill)
}
return nil
}
func (l *LibData) ToPayment() *payment.Payment {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.PAYMENT {
return l.Data.(*payment.Payment)
}
return nil
}
func (l *LibData) ToRefund() *refund.Refund {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.REFUND {
return l.Data.(*refund.Refund)
}
return nil
}
func (l *LibData) ToDiscount() *discount.Discount {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.DISCOUNT {
return l.Data.(*discount.Discount)
}
return nil
}
func (l *LibData) ToSubscription() *subscription.Subscription {
if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.SUBSCRIPTION {
return l.Data.(*subscription.Subscription)
}
return nil
}
// ------------- Loading resources ----------GetAccessor
func LoadOneStorage(storageId string, user string, peerID string, groups []string) (*resources.StorageResource, error) {
res := NewRequest(LibDataEnum(STORAGE_RESOURCE), user, peerID, groups, nil).LoadOne(storageId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading storage ressource " + storageId)
return nil, errors.New(res.Err)
}
return res.ToStorageResource(), nil
}
func LoadOneComputing(computingId string, user string, peerID string, groups []string) (*resources.ComputeResource, error) {
res := NewRequest(LibDataEnum(COMPUTE_RESOURCE), user, peerID, groups, nil).LoadOne(computingId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading computing ressource " + computingId)
return nil, errors.New(res.Err)
}
return res.ToComputeResource(), nil
}
func LoadOneProcessing(processingId string, user string, peerID string, groups []string) (*resources.ProcessingResource, error) {
res := NewRequest(LibDataEnum(PROCESSING_RESOURCE), user, peerID, groups, nil).LoadOne(processingId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading processing ressource " + processingId)
return nil, errors.New(res.Err)
}
return res.ToProcessingResource(), nil
}
func LoadOneData(dataId string, user string, peerID string, groups []string) (*resources.DataResource, error) {
res := NewRequest(LibDataEnum(DATA_RESOURCE), user, peerID, groups, nil).LoadOne(dataId)
if res.Code != 200 {
l := GetLogger()
l.Error().Msg("Error while loading data ressource " + dataId)
return nil, errors.New(res.Err)
}
return res.ToDataResource(), nil
}
// verify signature...
func InitNATSDecentralizedEmitter(authorizedDT ...tools.DataType) {
tools.NewNATSCaller().ListenNats(map[tools.NATSMethod]func(tools.NATSResponse){
tools.CREATE_RESOURCE: func(resp tools.NATSResponse) {
if resp.FromApp == config.GetAppName() || !slices.Contains(authorizedDT, resp.Datatype) {
return
}
p := map[string]interface{}{}
if err := json.Unmarshal(resp.Payload, &p); err == nil {
if err := verify(resp.Payload); err != nil {
return // don't trust anyone... only friends and foes are privilege
}
access := NewRequestAdmin(LibDataEnum(resp.Datatype), nil)
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false, 0, 1); len(data.Data) > 0 {
delete(p, "id")
access.UpdateOne(p, data.Data[0].GetID())
} else {
access.StoreOne(p)
}
}
},
tools.REMOVE_RESOURCE: func(resp tools.NATSResponse) {
if resp.FromApp == config.GetAppName() || !slices.Contains(authorizedDT, resp.Datatype) {
return
}
if err := verify(resp.Payload); err != nil {
return // don't trust anyone... only friends and foes are privilege
}
p := map[string]interface{}{}
access := NewRequestAdmin(LibDataEnum(resp.Datatype), nil)
err := json.Unmarshal(resp.Payload, &p)
if err == nil {
if data := access.Search(nil, fmt.Sprintf("%v", p[resp.SearchAttr]), false, 0, 1); len(data.Data) > 0 {
access.DeleteOne(data.Data[0].GetID())
}
}
},
})
}
func verify(payload []byte) error {
var obj utils.AbstractObject
if err := json.Unmarshal(payload, &obj); err == nil {
obj.Unsign()
origin := NewRequestAdmin(LibDataEnum(PEER), nil).LoadOne(obj.GetCreatorID())
if origin.Data == nil || origin.Data.(*peer.Peer).Relation != peer.PARTNER {
return errors.New("don't know personnaly this guy") // don't trust anyone... only friends and foes are privilege
}
data, err := base64.StdEncoding.DecodeString(origin.Data.(*peer.Peer).PublicKey)
if err != nil {
return err
}
pk, err := crypto.UnmarshalPublicKey(data)
if err != nil {
return err
}
b, err := json.Marshal(obj)
if err != nil {
return err
}
if ok, err := pk.Verify(b, obj.GetSignature()); err != nil {
return err
} else if !ok {
return errors.New("signature is not corresponding to public key")
} else {
return nil
}
} else {
return err
}
}
Regular → Executable
+61 -12
View File
@@ -1,28 +1,75 @@
module cloud.o-forge.io/core/oc-lib
go 1.22.0
go 1.25.0
require (
github.com/beego/beego/v2 v2.3.1
github.com/beego/beego/v2 v2.3.8
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/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.11.1
k8s.io/apimachinery v0.35.1
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/ipfs/go-cid v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.16.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.1 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.6.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/time v0.9.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
go.mongodb.org/mongo-driver v1.16.0
golang.org/x/sys v0.22.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)
require (
@@ -36,7 +83,6 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
@@ -45,16 +91,19 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/robfig/cron v1.2.0
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.1
k8s.io/client-go v0.35.1
)
replace github.com/libp2p/go-libp2p/core => github.com/libp2p/go-libp2p v0.47.0
Regular → Executable
+150 -25
View File
@@ -1,6 +1,8 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beego/beego/v2 v2.3.1 h1:7MUKMpJYzOXtCUsTEoXOxsDV/UcHw6CPbaWMlthVNsc=
github.com/beego/beego/v2 v2.3.1/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc=
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
@@ -10,15 +12,34 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -27,12 +48,18 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -42,34 +69,76 @@ github.com/goraz/onion v0.1.3/go.mod h1:XEmz1XoBz+wxTgWB8NwuvRm4RAu3vKxvrmYtzK+X
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-libp2p/core v0.43.0-rc2 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc=
github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo=
github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ=
github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
@@ -77,6 +146,10 @@ github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDm
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -89,12 +162,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -105,10 +174,23 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -120,23 +202,33 @@ github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dh
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -147,27 +239,60 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+5 -6
View File
@@ -58,14 +58,13 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
// Add label that have been added to the event
// A bit unsafe since we don't know what could be stored in the event
// but we can't access this object once passed to the multilevel writter
for k,v := range(event){
if k != "level" && k != "time" && k != "message"{
labels[k] = v.(string)
for k, v := range event {
if k != "level" && k != "time" && k != "message" {
labels[k] = fmt.Sprintf("%v", v)
}
}
// Format the timestamp in nanoseconds
timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond))
@@ -87,7 +86,7 @@ func (w *LokiWriter) Write(p []byte) (n int, err error) {
//fmt.Printf("Sending payload to Loki: %s\n", string(payloadBytes))
req, err := http.NewRequest("POST", w.url + "/loki/api/v1/push", bytes.NewReader(payloadBytes))
req, err := http.NewRequest("POST", w.url+"/loki/api/v1/push", bytes.NewReader(payloadBytes))
if err != nil {
return 0, fmt.Errorf("failed to create HTTP request: %w", err)
}
+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"},
},
}
}
+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
}
+22
View File
@@ -0,0 +1,22 @@
package billing
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type billMongoAccessor struct {
utils.AbstractAccessor[*Bill]
}
func NewAccessor(request *tools.APIRequest) *billMongoAccessor {
return &billMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Bill]{
Logger: logs.CreateLogger(tools.BILL.String()),
Request: request,
Type: tools.BILL,
New: func() *Bill { return &Bill{} },
},
}
}
+146
View File
@@ -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{} },
},
}
}
+89
View File
@@ -0,0 +1,89 @@
package billing_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/billing"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBill_StoreDraftDefault(t *testing.T) {
b := &billing.Bill{}
b.StoreDraftDefault()
assert.True(t, b.IsDraft)
}
func TestBill_CanDelete_Draft(t *testing.T) {
b := &billing.Bill{}
b.IsDraft = true
assert.True(t, b.CanDelete())
}
func TestBill_CanDelete_NonDraft(t *testing.T) {
b := &billing.Bill{}
b.IsDraft = false
assert.False(t, b.CanDelete())
}
func TestBill_CanUpdate_StatusChange_NonDraft(t *testing.T) {
b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = false
set := &billing.Bill{Status: enum.PAID}
ok, returned := b.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.PAID, returned.(*billing.Bill).Status)
}
func TestBill_CanUpdate_SameStatus_NonDraft(t *testing.T) {
b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = false
set := &billing.Bill{Status: enum.PENDING}
ok, _ := b.CanUpdate(set)
assert.False(t, ok)
}
func TestBill_CanUpdate_Draft(t *testing.T) {
b := &billing.Bill{Status: enum.PENDING}
b.IsDraft = true
set := &billing.Bill{Status: enum.PAID}
ok, _ := b.CanUpdate(set)
assert.True(t, ok)
}
func TestBill_GetAccessor(t *testing.T) {
b := &billing.Bill{}
acc := b.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestBill_GetAccessor_NilRequest(t *testing.T) {
b := &billing.Bill{}
acc := b.GetAccessor(nil)
assert.NotNil(t, acc)
}
func TestGenerateBill_Basic(t *testing.T) {
o := &order.Order{
AbstractObject: utils.AbstractObject{UUID: "order-uuid-1"},
}
req := &tools.APIRequest{PeerID: "peer-abc"}
b, err := billing.GenerateBill(o, req)
require.NoError(t, err)
assert.NotNil(t, b)
assert.Equal(t, "order-uuid-1", b.OrderID)
assert.Equal(t, enum.PENDING, b.Status)
assert.False(t, b.IsDraft)
assert.Contains(t, b.Name, "peer-abc")
}
func TestBill_SumUpBill_NoSubOrders(t *testing.T) {
b := &billing.Bill{Total: 0}
result, err := b.SumUpBill(nil)
require.NoError(t, err)
assert.Equal(t, 0.0, result.Total)
}
+2
View File
@@ -0,0 +1,2 @@
# Billing process
Scheduler process a drafted order + a first bill corresponding to every once buying.
+84 -43
View File
@@ -3,56 +3,90 @@ package booking
import (
"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/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"go.mongodb.org/mongo-driver/bson/primitive"
)
/*
* Booking is a struct that represents a booking
*/
type Booking struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
DestPeerID string `json:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer
WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`
State enum.BookingStatus `json:"state,omitempty" bson:"state,omitempty" validate:"required"` // State is the state of the booking
ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
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"`
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
DestPeerID string `json:"dest_peer_id,omitempty" bson:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer
WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`
State enum.BookingStatus `json:"state" bson:"state"` // State is the state of the booking
ExpectedStartDate time.Time `json:"expected_start_date,omitempty" bson:"expected_start_date,omitempty" validate:"required"` // ExpectedStartDate is the expected start date of the booking
ExpectedEndDate *time.Time `json:"expected_end_date,omitempty" bson:"expected_end_date,omitempty" validate:"required"` // ExpectedEndDate is the expected end date of the booking
RealStartDate *time.Time `json:"real_start_date,omitempty" bson:"real_start_date,omitempty"` // RealStartDate is the real start date of the booking
RealEndDate *time.Time `json:"real_end_date,omitempty" bson:"real_end_date,omitempty"` // RealEndDate is the real end date of the booking
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty" validate:"required"` // ResourceType is the type of the resource
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty" validate:"required"` // could be a Compute or a Storage
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
// Authorization: identifies who created this draft and the Check session it belongs to.
// Used to verify UPDATE and DELETE orders from remote schedulers.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
// Peerless is true when the booked resource has no destination peer
// (e.g. a public Docker Hub image). No peer confirmation or pricing
// negotiation is needed; the booking is stored locally only.
Peerless bool `json:"peerless,omitempty" bson:"peerless,omitempty"`
// OriginRef carries the registry reference of a peerless resource
// (e.g. "docker.io/pytorch/pytorch:2.1") so schedulers can validate it.
OriginRef string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"`
// BillingStrategy est la fréquence de facturation appliquée à ce booking
// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
// Transmis depuis WorkflowExecution.SelectedBillingStrategy lors du Book().
BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
// PaymentType est le mode de paiement choisi pour cette ressource spécifique
// (PAY_ONCE, PAY_EVERY_WEEK, PAY_EVERY_MONTH, PAY_EVERY_YEAR).
// Résolu depuis WorkflowExecution.SelectedPaymentMode[itemID] lors du Book().
PaymentType pricing.PaymentType `json:"payment_type" bson:"payment_type"`
}
// CheckBooking checks if a booking is possible on a specific compute resource
func (wfa *Booking) Check(id string, start time.Time, end *time.Time, parrallelAllowed int) (bool, error) {
// check if
if end == nil {
// if no end... then Book like a savage
e := start.Add(time.Hour)
end = &e
func (b *Booking) CalcDeltaOfExecution() map[string]map[string]models.MetricResume {
m := map[string]map[string]models.MetricResume{}
for instance, snapshot := range b.ExecutionMetrics {
m[instance] = map[string]models.MetricResume{}
for _, metric := range snapshot {
for _, mm := range metric.Metrics {
if resume, ok := m[instance][mm.Name]; !ok {
m[instance][mm.Name] = models.MetricResume{
Delta: 0,
LastValue: mm.Value,
}
} else {
delta := resume.LastValue - mm.Value
if delta == 0 {
resume.Delta = delta
} else {
resume.Delta = (resume.Delta + delta) / 2
}
resume.LastValue = mm.Value
m[instance][mm.Name] = resume
}
}
}
}
accessor := NewAccessor(nil)
res, code, err := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{ // check if there is a booking on the same compute resource by filtering on the compute_resource_id, the state and the execution date
"resource_id": {{Operator: dbs.EQUAL.String(), Value: id}},
"state": {{Operator: dbs.EQUAL.String(), Value: enum.DRAFT.EnumIndex()}},
"expected_start_date": {
{Operator: dbs.LTE.String(), Value: primitive.NewDateTimeFromTime(*end)},
{Operator: dbs.GTE.String(), Value: primitive.NewDateTimeFromTime(start)},
},
},
}, "", wfa.IsDraft)
if code != 200 {
return false, err
}
return len(res) <= parrallelAllowed, nil
return m
}
func (d *Booking) GetDelayForLaunch() time.Duration {
@@ -60,10 +94,10 @@ func (d *Booking) GetDelayForLaunch() time.Duration {
}
func (d *Booking) GetDelayForFinishing() time.Duration {
if d.ExpectedEndDate == nil {
if d.ExpectedEndDate == nil || d.RealEndDate == nil {
return time.Duration(0)
}
return d.RealEndDate.Sub(d.ExpectedStartDate)
return d.RealEndDate.Sub(*d.ExpectedEndDate)
}
func (d *Booking) GetUsualDuration() time.Duration {
@@ -85,23 +119,30 @@ func (d *Booking) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *Booking) VerifyAuth(request *tools.APIRequest) bool {
func (d *Booking) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}
func (r *Booking) StoreDraftDefault() {
r.IsDraft = false
r.IsDraft = true
r.State = enum.DRAFT
}
func (r *Booking) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
if !r.IsDraft && r.State != set.(*Booking).State || r.RealStartDate != set.(*Booking).RealStartDate || r.RealEndDate != set.(*Booking).RealEndDate {
return true, &Booking{
State: set.(*Booking).State,
RealStartDate: set.(*Booking).RealStartDate,
RealEndDate: set.(*Booking).RealEndDate,
} // only state can be updated
incoming := set.(*Booking)
if !r.IsDraft && r.State != incoming.State || r.RealStartDate != incoming.RealStartDate || r.RealEndDate != incoming.RealEndDate {
patch := &Booking{
State: incoming.State,
RealStartDate: incoming.RealStartDate,
RealEndDate: incoming.RealEndDate,
}
// Auto-set RealStartDate when transitioning to STARTED and not already set
if r.State != enum.STARTED && incoming.State == enum.STARTED && patch.RealStartDate == nil {
now := time.Now()
patch.RealStartDate = &now
}
return true, patch
}
// TODO : HERE WE CAN HANDLE THE CASE WHERE THE BOOKING IS DELAYED OR EXCEEDING OR ending sooner
return r.IsDraft, set
}
+23 -36
View File
@@ -4,24 +4,25 @@ import (
"errors"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"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/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type bookingMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
type BookingMongoAccessor struct {
utils.AbstractAccessor[*Booking] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
return &bookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
// New creates a new instance of the BookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
return &BookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*Booking]{
Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type
Request: request,
Type: tools.BOOKING,
New: func() *Booking { return &Booking{} },
},
}
}
@@ -29,32 +30,25 @@ func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *bookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if set.(*Booking).State == 0 {
func (a *BookingMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
if set["state"] == nil {
return nil, 400, errors.New("state is required")
}
realSet := &Booking{State: set.(*Booking).State}
return utils.GenericUpdateOne(realSet, id, a, &Booking{})
set = map[string]interface{}{
"state": set["state"],
}
return utils.GenericUpdateOne(set, id, a)
}
func (a *bookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *bookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) {
func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne(id, a.New(), func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now()
now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
return utils.GenericDeleteOne(d.GetID(), a)
// Direct raw delete to avoid infinite recursion:
// GenericDeleteOne calls a.LoadOne which would re-enter this callback.
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
return nil, 410, errors.New("draft booking expired and deleted")
}
if (d.(*Booking).ExpectedEndDate) == nil {
d.(*Booking).State = enum.FORGOTTEN
@@ -67,20 +61,13 @@ func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
}, a)
}
func (a *bookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Booking](a.getExec(), isDraft, a)
}
func (a *bookingMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Booking](filters, search, (&Booking{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *bookingMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
func (a *BookingMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now()
now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
utils.GenericDeleteOne(d.GetID(), a)
// Direct raw delete to avoid infinite recursion (same as LoadOne callback).
mongo.MONGOService.DeleteOne(d.GetID(), a.GetType().String())
return nil
}
if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) {
+13
View File
@@ -0,0 +1,13 @@
package booking
import "cloud.o-forge.io/core/oc-lib/models/common/enum"
// BookingMode is kept here as an alias for backward compatibility.
// The canonical definition lives in models/common/enum.
type BookingMode = enum.BookingMode
const (
PLANNED = enum.PLANNED
PREEMPTED = enum.PREEMPTED
WHEN_POSSIBLE = enum.WHEN_POSSIBLE
)
+567
View File
@@ -0,0 +1,567 @@
package planner
import (
"encoding/json"
"sort"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
// InstanceCapacity holds the maximum available resources of a single resource instance.
type InstanceCapacity struct {
CPUCores map[string]float64 `json:"cpu_cores,omitempty"` // model -> total cores
GPUMemGB map[string]float64 `json:"gpu_mem_gb,omitempty"` // model -> total memory GB
RAMGB float64 `json:"ram_gb,omitempty"` // total RAM GB
StorageGB float64 `json:"storage_gb,omitempty"` // total storage GB
MaxConcurrent float64 `json:"max_concurrent,omitempty"` // HOSTED service: max simultaneous callers
}
// ResourceRequest describes the resource amounts needed for a prospective booking.
// A nil map or nil pointer for a dimension means "use the full instance capacity" for that dimension.
type ResourceRequest struct {
CPUCores map[string]float64 // model -> cores needed (nil = max)
GPUMemGB map[string]float64 // model -> memory GB needed (nil = max)
RAMGB *float64 // GB needed (nil = max)
StorageGB *float64 // GB needed (nil = max)
}
// PlannerSlot represents a single booking occupying a resource instance during a time window.
// Usage maps each resource dimension (cpu_<model>, gpu_<model>, ram, storage) to
// its percentage of consumption relative to the instance's maximum capacity (0100).
type PlannerSlot struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
InstanceID string `json:"instance_id,omitempty"` // instance targeted by this booking
BookingID string `json:"booking_id,omitempty"` // empty in shallow mode
Usage map[string]float64 `json:"usage,omitempty"` // dimension -> % of max (0-100)
}
// PlannerITF is the interface used by Planify to check resource availability.
// *Planner satisfies this interface.
type PlannerITF interface {
NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time
}
// Planner is a volatile (non-persisted) object that organises bookings by resource.
// ComputeResource, StorageResource and HOSTED ServiceResource bookings appear in the schedule.
// BlockedResources marks resources for which no matching Live was found at generation time:
// any availability check against a blocked resource returns false immediately.
type Planner struct {
GeneratedAt time.Time `json:"generated_at"`
Schedule map[string][]*PlannerSlot `json:"schedule"` // resource_id -> slots
Capacities map[string]map[string]*InstanceCapacity `json:"capacities"` // resource_id -> instance_id -> max capacity
BlockedResources map[string]bool `json:"blocked_resources,omitempty"` // resource_id -> no Live found
}
// Generate builds a full Planner from all active bookings.
// Each slot includes the booking ID, the instance ID, and the usage percentage of every resource dimension.
func Generate(request *tools.APIRequest) (*Planner, error) {
return generate(request, false)
}
// GenerateShallow builds a Planner from all active bookings without booking IDs.
func GenerateShallow(request *tools.APIRequest) (*Planner, error) {
return generate(request, true)
}
func generate(request *tools.APIRequest, shallow bool) (*Planner, error) {
accessor := booking.NewAccessor(request)
// Include both confirmed (IsDraft=false) and draft (IsDraft=true) bookings
// so the planner reflects the full picture: first-come first-served on all
// pending reservations regardless of confirmation state.
confirmed, code, err := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
},
}, "*", false, 0, 10000)
if code != 200 || err != nil {
return nil, err
}
drafts, _, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"expected_start_date": {{Operator: dbs.GTE.String(), Value: time.Now().UTC()}},
},
}, "*", true, 0, 10000)
bookings := append(confirmed, drafts...)
p := &Planner{
GeneratedAt: time.Now(),
Schedule: map[string][]*PlannerSlot{},
Capacities: map[string]map[string]*InstanceCapacity{},
BlockedResources: map[string]bool{},
}
for _, b := range bookings {
bk := b.(*booking.Booking)
// Skip terminal bookings — they no longer occupy capacity.
switch bk.State {
case enum.SUCCESS, enum.FAILURE, enum.FORGOTTEN, enum.CANCELLED:
continue
}
// Eligible resource types: compute, storage, and HOSTED services.
if bk.ResourceType != tools.COMPUTE_RESOURCE &&
bk.ResourceType != tools.STORAGE_RESOURCE &&
bk.ResourceType != tools.SERVICE_RESOURCE {
continue
}
end := bk.ExpectedEndDate
if end == nil {
e := bk.ExpectedStartDate.UTC().Add(5 * time.Minute)
end = &e
}
instanceID, usage, cap, blocked := extractSlotData(bk, request)
if blocked {
p.BlockedResources[bk.ResourceID] = true
continue
}
if instanceID == "" {
instanceID = bk.InstanceID
}
if cap != nil && instanceID != "" {
if p.Capacities[bk.ResourceID] == nil {
p.Capacities[bk.ResourceID] = map[string]*InstanceCapacity{}
}
p.Capacities[bk.ResourceID][instanceID] = cap
}
slot := &PlannerSlot{
Start: bk.ExpectedStartDate,
End: *end,
InstanceID: instanceID,
Usage: usage,
}
if !shallow {
slot.BookingID = bk.GetID()
}
p.Schedule[bk.ResourceID] = append(p.Schedule[bk.ResourceID], slot)
}
return p, nil
}
// Check reports whether the requested time window has enough remaining capacity
// on the specified instance of the given resource.
//
// req describes the amounts needed; nil fields default to the full instance capacity.
// If req itself is nil, the full capacity of every dimension is assumed.
// If end is nil, a 1-hour window from start is assumed.
//
// A slot that overlaps the requested window is acceptable if, for every requested
// dimension, existing usage + requested usage ≤ 100 %.
// Slots targeting other instances are ignored.
// If no capacity is known for this instance (never booked), it is fully available.
func (p *Planner) Check(resourceID string, instanceID string, req *ResourceRequest, start time.Time, end *time.Time) bool {
if p.BlockedResources[resourceID] {
return false // no Live found at generation time — cannot book
}
if end == nil {
e := start.Add(5 * time.Minute)
end = &e
}
cap := p.instanceCapacity(resourceID, instanceID)
reqPct := toPercentages(req, cap)
slots, ok := p.Schedule[resourceID]
if !ok {
return true
}
for _, slot := range slots {
// Only consider slots on the same instance
if slot.InstanceID != instanceID {
continue
}
// Only consider overlapping slots
if !slot.Start.Before(*end) || !slot.End.After(start) {
continue
}
// If capacity is unknown (reqPct empty), any overlap blocks the slot.
if len(reqPct) == 0 {
return false
}
// Combined usage must not exceed 100 % for any requested dimension
for dim, needed := range reqPct {
if slot.Usage[dim]+needed >= 100.0 {
return false
}
}
}
return true
}
// instanceCapacity returns the stored max capacity for a resource/instance pair.
// Returns an empty (but non-nil) capacity when the instance has never been booked.
func (p *Planner) instanceCapacity(resourceID, instanceID string) *InstanceCapacity {
if instances, ok := p.Capacities[resourceID]; ok {
if c, ok := instances[instanceID]; ok {
return c
}
}
return &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
}
}
// toPercentages converts a ResourceRequest into a map of dimension -> percentage-of-max.
// nil fields in req (or nil req) are treated as requesting the full capacity (100 %).
func toPercentages(req *ResourceRequest, cap *InstanceCapacity) map[string]float64 {
pct := map[string]float64{}
if req == nil {
for model := range cap.CPUCores {
pct["cpu_"+model] = 100.0
}
for model := range cap.GPUMemGB {
pct["gpu_"+model] = 100.0
}
if cap.RAMGB > 0 {
pct["ram"] = 100.0
}
if cap.StorageGB > 0 {
pct["storage"] = 100.0
}
return pct
}
if req.CPUCores == nil {
for model, maxCores := range cap.CPUCores {
if maxCores > 0 {
pct["cpu_"+model] = 100.0
}
}
} else {
for model, needed := range req.CPUCores {
if maxCores, ok := cap.CPUCores[model]; ok && maxCores > 0 {
pct["cpu_"+model] = (needed / maxCores) * 100.0
}
}
}
if req.GPUMemGB == nil {
for model, maxMem := range cap.GPUMemGB {
if maxMem > 0 {
pct["gpu_"+model] = 100.0
}
}
} else {
for model, needed := range req.GPUMemGB {
if maxMem, ok := cap.GPUMemGB[model]; ok && maxMem > 0 {
pct["gpu_"+model] = (needed / maxMem) * 100.0
}
}
}
if req.RAMGB == nil {
if cap.RAMGB > 0 {
pct["ram"] = 100.0
}
} else if cap.RAMGB > 0 {
pct["ram"] = (*req.RAMGB / cap.RAMGB) * 100.0
}
if req.StorageGB == nil {
if cap.StorageGB > 0 {
pct["storage"] = 100.0
}
} else if cap.StorageGB > 0 {
pct["storage"] = (*req.StorageGB / cap.StorageGB) * 100.0
}
// HOSTED service: each booking consumes one call slot.
if cap.MaxConcurrent > 0 {
pct["calls"] = (1.0 / cap.MaxConcurrent) * 100.0
}
return pct
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
// extractSlotData parses the booking's PricedItem, loads the corresponding Live resource
// as the authoritative capacity source, and returns the instance ID, usage percentages,
// capacity, and whether a matching Live was found.
// blocked=true means no Live exists for this resource; the resource must not be scheduled.
func extractSlotData(bk *booking.Booking, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
usage = map[string]float64{}
if len(bk.PricedItem) == 0 {
return
}
b, err := json.Marshal(bk.PricedItem)
if err != nil {
return
}
switch bk.ResourceType {
case tools.COMPUTE_RESOURCE:
instanceID, usage, cap = extractComputeSlot(b, bk.ResourceID, request)
case tools.STORAGE_RESOURCE:
instanceID, usage, cap = extractStorageSlot(b, bk.ResourceID, request)
case tools.SERVICE_RESOURCE:
instanceID, usage, cap, blocked = extractServiceSlot(b, bk.ResourceID, request)
}
return
}
// extractComputeSlot extracts the instance ID, usage percentages, and max capacity for a compute booking.
// Keys in usage: "cpu_<model>", "gpu_<model>", "ram".
func extractComputeSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
usage = map[string]float64{}
var priced resources.PricedComputeResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
return
}
res, _, err := (&resources.ComputeResource{}).GetAccessor(request).LoadOne(resourceID)
if err != nil {
return
}
compute := res.(*resources.ComputeResource)
instance := findComputeInstance(compute, priced.InstancesRefs)
if instance == nil {
return
}
instanceID = instance.GetID()
// Build the instance's maximum capacity
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
RAMGB: totalRAM(instance),
}
for model := range instance.CPUs {
cap.CPUCores[model] = totalCPUCores(instance, model)
}
for model := range instance.GPUs {
cap.GPUMemGB[model] = totalGPUMemory(instance, model)
}
// Compute usage as a percentage of the instance's maximum capacity
for model, usedCores := range priced.CPUsLocated {
if maxCores := cap.CPUCores[model]; maxCores > 0 {
usage["cpu_"+model] = (usedCores / maxCores) * 100.0
}
}
for model, usedMem := range priced.GPUsLocated {
if maxMem := cap.GPUMemGB[model]; maxMem > 0 {
usage["gpu_"+model] = (usedMem / maxMem) * 100.0
}
}
if cap.RAMGB > 0 && priced.RAMLocated > 0 {
usage["ram"] = (priced.RAMLocated / cap.RAMGB) * 100.0
}
return
}
// extractStorageSlot extracts the instance ID, usage percentages, and max capacity for a storage booking.
// Key in usage: "storage".
func extractStorageSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity) {
usage = map[string]float64{}
var priced resources.PricedStorageResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
return
}
res, _, err := (&resources.StorageResource{}).GetAccessor(request).LoadOne(resourceID)
if err != nil {
return
}
storage := res.(*resources.StorageResource)
instance := findStorageInstance(storage, priced.InstancesRefs)
if instance == nil {
return
}
instanceID = instance.GetID()
maxStorage := float64(instance.SizeGB)
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
StorageGB: maxStorage,
}
if maxStorage > 0 && priced.UsageStorageGB > 0 {
usage["storage"] = (priced.UsageStorageGB / maxStorage) * 100.0
}
return
}
// extractServiceSlot extracts the instance ID, usage, and capacity for a HOSTED service booking.
// The LiveService is the authoritative source for MaxConcurrent — the ServiceResource is not trusted.
// If no LiveService references this resourceID, blocked=true signals the resource cannot be scheduled.
func extractServiceSlot(pricedJSON []byte, resourceID string, request *tools.APIRequest) (instanceID string, usage map[string]float64, cap *InstanceCapacity, blocked bool) {
usage = map[string]float64{}
var priced resources.PricedServiceResource
if err := json.Unmarshal(pricedJSON, &priced); err != nil {
blocked = true
return
}
// LiveService is the authoritative capacity source — look it up by resources_id.
liveResults, _, err := (&live.LiveService{}).GetAccessor(request).Search(
&dbs.Filters{
And: map[string][]dbs.Filter{
"resources_id": {{Operator: dbs.EQUAL.String(), Value: resourceID}},
},
}, "*", false, 0, 1)
if err != nil || len(liveResults) == 0 {
blocked = true // no Live → cannot schedule
return
}
ls := liveResults[0].(*live.LiveService)
if ls.MaxConcurrent <= 0 {
blocked = true
return
}
// Instance ID: use the first instance referenced by the priced item.
instanceID = priced.GetID()
if instanceID == "" {
instanceID = resourceID // fallback: treat the resource itself as the instance key
}
maxC := float64(ls.MaxConcurrent)
cap = &InstanceCapacity{
CPUCores: map[string]float64{},
GPUMemGB: map[string]float64{},
MaxConcurrent: maxC,
}
usage["calls"] = (1.0 / maxC) * 100.0
return
}
// findComputeInstance returns the instance referenced by the priced item's InstancesRefs,
// falling back to the first available instance.
func findComputeInstance(compute *resources.ComputeResource, refs map[string]string) *resources.ComputeResourceInstance {
for _, inst := range compute.Instances {
if _, ok := refs[inst.GetID()]; ok {
return inst
}
}
if len(compute.Instances) > 0 {
return compute.Instances[0]
}
return nil
}
// findStorageInstance returns the instance referenced by the priced item's InstancesRefs,
// falling back to the first available instance.
func findStorageInstance(storage *resources.StorageResource, refs map[string]string) *resources.StorageResourceInstance {
for _, inst := range storage.Instances {
if _, ok := refs[inst.GetID()]; ok {
return inst
}
}
if len(storage.Instances) > 0 {
return storage.Instances[0]
}
return nil
}
// totalCPUCores returns the total number of cores for a given CPU model across all nodes.
// It multiplies the per-chip core count (from the instance's CPU spec) by the total
// number of chips of that model across all nodes (chip_count × node.Quantity).
// Falls back to the spec's core count if no nodes are defined.
func totalCPUCores(instance *resources.ComputeResourceInstance, model string) float64 {
spec, ok := instance.CPUs[model]
if !ok || spec == nil || spec.Cores == 0 {
return 0
}
if len(instance.Nodes) == 0 {
return float64(spec.Cores)
}
totalChips := int64(0)
for _, node := range instance.Nodes {
if chipCount, ok := node.CPUs[model]; ok {
totalChips += chipCount * max(node.Quantity, 1)
}
}
if totalChips == 0 {
return float64(spec.Cores)
}
return float64(totalChips * int64(spec.Cores))
}
// totalGPUMemory returns the total GPU memory (GB) for a given model across all nodes.
// Falls back to the spec's memory if no nodes are defined.
func totalGPUMemory(instance *resources.ComputeResourceInstance, model string) float64 {
spec, ok := instance.GPUs[model]
if !ok || spec == nil || spec.MemoryGb == 0 {
return 0
}
if len(instance.Nodes) == 0 {
return spec.MemoryGb
}
totalUnits := int64(0)
for _, node := range instance.Nodes {
if unitCount, ok := node.GPUs[model]; ok {
totalUnits += unitCount * max(node.Quantity, 1)
}
}
if totalUnits == 0 {
return spec.MemoryGb
}
return float64(totalUnits) * spec.MemoryGb
}
// totalRAM returns the total RAM (GB) across all nodes of a compute instance.
func totalRAM(instance *resources.ComputeResourceInstance) float64 {
total := float64(0)
for _, node := range instance.Nodes {
if node.RAM != nil && node.RAM.SizeGb > 0 {
total += node.RAM.SizeGb * float64(max(node.Quantity, 1))
}
}
return total
}
// NextAvailableStart returns the earliest time >= start when resourceID/instanceID has a
// free window of duration d. Slots are scanned in order so a single linear pass suffices.
// If the planner has no slots for this resource/instance, start is returned unchanged.
func (p *Planner) NextAvailableStart(resourceID, instanceID string, start time.Time, d time.Duration) time.Time {
slots := p.Schedule[resourceID]
if len(slots) == 0 {
return start
}
// Collect and sort slots for this instance by start time.
relevant := make([]*PlannerSlot, 0, len(slots))
for _, s := range slots {
if s.InstanceID == instanceID {
relevant = append(relevant, s)
}
}
sort.Slice(relevant, func(i, j int) bool { return relevant[i].Start.Before(relevant[j].Start) })
end := start.Add(d)
for _, slot := range relevant {
if !slot.Start.Before(end) {
break // all remaining slots start after our window — done
}
if slot.End.After(start) {
// conflict: push start to after this slot
start = slot.End
end = start.Add(d)
}
}
return start
}
+87
View File
@@ -0,0 +1,87 @@
package booking_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
func TestBooking_GetDurations(t *testing.T) {
start := time.Now().Add(-10 * time.Minute)
end := start.Add(5 * time.Minute)
realStart := start.Add(30 * time.Minute)
realEnd := realStart.Add(90 * time.Minute)
b := &booking.Booking{
ExpectedStartDate: start,
ExpectedEndDate: &end,
RealStartDate: &realStart,
RealEndDate: &realEnd,
}
assert.Equal(t, 30*time.Minute, b.GetDelayForLaunch())
assert.Equal(t, 90*time.Minute, b.GetRealDuration())
assert.Equal(t, end.Sub(start), b.GetUsualDuration())
assert.Equal(t, b.GetRealDuration()-b.GetUsualDuration(), b.GetDelayOnDuration())
assert.Equal(t, realEnd.Sub(start), b.GetDelayForFinishing())
}
func TestBooking_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
b := &booking.Booking{}
accessor := b.GetAccessor(req)
assert.NotNil(t, accessor)
assert.Equal(t, tools.BOOKING, accessor.(*booking.BookingMongoAccessor).Type)
}
func TestBooking_VerifyAuth(t *testing.T) {
assert.True(t, (&booking.Booking{}).VerifyAuth("get", nil))
}
func TestBooking_StoreDraftDefault(t *testing.T) {
b := &booking.Booking{}
b.StoreDraftDefault()
assert.False(t, b.IsDraft)
}
func TestBooking_CanUpdate(t *testing.T) {
now := time.Now()
b := &booking.Booking{
State: enum.SCHEDULED,
AbstractObject: utils.AbstractObject{IsDraft: false},
RealStartDate: &now,
}
set := &booking.Booking{
State: enum.DELAYED,
RealStartDate: &now,
}
ok, result := b.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.DELAYED, result.(*booking.Booking).State)
}
func TestBooking_CanDelete(t *testing.T) {
b := &booking.Booking{AbstractObject: utils.AbstractObject{IsDraft: true}}
assert.True(t, b.CanDelete())
b.IsDraft = false
assert.False(t, b.CanDelete())
}
func TestNewAccessor(t *testing.T) {
req := &tools.APIRequest{}
accessor := booking.NewAccessor(req)
assert.NotNil(t, accessor)
assert.Equal(t, tools.BOOKING, accessor.Type)
assert.Equal(t, req, accessor.Request)
}
@@ -71,7 +71,7 @@ func (ao *CollaborativeArea) Clear(peerID string) {
ao.CollaborativeAreaRule.CreatedAt = time.Now().UTC()
}
func (ao *CollaborativeArea) VerifyAuth(request *tools.APIRequest) bool {
func (ao *CollaborativeArea) VerifyAuth(callName string, request *tools.APIRequest) bool {
if (ao.AllowedPeersGroup != nil || config.GetConfig().Whitelist) && request != nil {
if grps, ok := ao.AllowedPeersGroup[request.PeerID]; ok || config.GetConfig().Whitelist {
if slices.Contains(grps, "*") || (!ok && config.GetConfig().Whitelist) {
@@ -84,20 +84,16 @@ func (ao *CollaborativeArea) VerifyAuth(request *tools.APIRequest) bool {
}
}
}
return ao.AbstractObject.VerifyAuth(request)
return ao.AbstractObject.VerifyAuth(callName, request)
}
func (d *CollaborativeArea) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *CollaborativeArea) Trim() *CollaborativeArea {
return d
}
func (d *CollaborativeArea) StoreDraftDefault() {
d.AllowedPeersGroup = map[string][]string{
d.CreatorID: []string{"*"},
d.CreatorID: {"*"},
}
d.IsDraft = false
}
@@ -17,7 +17,7 @@ import (
// SharedWorkspace is a struct that represents a collaborative area
type collaborativeAreaMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
utils.AbstractAccessor[*CollaborativeArea] // AbstractAccessor contains the basic fields of an accessor (model, caller)
workspaceAccessor utils.Accessor
workflowAccessor utils.Accessor
@@ -27,10 +27,11 @@ type collaborativeAreaMongoAccessor struct {
func NewAccessor(request *tools.APIRequest) *collaborativeAreaMongoAccessor {
return &collaborativeAreaMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
AbstractAccessor: utils.AbstractAccessor[*CollaborativeArea]{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request,
Type: tools.COLLABORATIVE_AREA,
New: func() *CollaborativeArea { return &CollaborativeArea{} },
},
workspaceAccessor: (&workspace.Workspace{}).GetAccessor(request),
workflowAccessor: (&w.Workflow{}).GetAccessor(request),
@@ -52,8 +53,8 @@ func (a *collaborativeAreaMongoAccessor) DeleteOne(id string) (utils.DBObject, i
}
// UpdateOne updates a collaborative area in the database, given its ID and the new data, it automatically share to peers if the workspace is shared
func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
res, code, err := utils.GenericUpdateOne(set.(*CollaborativeArea).Trim(), id, a, &CollaborativeArea{})
func (a *collaborativeAreaMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
res, code, err := utils.GenericUpdateOne(set, id, a)
// a.deleteToPeer(res.(*CollaborativeArea)) // delete the collaborative area on the peer
a.sharedWorkflow(res.(*CollaborativeArea), id) // replace all shared workflows
a.sharedWorkspace(res.(*CollaborativeArea), id) // replace all collaborative areas (not shared worspace obj but workspace one)
@@ -63,14 +64,19 @@ func (a *collaborativeAreaMongoAccessor) UpdateOne(set utils.DBObject, id string
// StoreOne stores a collaborative area in the database, it automatically share to peers if the workspace is shared
func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
_, id := (&peer.Peer{}).IsMySelf() // get the local peer
data.(*CollaborativeArea).Clear(id) // set the creator
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})) // get the local peer
if err != nil || pp == nil {
return data, 404, err
}
data.(*CollaborativeArea).Clear(pp.GetID()) // set the creator
// retrieve or proper peer
if data.(*CollaborativeArea).CollaborativeAreaRule != nil {
data.(*CollaborativeArea).CollaborativeAreaRule = &CollaborativeAreaRule{}
}
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = id
d, code, err := utils.GenericStoreOne(data.(*CollaborativeArea).Trim(), a)
data.(*CollaborativeArea).CollaborativeAreaRule.Creator = pp.GetID()
d, code, err := utils.GenericStoreOne(data, a)
if code == 200 {
a.sharedWorkflow(d.(*CollaborativeArea), d.GetID()) // create all shared workflows
a.sharedWorkspace(d.(*CollaborativeArea), d.GetID()) // create all collaborative areas
@@ -79,19 +85,13 @@ func (a *collaborativeAreaMongoAccessor) StoreOne(data utils.DBObject) (utils.DB
return data, code, err
}
// CopyOne copies a CollaborativeArea in the database
func (a *collaborativeAreaMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return a.StoreOne(data)
}
func filterEnrich[T utils.ShallowDBObject](arr []string, isDrafted bool, a utils.Accessor) []T {
var new []T
res, code, _ := a.Search(&dbs.Filters{
Or: map[string][]dbs.Filter{
"abstractobject.id": {{Operator: dbs.IN.String(), Value: arr}},
},
}, "", isDrafted)
fmt.Println(res, arr, isDrafted, a)
}, "", isDrafted, 0, int64(len(arr)))
if code == 200 {
for _, r := range res {
new = append(new, r.(T))
@@ -125,23 +125,10 @@ func (a *collaborativeAreaMongoAccessor) enrich(sharedWorkspace *CollaborativeAr
return sharedWorkspace
}
func (a *collaborativeAreaMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*CollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return a.enrich(d.(*CollaborativeArea), false, a.Request), 200, nil
}, a)
}
func (a *collaborativeAreaMongoAccessor) LoadAll(isDrafted bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*CollaborativeArea](func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), isDrafted, a.Request)
}, isDrafted, a)
}
func (a *collaborativeAreaMongoAccessor) Search(filters *dbs.Filters, search string, isDrafted bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*CollaborativeArea](filters, search, (&CollaborativeArea{}).GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), isDrafted, a.Request)
}, isDrafted, a)
func (a *collaborativeAreaMongoAccessor) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return a.enrich(d.(*CollaborativeArea), isDraft, a.Request)
}
}
/*
@@ -153,7 +140,9 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
eld := eldest.(*CollaborativeArea)
if eld.Workspaces != nil { // update all your workspaces in the eldest by replacing shared ref by an empty string
for _, v := range eld.Workspaces {
a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: ""}, v)
a.workspaceAccessor.UpdateOne(map[string]interface{}{
"shared": "",
}, v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
continue
}
@@ -169,7 +158,10 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkspace(shared *CollaborativeAr
}
if shared.Workspaces != nil {
for _, v := range shared.Workspaces { // update all the collaborative areas
workspace, code, _ := a.workspaceAccessor.UpdateOne(&workspace.Workspace{Shared: shared.UUID}, v) // add the shared ref to workspace
workspace, code, _ := a.workspaceAccessor.UpdateOne(
map[string]interface{}{
"shared": shared.UUID,
}, v) // add the shared ref to workspace
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKSPACE] == nil {
continue
}
@@ -209,7 +201,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
} // kick the shared reference in your old shared workflow
n := &w.Workflow{}
n.Shared = new
a.workflowAccessor.UpdateOne(n, v)
a.workflowAccessor.UpdateOne(n.Serialize(n), v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
continue
}
@@ -231,7 +223,7 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
s := data.(*w.Workflow)
if !slices.Contains(s.Shared, id) {
s.Shared = append(s.Shared, id)
workflow, code, _ := a.workflowAccessor.UpdateOne(s, v)
workflow, code, _ := a.workflowAccessor.UpdateOne(s.Serialize(s), v)
if a.GetCaller() != nil || a.GetCaller().URLS == nil || a.GetCaller().URLS[tools.WORKFLOW] == nil {
continue
}
@@ -254,6 +246,8 @@ func (a *collaborativeAreaMongoAccessor) sharedWorkflow(shared *CollaborativeAre
// because you have no reference to the remote shared workflow
}
// TODO it's a Shared API Problem with OC-DISCOVERY
// sharedWorkspace is a function that shares the collaborative area to the peers
func (a *collaborativeAreaMongoAccessor) deleteToPeer(shared *CollaborativeArea) {
a.contactPeer(shared, tools.POST)
@@ -271,7 +265,9 @@ func (a *collaborativeAreaMongoAccessor) contactPeer(shared *CollaborativeArea,
paccess := (&peer.Peer{})
for k := range shared.AllowedPeersGroup {
if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: k}}).IsMySelf(); ok || (shared.IsSent && meth == tools.POST) || (!shared.IsSent && meth != tools.POST) {
if ok, _ := utils.IsMySelf(k, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok || (shared.IsSent && meth == tools.POST) || (!shared.IsSent && meth != tools.POST) {
continue
}
shared.IsSent = meth == tools.POST
+1 -1
View File
@@ -24,6 +24,6 @@ func (d *Rule) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request)
}
func (d *Rule) VerifyAuth(request *tools.APIRequest) bool {
func (d *Rule) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}
@@ -1,62 +1,23 @@
package rule
import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type ruleMongoAccessor struct {
utils.AbstractAccessor
utils.AbstractAccessor[*Rule]
}
// New creates a new instance of the ruleMongoAccessor
func NewAccessor(request *tools.APIRequest) *ruleMongoAccessor {
return &ruleMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
AbstractAccessor: utils.AbstractAccessor[*Rule]{
Logger: logs.CreateLogger(tools.RULE.String()), // Create a logger with the data type
Request: request,
Type: tools.RULE,
New: func() *Rule { return &Rule{} },
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *ruleMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *ruleMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &Rule{})
}
func (a *ruleMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *ruleMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *ruleMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Rule](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *ruleMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Rule](a.getExec(), isDraft, a)
}
func (a *ruleMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Rule](filters, search, (&Rule{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *ruleMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}
@@ -1,56 +1,22 @@
package shallow_collaborative_area
import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type shallowSharedWorkspaceMongoAccessor struct {
utils.AbstractAccessor
utils.AbstractAccessor[*ShallowCollaborativeArea]
}
func NewAccessor(request *tools.APIRequest) *shallowSharedWorkspaceMongoAccessor {
return &shallowSharedWorkspaceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
AbstractAccessor: utils.AbstractAccessor[*ShallowCollaborativeArea]{
Logger: logs.CreateLogger(tools.COLLABORATIVE_AREA.String()), // Create a logger with the data type
Request: request, // Set the caller
Type: tools.COLLABORATIVE_AREA,
New: func() *ShallowCollaborativeArea { return &ShallowCollaborativeArea{} },
},
}
}
func (a *shallowSharedWorkspaceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set.(*ShallowCollaborativeArea), id, a, &ShallowCollaborativeArea{})
}
func (a *shallowSharedWorkspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*ShallowCollaborativeArea), a)
}
func (a *shallowSharedWorkspaceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return a.StoreOne(data)
}
func (a *shallowSharedWorkspaceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*ShallowCollaborativeArea](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*ShallowCollaborativeArea](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, a)
}
func (a *shallowSharedWorkspaceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*ShallowCollaborativeArea](filters, search, (&ShallowCollaborativeArea{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, a)
}
+16
View File
@@ -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 {
+17
View File
@@ -0,0 +1,17 @@
package models
type MetricsSnapshot struct {
From string `json:"origin"`
Metrics []Metric `json:"metrics"`
}
type Metric struct {
Name string `json:"name"`
Value float64 `json:"value"`
Error error `json:"error"`
}
type MetricResume struct {
Delta float64 `json:"delta"`
LastValue float64 `json:"last_value"`
}
Regular → Executable
+19 -17
View File
@@ -7,36 +7,38 @@ import (
"cloud.o-forge.io/core/oc-lib/tools"
)
func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 {
near := float64(10000000000) // set a high value
func GetPlannerNearestStart(start time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF) float64 {
near := float64(-1) // unset sentinel
for _, items := range planned { // loop through the planned items
for _, priced := range items { // loop through the priced items
if priced.GetLocationStart() == nil { // if the start is nil,
continue // skip the iteration
}
newS := priced.GetLocationStart() // get the start
if newS.Sub(start).Seconds() < near { // if the difference between the start and the new start is less than the nearest start
near = newS.Sub(start).Seconds()
newS := priced.GetLocationStart() // get the start
diff := newS.Sub(start).Seconds() // get the difference
if near < 0 || diff < near { // if the difference is less than the nearest start
near = diff
}
}
}
if near < 0 {
return 0 // no items found, start at the given start time
}
return near
}
func GetPlannerLongestTime(end *time.Time, planned map[tools.DataType]map[string]pricing.PricedItemITF, request *tools.APIRequest) float64 {
if end == nil {
return -1
}
// 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] {
if priced.GetLocationEnd() == nil {
continue
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
}
newS := priced.GetLocationEnd()
if end == nil && longestTime < newS.Sub(*end).Seconds() {
longestTime = newS.Sub(*end).Seconds()
}
// get the nearest start from start var
}
return longestTime
}
+10 -2
View File
@@ -3,18 +3,26 @@ package pricing
import (
"time"
"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
IsPurchased() bool
IsPurchasable() bool
IsBooked() bool
GetQuantity() int
AddQuantity(amount int)
GetBookingMode() enum.BookingMode
GetCreatorID() string
SelectPricing() PricingProfileITF
GetLocationStart() *time.Time
SetLocationStart(start time.Time)
SetLocationEnd(end time.Time)
GetLocationEnd() *time.Time
GetExplicitDurationInS() float64
GetPrice() (float64, error)
GetPriceHT() (float64, error)
}
+52 -5
View File
@@ -5,9 +5,11 @@ import (
)
type PricingProfileITF interface {
GetPrice(quantity float64, val float64, start time.Time, end time.Time, params ...string) (float64, error)
IsPurchased() bool
IsBooked() bool
IsPurchasable() bool
GetPurchase() BuyingStrategy
GetOverrideStrategyValue() int
GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variation []*PricingVariation, params ...string) (float64, error)
}
type RefundType int
@@ -26,16 +28,61 @@ 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 {
return a.Pricing.BuyingStrategy == SUBSCRIPTION
}
func (a AccessPricingProfile[T]) IsPurchasable() bool {
return a.Pricing.BuyingStrategy == PERMANENT
}
func (a AccessPricingProfile[T]) GetPurchase() BuyingStrategy {
return a.Pricing.BuyingStrategy
}
func (a AccessPricingProfile[T]) GetPriceHT(quantity float64, val float64, start time.Time, end time.Time, variations []*PricingVariation, params ...string) (float64, error) {
return a.Pricing.GetPriceHT(quantity, val, start, &end, variations)
}
func (b *AccessPricingProfile[T]) GetOverrideStrategyValue() int {
return -1
}
func GetDefaultPricingProfile() PricingProfileITF {
return &AccessPricingProfile[TimePricingStrategy]{
Pricing: PricingStrategy[TimePricingStrategy]{
Price: 0,
Currency: "EUR",
BuyingStrategy: SUBSCRIPTION,
TimePricingStrategy: PER_SECOND,
},
}
}
type ExploitPrivilegeStrategy int
const (
+110 -16
View File
@@ -7,20 +7,65 @@ import (
"time"
)
type BillingStrategy int // BAM BAM
// should except... on
const (
BILL_ONCE BillingStrategy = iota // is a permanent buying ( predictible )
BILL_PER_WEEK
BILL_PER_MONTH
BILL_PER_YEAR
)
func (t BillingStrategy) IsBillingStrategyAllowed(bs int) (BillingStrategy, bool) {
switch t {
case BILL_ONCE:
return BILL_ONCE, bs == 0
case BILL_PER_WEEK:
case BILL_PER_MONTH:
case BILL_PER_YEAR:
return t, bs != 0
}
return t, false
}
func (t BillingStrategy) String() string {
return [...]string{"BILL_ONCE", "BILL_PER_WEEK", "BILL_PER_MONTH", "BILL_PER_YEAR"}[t]
}
func BillingStrategyList() []BillingStrategy {
return []BillingStrategy{BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR}
}
type BuyingStrategy int
// should except... on
const (
UNLIMITED BuyingStrategy = iota
SUBSCRIPTION
PAY_PER_USE
SUBSCRIPTION BuyingStrategy = iota // is a permanent buying ( predictible )
UNDEFINED_SUBSCRIPTION // a endless subscription ( unpredictible )
PERMANENT // a defined subscription ( predictible )
// PAY_PER_USE // per request. ( unpredictible )
)
func (t BuyingStrategy) String() string {
return [...]string{"UNLIMITED", "SUBSCRIPTION", "PAY PER USE"}[t]
return [...]string{"SUBSCRIPTION", "UNDEFINED_SUBSCRIPTION", "PERMANENT"}[t]
}
func (t BuyingStrategy) IsBillingStrategyAllowed(bs BillingStrategy) (BillingStrategy, bool) {
switch t {
case PERMANENT:
return BILL_ONCE, bs == BILL_ONCE
case UNDEFINED_SUBSCRIPTION:
return BILL_PER_MONTH, bs != BILL_ONCE
case SUBSCRIPTION:
/*case PAY_PER_USE:
return bs, true*/
}
return bs, false
}
func BuyingStrategyList() []BuyingStrategy {
return []BuyingStrategy{UNLIMITED, SUBSCRIPTION, PAY_PER_USE}
return []BuyingStrategy{SUBSCRIPTION, UNDEFINED_SUBSCRIPTION, PERMANENT}
}
type Strategy interface {
@@ -44,6 +89,10 @@ func (t TimePricingStrategy) String() string {
return [...]string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}[t]
}
func TimePricingStrategyListStr() []string {
return []string{"ONCE", "PER SECOND", "PER MINUTE", "PER HOUR", "PER DAY", "PER WEEK", "PER MONTH"}
}
func TimePricingStrategyList() []TimePricingStrategy {
return []TimePricingStrategy{ONCE, PER_SECOND, PER_MINUTE, PER_HOUR, PER_DAY, PER_WEEK, PER_MONTH}
}
@@ -62,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(1 * time.Hour)
} else {
fromDateDuration := float64(0)
if end != nil {
tEnd = *end
fromDateDuration = tEnd.Sub(start).Seconds()
}
fromDateDuration := tEnd.Sub(start).Seconds()
if fromAverageDuration > fromDateDuration {
return fromAverageDuration
}
@@ -77,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 {
@@ -101,19 +151,48 @@ func BookingEstimation(t TimePricingStrategy, price float64, locationDurationInS
return 0, errors.New("pricing strategy not found")
}
// may suppress in pricing strategy -> to set in map
type PricingStrategy[T Strategy] struct {
Price float64 `json:"price" bson:"price" default:"0"` // Price is the Price of the pricing
Currency string `json:"currency" bson:"currency" default:"USD"` // Currency is the currency of the pricing
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
}
func (p PricingStrategy[T]) GetPrice(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time) (float64, error) {
if p.BuyingStrategy == SUBSCRIPTION {
return BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
} else if p.BuyingStrategy == UNLIMITED {
func (p PricingStrategy[T]) GetPriceHT(amountOfData float64, bookingTimeDuration float64, start time.Time, end *time.Time, variations []*PricingVariation) (float64, error) {
switch p.BuyingStrategy {
case SUBSCRIPTION:
price, err := BookingEstimation(p.GetTimePricingStrategy(), p.Price*float64(amountOfData), bookingTimeDuration, start, end)
if err != nil {
return 0, err
}
if variations != nil {
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.Price, nil
case PERMANENT:
if variations != nil {
price := p.Price
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.Price, nil
}
if variations != nil {
price := p.Price
for _, v := range variations {
price = v.GetPriceHT(price)
}
return price, nil
}
return p.Price * float64(amountOfData), nil
}
@@ -129,3 +208,18 @@ func (p PricingStrategy[T]) GetTimePricingStrategy() TimePricingStrategy {
func (p PricingStrategy[T]) GetOverrideStrategy() T {
return p.OverrideStrategy
}
type PricingVariation struct {
Inflate bool `json:"inflate" bson:"price"` // Price is the Price of the pricing
Percentage float64 `json:"percent" bson:"percent"` // Currency is the currency of the pricing // Modulation is the modulation of the pricing
Priority int `json:"priority" bson:"priority"`
}
func (pv *PricingVariation) GetPriceHT(priceHT float64) float64 {
value := (priceHT * pv.Percentage) / 100
if pv.Inflate {
return priceHT + value
} else {
return priceHT - value
}
}
+129
View File
@@ -0,0 +1,129 @@
package pricing_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
)
type DummyStrategy int
func (d DummyStrategy) GetStrategy() string { return "DUMMY" }
func (d DummyStrategy) GetStrategyValue() int { return int(d) }
func TestBuyingStrategy_String(t *testing.T) {
assert.Equal(t, "PERMANENT", pricing.PERMANENT.String())
assert.Equal(t, "UNDEFINED_SUBSCRIPTION", pricing.UNDEFINED_SUBSCRIPTION.String())
assert.Equal(t, "SUBSCRIPTION", pricing.SUBSCRIPTION.String())
}
func TestBuyingStrategyList(t *testing.T) {
list := pricing.BuyingStrategyList()
assert.Equal(t, 3, len(list))
assert.Contains(t, list, pricing.SUBSCRIPTION)
}
func TestTimePricingStrategy_String(t *testing.T) {
assert.Equal(t, "ONCE", pricing.ONCE.String())
assert.Equal(t, "PER SECOND", pricing.PER_SECOND.String())
assert.Equal(t, "PER MONTH", pricing.PER_MONTH.String())
}
func TestTimePricingStrategyList(t *testing.T) {
list := pricing.TimePricingStrategyList()
assert.Equal(t, 7, len(list))
assert.Contains(t, list, pricing.PER_DAY)
}
func TestTimePricingStrategy_Methods(t *testing.T) {
ts := pricing.PER_MINUTE
assert.Equal(t, "PER_MINUTE", ts.GetStrategy())
assert.Equal(t, 2, ts.GetStrategyValue())
}
func Test_getAverageTimeInSecond_WithEnd(t *testing.T) {
start := time.Now()
end := start.Add(30 * time.Minute)
_, err := pricing.BookingEstimation(pricing.PER_MINUTE, 2.0, 1200, start, &end)
assert.NoError(t, err)
}
func Test_getAverageTimeInSecond_WithoutEnd(t *testing.T) {
start := time.Now()
// getAverageTimeInSecond is tested via BookingEstimation
price, err := pricing.BookingEstimation(pricing.PER_HOUR, 10.0, 100, start, nil)
assert.NoError(t, err)
assert.True(t, price > 0)
}
func TestBookingEstimation(t *testing.T) {
start := time.Now()
end := start.Add(10 * time.Minute)
strategies := map[pricing.TimePricingStrategy]float64{
pricing.ONCE: 50,
pricing.PER_HOUR: 10,
pricing.PER_MINUTE: 1,
pricing.PER_SECOND: 0.1,
pricing.PER_DAY: 100,
pricing.PER_WEEK: 500,
pricing.PER_MONTH: 2000,
}
for strategy, price := range strategies {
t.Run(strategy.String(), func(t *testing.T) {
cost, err := pricing.BookingEstimation(strategy, price, 3600, start, &end)
assert.NoError(t, err)
assert.True(t, cost >= 0)
})
}
_, err := pricing.BookingEstimation(999, 10, 3600, start, &end)
assert.Error(t, err)
}
func TestPricingStrategy_Getters(t *testing.T) {
ps := pricing.PricingStrategy[DummyStrategy]{
Price: 20,
Currency: "USD",
BuyingStrategy: pricing.SUBSCRIPTION,
TimePricingStrategy: pricing.PER_MINUTE,
OverrideStrategy: DummyStrategy(1),
}
assert.Equal(t, pricing.SUBSCRIPTION, ps.GetBuyingStrategy())
assert.Equal(t, pricing.PER_MINUTE, ps.GetTimePricingStrategy())
assert.Equal(t, DummyStrategy(1), ps.GetOverrideStrategy())
}
func TestPricingStrategy_GetPriceHT(t *testing.T) {
start := time.Now()
end := start.Add(5 * time.Minute)
// SUBSCRIPTION case
ps := pricing.PricingStrategy[DummyStrategy]{
Price: 5,
BuyingStrategy: pricing.SUBSCRIPTION,
TimePricingStrategy: pricing.PER_HOUR,
}
p, err := ps.GetPriceHT(2, 3600, start, &end, nil)
assert.NoError(t, err)
assert.True(t, p > 0)
// UNLIMITED case
ps.BuyingStrategy = pricing.PERMANENT
p, err = ps.GetPriceHT(10, 0, start, &end, nil)
assert.NoError(t, err)
assert.Equal(t, 5.0, p)
// UNDEFINED_SUBSCRIPTION case: price * quantity
ps.BuyingStrategy = pricing.UNDEFINED_SUBSCRIPTION
p, err = ps.GetPriceHT(3, 0, start, &end, nil)
assert.NoError(t, err)
assert.Equal(t, 15.0, p)
}
+45
View File
@@ -0,0 +1,45 @@
package execution_verification
import (
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* ExecutionVerification is a struct that represents a list of workflow executions
* Warning: No user can write (del, post, put) a workflow execution, it is only used by the system
* workflows generate their own executions
*/
type ExecutionVerification struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
WorkflowID string `json:"workflow_id" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
Payload string `json:"payload" bson:"payload,omitempty"`
IsVerified bool `json:"is_verified" bson:"is_verified,omitempty"`
Validate bool `json:"validate" bson:"validate,omitempty"`
}
func (ri *ExecutionVerification) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "wokflow":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW)
}
}
return ext
}
func (r *ExecutionVerification) StoreDraftDefault() {
r.IsDraft = false // TODO: TEMPORARY
}
func (d *ExecutionVerification) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *ExecutionVerification) VerifyAuth(callName string, request *tools.APIRequest) bool {
return true
}
@@ -0,0 +1,38 @@
package execution_verification
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type ExecutionVerificationMongoAccessor struct {
utils.AbstractAccessor[*ExecutionVerification]
shallow bool
}
func NewAccessor(request *tools.APIRequest) *ExecutionVerificationMongoAccessor {
return &ExecutionVerificationMongoAccessor{
shallow: false,
AbstractAccessor: utils.AbstractAccessor[*ExecutionVerification]{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
Request: request,
Type: tools.WORKFLOW_EXECUTION,
New: func() *ExecutionVerification { return &ExecutionVerification{} },
NotImplemented: []string{"DeleteOne", "StoreOne", "CopyOne"},
},
}
}
func (wfa *ExecutionVerificationMongoAccessor) StoreOne(set utils.DBObject) (utils.DBObject, int, error) {
set.(*ExecutionVerification).IsVerified = false
return utils.GenericStoreOne(set, wfa)
}
func (wfa *ExecutionVerificationMongoAccessor) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
set = map[string]interface{}{
"is_verified": true,
"validate": set["validate"],
}
return utils.GenericUpdateOne(set, id, wfa)
}
+13
View File
@@ -0,0 +1,13 @@
package live
import (
"cloud.o-forge.io/core/oc-lib/models/utils"
)
type LiveInterface interface {
utils.DBObject
IsCompatible(service map[string]interface{}) bool
GetMonitorPath() string
GetResourcesID() []string
SetResourcesID(string)
}
+93
View File
@@ -0,0 +1,93 @@
package live
import (
"slices"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/biter777/countries"
)
/*
* LiveDatacenter is a struct that represents a compute units in your datacenters
*/
type Credentials struct {
Login string `json:"login,omitempty" bson:"login,omitempty"`
Pass string `json:"password,omitempty" bson:"password,omitempty"`
Token string `json:"token,omitempty" bson:"token,omitempty"`
}
type Certs struct {
AuthorityCertificate string `json:"authority_certificate,omitempty" bson:"authority_certificate,omitempty"`
ClientCertificate string `json:"client_certificate,omitempty" bson:"client_certificate,omitempty"`
}
type LiveCerts struct {
Host string `json:"host,omitempty" bson:"host,omitempty"`
Port string `json:"port,omitempty" bson:"port,omitempty"`
Certificates *Certs `json:"certs,omitempty" bson:"certs,omitempty"`
Credentials *Credentials `json:"creds,omitempty" bson:"creds,omitempty"`
}
// TODO in the future multiple type of certs depending of infra type
type GeoPoint struct {
Latitude float64 `json:"latitude,omitempty" bson:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty" bson:"longitude,omitempty"`
}
type AbstractLive struct {
utils.AbstractObject
Certs LiveCerts `json:"certs,omitempty" bson:"certs,omitempty"`
MonitorPath string `json:"monitor_path,omitempty" bson:"monitor_path,omitempty"`
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
ResourcesID []string `json:"resources_id" bson:"resources_id"`
}
func (ri *AbstractLive) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "resource":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW_RESOURCE)
ext[t] = append(ext[t], tools.DATA_RESOURCE)
ext[t] = append(ext[t], tools.COMPUTE_RESOURCE)
ext[t] = append(ext[t], tools.STORAGE_RESOURCE)
ext[t] = append(ext[t], tools.PROCESSING_RESOURCE)
ext[t] = append(ext[t], tools.SERVICE_RESOURCE)
}
}
return ext
}
func (d *AbstractLive) GetMonitorPath() string {
return d.MonitorPath
}
func (d *AbstractLive) GetResourcesID() []string {
return d.ResourcesID
}
func (d *AbstractLive) SetResourcesID(resourcesid string) {
if slices.Contains(d.ResourcesID, resourcesid) {
d.ResourcesID = append(d.ResourcesID, resourcesid)
}
}
func (r *AbstractLive) GetResourceType() tools.DataType {
return tools.INVALID
}
func (r *AbstractLive) StoreDraftDefault() {
r.IsDraft = false
}
func (r *AbstractLive) CanDelete() bool {
return r.IsDraft // only draft ComputeUnits can be deleted
}
+47
View File
@@ -0,0 +1,47 @@
package live
import (
"fmt"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* LiveDatacenter is a struct that represents a compute units in your datacenters
*/
type ComputeNode struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Quantity int64 `json:"quantity" bson:"quantity" default:"1"`
RAM *models.RAM `bson:"ram,omitempty" json:"ram,omitempty"` // RAM is the RAM
CPUs map[string]int64 `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]int64 `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
}
type LiveDatacenter struct {
AbstractLive
StorageType enum.StorageType `bson:"storage_type" json:"storage_type" default:"-1"` // Type is the type of the storage
Acronym string `bson:"acronym,omitempty" json:"acronym,omitempty"` // Acronym is the acronym of the storage
Architecture string `json:"architecture,omitempty" bson:"architecture,omitempty"` // Architecture is the architecture
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
}
func (r *LiveDatacenter) IsCompatible(service map[string]interface{}) bool {
fmt.Println("COMPARE <", r.Infrastructure.Compare(service["infrastructure"]), "> AND <", service["architecture"], "> <", r.Architecture, ">")
return r.Infrastructure.Compare(service["infrastructure"]) && service["architecture"] == r.Architecture
}
func (d *LiveDatacenter) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveDatacenter](tools.LIVE_DATACENTER, request) // Create a new instance of the accessor
}
+57
View File
@@ -0,0 +1,57 @@
package live
import (
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type liveMongoAccessor[T LiveInterface] struct {
utils.AbstractAccessor[LiveInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the computeUnitsMongoAccessor
func NewAccessor[T LiveInterface](t tools.DataType, request *tools.APIRequest) *liveMongoAccessor[T] {
return &liveMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor[LiveInterface]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
Type: t,
New: func() LiveInterface {
switch t {
case tools.LIVE_DATACENTER:
return &LiveDatacenter{}
case tools.LIVE_STORAGE:
return &LiveStorage{}
case tools.LIVE_SERVICE:
return &LiveService{}
}
return &LiveDatacenter{}
},
NotImplemented: []string{"CopyOne"},
},
}
}
func (wfa *liveMongoAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit)
}
func (wfa *liveMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
if filters == nil && search == "*" {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa, offset, limit)
}
return utils.GenericSearch[T](filters, search, wfa.New().GetObjectFilters(search),
func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa, offset, limit)
}
func (a *liveMongoAccessor[T]) GetExec(isDraft bool) func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}
+46
View File
@@ -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"])
}
+38
View File
@@ -0,0 +1,38 @@
package live
import (
"fmt"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* LiveStorage is a struct that represents a compute units in your datacenters
*/
type LiveStorage struct {
AbstractLive
StorageType enum.StorageType `bson:"storage_type" json:"storage_type" default:"-1"`
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
Path string `bson:"path,omitempty" json:"path,omitempty"` // Path is the store folders in the source
Local bool `bson:"local" json:"local"`
SecurityLevel string `bson:"security_level,omitempty" json:"security_level,omitempty"`
SizeType enum.StorageSize `bson:"size_type" json:"size_type" default:"0"` // SizeType is the type of the storage size
SizeGB int64 `bson:"size,omitempty" json:"size,omitempty"` // Size is the size of the storage
Encryption bool `bson:"encryption,omitempty" json:"encryption,omitempty"` // Encryption is a flag that indicates if the storage is encrypted
Redundancy string `bson:"redundancy,omitempty" json:"redundancy,omitempty"` // Redundancy is the redundancy of the storage
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
}
func (r *LiveStorage) IsCompatible(service map[string]interface{}) bool {
fmt.Println("COMPARE <", r.StorageType.Compare(service["storage_type"]), ">")
return r.StorageType.Compare(service["storage_type"])
}
func (d *LiveStorage) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*LiveStorage](tools.LIVE_STORAGE, request) // Create a new instance of the accessor
}
+120
View File
@@ -0,0 +1,120 @@
package live_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- AbstractLive (via LiveDatacenter embedding) ----
func TestAbstractLive_StoreDraftDefault(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.StoreDraftDefault()
assert.True(t, dc.IsDraft)
}
func TestAbstractLive_CanDelete_Draft(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.IsDraft = true
assert.True(t, dc.CanDelete())
}
func TestAbstractLive_CanDelete_NonDraft(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.IsDraft = false
assert.False(t, dc.CanDelete())
}
func TestAbstractLive_GetMonitorPath(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.MonitorPath = "/metrics"
assert.Equal(t, "/metrics", dc.GetMonitorPath())
}
func TestAbstractLive_GetResourcesID_Empty(t *testing.T) {
dc := &live.LiveDatacenter{}
assert.Empty(t, dc.GetResourcesID())
}
func TestAbstractLive_SetResourcesID_Append(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.SetResourcesID("res-1")
assert.Equal(t, []string{"res-1"}, dc.GetResourcesID())
}
func TestAbstractLive_SetResourcesID_NoDuplication(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.SetResourcesID("res-1")
dc.SetResourcesID("res-1") // second call should not duplicate
assert.Len(t, dc.GetResourcesID(), 1)
}
func TestAbstractLive_SetResourcesID_MultipleDistinct(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.SetResourcesID("res-1")
dc.SetResourcesID("res-2")
assert.Len(t, dc.GetResourcesID(), 2)
}
func TestAbstractLive_GetResourceType(t *testing.T) {
dc := &live.LiveDatacenter{}
assert.Equal(t, tools.INVALID, dc.GetResourceType())
}
// ---- LiveDatacenter ----
func TestLiveDatacenter_GetAccessor(t *testing.T) {
dc := &live.LiveDatacenter{}
acc := dc.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestLiveDatacenter_GetAccessor_NilRequest(t *testing.T) {
dc := &live.LiveDatacenter{}
acc := dc.GetAccessor(nil)
assert.NotNil(t, acc)
}
func TestLiveDatacenter_IDAndName(t *testing.T) {
dc := &live.LiveDatacenter{}
dc.AbstractLive.AbstractObject = utils.AbstractObject{UUID: "dc-id", Name: "dc-name"}
assert.Equal(t, "dc-id", dc.GetID())
assert.Equal(t, "dc-name", dc.GetName())
}
// ---- LiveStorage ----
func TestLiveStorage_StoreDraftDefault(t *testing.T) {
s := &live.LiveStorage{}
s.StoreDraftDefault()
assert.True(t, s.IsDraft)
}
func TestLiveStorage_CanDelete_Draft(t *testing.T) {
s := &live.LiveStorage{}
s.IsDraft = true
assert.True(t, s.CanDelete())
}
func TestLiveStorage_CanDelete_NonDraft(t *testing.T) {
s := &live.LiveStorage{}
s.IsDraft = false
assert.False(t, s.CanDelete())
}
func TestLiveStorage_GetAccessor(t *testing.T) {
s := &live.LiveStorage{}
acc := s.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestLiveStorage_SetResourcesID_NoDuplication(t *testing.T) {
s := &live.LiveStorage{}
s.SetResourcesID("storage-1")
s.SetResourcesID("storage-1")
assert.Len(t, s.GetResourcesID(), 1)
}
+47 -22
View File
@@ -2,8 +2,17 @@ package models
import (
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/allowed_image"
"cloud.o-forge.io/core/oc-lib/models/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"
@@ -11,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"
@@ -21,39 +29,56 @@ import (
This package contains the models used in the application
It's used to create the models dynamically
*/
var models = map[string]func() utils.DBObject{
tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} },
tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} },
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
tools.STORAGE_RESOURCE.String(): func() utils.DBObject { return &resource.StorageResource{} },
tools.PROCESSING_RESOURCE.String(): func() utils.DBObject { return &resource.ProcessingResource{} },
tools.WORKFLOW.String(): func() utils.DBObject { return &w2.Workflow{} },
tools.WORKFLOW_EXECUTION.String(): func() utils.DBObject { return &workflow_execution.WorkflowExecution{} },
tools.WORKSPACE.String(): func() utils.DBObject { return &w3.Workspace{} },
tools.PEER.String(): func() utils.DBObject { return &peer.Peer{} },
tools.COLLABORATIVE_AREA.String(): func() utils.DBObject { return &collaborative_area.CollaborativeArea{} },
tools.RULE.String(): func() utils.DBObject { return &rule.Rule{} },
tools.BOOKING.String(): func() utils.DBObject { return &booking.Booking{} },
tools.WORKFLOW_HISTORY.String(): func() utils.DBObject { return &w2.WorkflowHistory{} },
tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} },
tools.ORDER.String(): func() utils.DBObject { return &order.Order{} },
tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} },
var ModelsCatalog = map[string]func() utils.DBObject{
tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} },
tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} },
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
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{} },
tools.WORKSPACE.String(): func() utils.DBObject { return &w3.Workspace{} },
tools.PEER.String(): func() utils.DBObject { return &peer.Peer{} },
tools.COLLABORATIVE_AREA.String(): func() utils.DBObject { return &collaborative_area.CollaborativeArea{} },
tools.RULE.String(): func() utils.DBObject { return &rule.Rule{} },
tools.BOOKING.String(): func() utils.DBObject { return &booking.Booking{} },
tools.WORKFLOW_HISTORY.String(): func() utils.DBObject { return &w2.WorkflowHistory{} },
tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} },
tools.ORDER.String(): func() utils.DBObject { return &order.Order{} },
tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} },
tools.LIVE_DATACENTER.String(): func() utils.DBObject { return &live.LiveDatacenter{} },
tools.LIVE_STORAGE.String(): func() utils.DBObject { return &live.LiveStorage{} },
tools.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
func Model(model int) utils.DBObject {
log := logs.GetLogger()
if _, ok := models[tools.FromInt(model)]; ok {
return models[tools.FromInt(model)]()
if model < 0 || model >= len(tools.Str) {
log.Error().Msg("Can't find model: index out of range")
return nil
}
log.Error().Msg("Can't find model " + tools.FromInt(model) + ".")
key := tools.FromInt(model)
if _, ok := ModelsCatalog[key]; ok {
return ModelsCatalog[key]()
}
log.Error().Msg("Can't find model " + key + ".")
return nil
}
// GetModelsNames returns the names of the models
func GetModelsNames() []string {
names := []string{}
for name := range models {
for name := range ModelsCatalog {
names = append(names, name)
}
return names
+13 -287
View File
@@ -1,19 +1,13 @@
package order
import (
"errors"
"fmt"
"sync"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
)
@@ -23,12 +17,17 @@ import (
type Order struct {
utils.AbstractObject
OrderBy string `json:"order_by" bson:"order_by" validate:"required"`
WorkflowID string `json:"workflow_id" bson:"workflow_id" validate:"required"`
WorkflowExecutionIDs []string `json:"workflow_execution_ids" bson:"workflow_execution_ids" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
Total float64 `json:"total" bson:"total" validate:"required"`
ExecutionsID string `json:"executions_id" bson:"executions_id" validate:"required"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
Purchases []*purchase_resource.PurchaseResource `json:"purchases" bson:"purchases"`
Bookings []*booking.Booking `json:"bookings" bson:"bookings"`
// Billing 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() {
@@ -46,287 +45,14 @@ func (r *Order) CanDelete() bool {
return r.IsDraft // only draft order can be deleted
}
func (o *Order) DraftOrder(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
// set the draft order from the model
if err := o.draftStoreFromModel(scheduler, request); err != nil {
return err
}
return nil
}
func (o *Order) Pay(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
if _, err := o.draftBookOrder(scheduler, request); err != nil {
return err
}
o.Status = enum.PENDING
_, code, err := o.GetAccessor(request).UpdateOne(o, o.GetID())
if code != 200 || err != nil {
return errors.New("could not update the order" + fmt.Sprintf("%v", err))
}
if err := o.pay(request); err != nil { // pay the order
return err
} else {
o.IsDraft = false
}
for _, exec := range scheduler.WorkflowExecution {
exec.IsDraft = false
_, code, err := utils.GenericUpdateOne(exec, exec.GetID(),
workflow_execution.NewAccessor(request), &workflow_execution.WorkflowExecution{})
if code != 200 || err != nil {
return errors.New("could not update the workflow execution" + fmt.Sprintf("%v", err))
}
}
_, code, err = o.GetAccessor(request).UpdateOne(o, o.GetID())
if code != 200 || err != nil {
return errors.New("could not update the order" + fmt.Sprintf("%v", err))
}
/*
TODO : TEMPORARY SET BOOKINGS TO UNDRAFT TO AVOID DELETION
BUT NEXT ONLY WHO IS PAYED WILL BE ALLOWED TO CHANGE IT
*/
return nil
}
func (o *Order) draftStoreFromModel(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) error {
if request == nil {
return errors.New("no request found")
}
fmt.Println("Drafting order", scheduler.Workflow)
if scheduler.Workflow == nil || scheduler.Workflow.Graph == nil { // if the workflow has no graph, return an error
return errors.New("no graph found")
}
o.SetName()
o.WorkflowID = scheduler.Workflow.GetID()
o.IsDraft = true
o.OrderBy = request.PeerID
o.WorkflowExecutionIDs = []string{} // create an array of ids
for _, exec := range scheduler.WorkflowExecution {
o.WorkflowExecutionIDs = append(o.WorkflowExecutionIDs, exec.GetID())
}
// set the name of the order
resourcesByPeer := map[string][]pricing.PricedItemITF{} // create a map of resources by peer
processings := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsProcessing, request) // get the processing items
datas := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsData, request) // get the data items
storages := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsStorage, request) // get the storage items
workflows := scheduler.Workflow.GetPricedItem(scheduler.Workflow.Graph.IsWorkflow, request) // get the workflow items
for _, items := range []map[string]pricing.PricedItemITF{processings, datas, storages, workflows} {
for _, item := range items {
if _, ok := resourcesByPeer[item.GetCreatorID()]; !ok {
resourcesByPeer[item.GetCreatorID()] = []pricing.PricedItemITF{}
}
resourcesByPeer[item.GetCreatorID()] = append(resourcesByPeer[item.GetCreatorID()], item)
}
}
for peerID, resources := range resourcesByPeer {
peerOrder := &PeerOrder{
Status: enum.DRAFTED,
PeerID: peerID,
}
peerOrder.GenerateID()
for _, resource := range resources {
peerOrder.AddItem(resource, len(resources)) // TODO SPECIALS REF ADDITIONALS NOTES
}
if o.SubOrders == nil {
o.SubOrders = map[string]*PeerOrder{}
}
o.SubOrders[peerOrder.GetID()] = peerOrder
}
// search an order with same user name and same session id
err := o.sumUpBill(request)
if err != nil {
return err
}
// should store the order
res, code, err := o.GetAccessor(request).Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"workflow_id": {{Operator: dbs.EQUAL.String(), Value: o.WorkflowID}},
"order_by": {{Operator: dbs.EQUAL.String(), Value: request.PeerID}},
},
}, "", o.IsDraft)
if code != 200 || err != nil {
return errors.New("could not search the order" + fmt.Sprintf("%v", err))
}
if len(res) > 0 {
_, code, err := utils.GenericUpdateOne(o, res[0].GetID(), o.GetAccessor(request), o)
if code != 200 || err != nil {
return errors.New("could not update the order" + fmt.Sprintf("%v", err))
}
} else {
_, code, err := utils.GenericStoreOne(o, o.GetAccessor(request))
if code != 200 || err != nil {
return errors.New("could not store the order" + fmt.Sprintf("%v", err))
}
}
return nil
}
func (o *Order) draftBookOrder(scheduler *workflow_execution.WorkflowSchedule, request *tools.APIRequest) ([]*booking.Booking, error) {
draftedBookings := []*booking.Booking{}
if request == nil {
return draftedBookings, errors.New("no request found")
}
for _, exec := range scheduler.WorkflowExecution {
_, priceds, _, err := scheduler.Workflow.Planify(exec.ExecDate, exec.EndDate, request)
if err != nil {
return draftedBookings, errors.New("could not planify the workflow" + fmt.Sprintf("%v", err))
}
bookings := exec.Book(scheduler.UUID, scheduler.Workflow.UUID, priceds)
for _, booking := range bookings {
_, err := (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "",
tools.BOOKING, tools.POST, booking.Serialize(booking), request.Caller)
if err != nil {
return draftedBookings, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err))
}
draftedBookings = append(draftedBookings, booking)
}
}
return draftedBookings, nil
}
func (o *Order) Quantity() int {
return len(o.WorkflowExecutionIDs)
return len(o.Purchases) + len(o.Purchases)
}
func (d *Order) SetName() {
func (d *Order) SetName(_ string) {
d.Name = d.UUID + "_order_" + "_" + time.Now().UTC().Format("2006-01-02T15:04:05")
}
func (d *Order) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
func (d *Order) sumUpBill(request *tools.APIRequest) error {
for _, b := range d.SubOrders {
err := b.SumUpBill(request)
if err != nil {
return err
}
d.Total += b.Total
}
return nil
}
// TO FINISH
func (d *Order) pay(request *tools.APIRequest) error {
responses := make(chan *PeerOrder, len(d.SubOrders))
var wg *sync.WaitGroup
wg.Add(len(d.SubOrders))
for _, b := range d.SubOrders {
go b.Pay(request, responses, wg)
}
wg.Wait()
errs := ""
gotAnUnpaid := false
count := 0
for range responses {
res := <-responses
count++
if res != nil {
if res.Error != "" {
errs += res.Error
}
if res.Status != enum.PAID {
gotAnUnpaid = true
}
d.Status = enum.PARTIAL
d.SubOrders[res.GetID()] = res
if count == len(d.SubOrders) && !gotAnUnpaid {
d.Status = enum.PAID
}
}
}
if errs != "" {
return errors.New(errs)
}
return nil
}
type PeerOrder struct {
utils.AbstractObject
Error string `json:"error,omitempty" bson:"error,omitempty"`
PeerID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
Status enum.CompletionStatus `json:"status" bson:"status" default:"0"`
BillingAddress string `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
Items []*PeerItemOrder `json:"items,omitempty" bson:"items,omitempty"`
Total float64 `json:"total,omitempty" bson:"total,omitempty"`
}
func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
d.Status = enum.PENDING
go func() {
// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER ON RETURN UPDATE STATUS
d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
if d.Status == enum.PAID {
for _, b := range d.Items {
if !b.Item.IsPurchased() {
continue
}
accessor := purchase_resource.NewAccessor(request)
accessor.StoreOne(&purchase_resource.PurchaseResource{
ResourceID: b.Item.GetID(),
ResourceType: b.Item.GetType(),
EndDate: b.Item.GetLocationEnd(),
})
}
}
if d.Status != enum.PENDING {
response <- d
}
wg.Done()
}()
}
func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
for _, b := range d.Items {
tot, err := b.GetPrice(request) // missing something
if err != nil {
return err
}
d.Total += tot
}
return nil
}
func (d *PeerOrder) AddItem(item pricing.PricedItemITF, quantity int) {
d.Items = append(d.Items, &PeerItemOrder{
Quantity: quantity,
Item: item,
})
}
func (d *PeerOrder) SetName() {
d.Name = d.UUID + "_order_" + d.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05")
}
type PeerItemOrder struct {
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
Purchase purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
Item pricing.PricedItemITF `json:"item,omitempty" bson:"item,omitempty"`
}
func (d *PeerItemOrder) GetPrice(request *tools.APIRequest) (float64, error) {
accessor := purchase_resource.NewAccessor(request)
search, code, _ := accessor.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"resource_id": {{Operator: dbs.EQUAL.String(), Value: d.Item.GetID()}},
},
}, "", d.Purchase.IsDraft)
if code == 200 && len(search) > 0 {
for _, s := range search {
if s.(*purchase_resource.PurchaseResource).EndDate == nil || time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
return 0, nil
}
}
}
p, err := d.Item.GetPrice()
if err != nil {
return 0, err
}
return p * float64(d.Quantity), nil
}
// WTF HOW TO SELECT THE RIGHT PRICE ???
// SHOULD SET A BUYING STATUS WHEN PAYMENT IS VALIDATED
+7 -47
View File
@@ -1,64 +1,24 @@
package order
import (
"errors"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type orderMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
utils.AbstractAccessor[*Order] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the orderMongoAccessor
func NewAccessor(request *tools.APIRequest) *orderMongoAccessor {
return &orderMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type
Request: request,
Type: tools.ORDER,
AbstractAccessor: utils.AbstractAccessor[*Order]{
Logger: logs.CreateLogger(tools.ORDER.String()), // Create a logger with the data type
Request: request,
Type: tools.ORDER,
New: func() *Order { return &Order{} },
NotImplemented: []string{"CopyOne"},
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *orderMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *orderMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &Order{})
}
func (a *orderMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("Not implemented")
}
func (a *orderMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("Not implemented")
}
func (a *orderMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Order](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *orderMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Order](a.getExec(), isDraft, a)
}
func (a *orderMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Order](filters, search, (&Order{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *orderMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}
+92
View File
@@ -0,0 +1,92 @@
package order_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- Order model ----
func TestOrder_StoreDraftDefault(t *testing.T) {
o := &order.Order{}
o.StoreDraftDefault()
assert.True(t, o.IsDraft)
}
func TestOrder_CanDelete_Draft(t *testing.T) {
o := &order.Order{}
o.IsDraft = true
assert.True(t, o.CanDelete())
}
func TestOrder_CanDelete_NonDraft(t *testing.T) {
o := &order.Order{}
o.IsDraft = false
assert.False(t, o.CanDelete())
}
func TestOrder_CanUpdate_StatusChange_NonDraft(t *testing.T) {
o := &order.Order{Status: enum.PENDING}
o.IsDraft = false
set := &order.Order{Status: enum.PAID}
ok, returned := o.CanUpdate(set)
assert.True(t, ok)
assert.Equal(t, enum.PAID, returned.(*order.Order).Status)
}
func TestOrder_CanUpdate_SameStatus_NonDraft(t *testing.T) {
o := &order.Order{Status: enum.PENDING}
o.IsDraft = false
set := &order.Order{Status: enum.PENDING}
ok, _ := o.CanUpdate(set)
// !r.IsDraft && r.Status == set.Status → first branch false → returns r.IsDraft = false
assert.False(t, ok)
}
func TestOrder_CanUpdate_Draft(t *testing.T) {
o := &order.Order{Status: enum.PENDING}
o.IsDraft = true
set := &order.Order{Status: enum.PAID}
ok, _ := o.CanUpdate(set)
// !r.IsDraft = false → first branch false → returns r.IsDraft = true
assert.True(t, ok)
}
func TestOrder_Quantity(t *testing.T) {
o := &order.Order{
Purchases: []*purchase_resource.PurchaseResource{{}, {}},
}
// Quantity = len(Purchases) + len(Purchases) (note: there is a bug in source: uses Purchases twice)
assert.Equal(t, 4, o.Quantity())
}
func TestOrder_Quantity_Empty(t *testing.T) {
o := &order.Order{}
assert.Equal(t, 0, o.Quantity())
}
func TestOrder_SetName(t *testing.T) {
o := &order.Order{}
o.UUID = "order-uuid"
o.SetName("ignored")
// Name is generated from UUID, not from the argument
assert.Contains(t, o.Name, "order-uuid")
assert.Contains(t, o.Name, "_order_")
}
func TestOrder_GetAccessor(t *testing.T) {
o := &order.Order{}
acc := o.GetAccessor(&tools.APIRequest{})
assert.NotNil(t, acc)
}
func TestOrder_GetAccessor_NilRequest(t *testing.T) {
o := &order.Order{}
acc := o.GetAccessor(nil)
assert.NotNil(t, acc)
}
+152 -25
View File
@@ -2,45 +2,186 @@ 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"
)
// now write a go enum for the state partner with self, blacklist, partner
type PeerState int
type PeerPerm int
const (
NONE PeerState = iota
READ PeerRelation = iota
WRITE
MONITOR
)
type PeerRelation int
const (
NONE PeerRelation = iota
SELF
PARTNER
BLACKLIST
PENDING_PARTNER
MASTER
NANO
PENDING_NANO
PENDING_MASTER
)
func (m PeerState) String() string {
return [...]string{"NONE", "SELF", "PARTNER", "BLACKLIST"}[m]
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
}
}
return -1
}
func (m PeerState) EnumIndex() int {
func (m PeerRelation) Path() string {
return path[m]
}
func (m PeerRelation) String() string {
return strings.ToUpper(path[m])
}
func (m PeerRelation) EnumIndex() int {
return int(m)
}
// 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
Url string `json:"url" bson:"url" validate:"required"` // Url is the URL of the peer (base64url)
IsNano bool `json:"is_nano" bson:"is_nano"`
PeerPerms []PeerPerm `json:"peer_perms" bson:"peer_perms"`
RelationLastChangeDate time.Time `json:"relation_last_change_date" bson:"relation_last_change_date"`
RelationLastChangeUser string `json:"relation_last_change_user" bson:"relation_last_change_user"`
Verify bool `json:"verify" bson:"verify"`
OrganizationID string `json:"organization_id" bson:"organization_id"`
PeerID string `json:"peer_id" bson:"peer_id" validate:"required"`
APIUrl string `json:"api_url" bson:"api_url" validate:"required"` // Url is the URL of the peer (base64url)
StreamAddress string `json:"stream_address" bson:"stream_address" validate:"required"` // Url is the URL of the peer (base64url)
NATSAddress string `json:"nats_address" bson:"nats_address" validate:"required"`
WalletAddress string `json:"wallet_address" bson:"wallet_address" validate:"required"` // WalletAddress is the wallet address of the peer
PublicKey string `json:"public_key" bson:"public_key" validate:"required"` // PublicKey is the public key of the peer
State PeerState `json:"state" bson:"state" default:"0"`
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 (ao *Peer) VerifyAuth(request *tools.APIRequest) bool {
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
@@ -66,18 +207,8 @@ func (ao *Peer) RemoveExecution(exec PeerExecution) {
ao.FailedExecution = new
}
// IsMySelf checks if the peer is the local peer
func (p *Peer) IsMySelf() (bool, string) {
d, code, err := NewAccessor(nil).Search(nil, SELF.String(), p.IsDraft)
if code != 200 || err != nil || len(d) == 0 {
return false, ""
}
id := d[0].GetID()
return p.UUID == id, id
}
// LaunchPeerExecution launches an execution on a peer
func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
func (p *Peer) LaunchPeerExecution(peerID string, dataID string, dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (map[string]interface{}, error) {
p.UUID = peerID
return cache.LaunchPeerExecution(peerID, dataID, dt, method, body, caller) // Launch the execution on the peer through the cache
}
@@ -85,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
}
+28 -49
View File
@@ -28,90 +28,69 @@ type PeerCache struct {
}
// urlFormat formats the URL of the peer with the data type API function
func (p *PeerCache) urlFormat(hostUrl string, dt tools.DataType) string {
// localhost is replaced by the local peer URL
// because localhost must collide on a web request security protocol
/*localhost := ""
if strings.Contains(hostUrl, "localhost") {
localhost = "localhost"
}
if strings.Contains(hostUrl, "127.0.0.1") {
localhost = "127.0.0.1"
}
if localhost != "" {
r := regexp.MustCompile("(" + localhost + ":[0-9]+)")
t := r.FindString(hostUrl)
if t != "" {
hostUrl = strings.Replace(hostUrl, t, dt.API()+":8080/oc", -1)
} else {
hostUrl = strings.ReplaceAll(hostUrl, localhost, dt.API()+":8080/oc")
}
} else {*/
hostUrl = hostUrl + "/" + strings.ReplaceAll(dt.API(), "oc-", "")
//}
fmt.Println("Contacting", hostUrl)
return hostUrl
func urlFormat(hostUrl string, dt tools.DataType) string {
return hostUrl + "/" + strings.ReplaceAll(dt.String(), "oc-", "")
}
// checkPeerStatus checks the status of a peer
func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool) {
func CheckPeerStatus(peerID string, appName string) (*Peer, bool) {
api := tools.API{}
access := NewShallowAccessor()
res, code, _ := access.LoadOne(peerID) // Load the peer from db
if code != 200 { // no peer no party
return nil, false
}
url := p.urlFormat(res.(*Peer).Url, tools.PEER) + "/status" // Format the URL
url := urlFormat(res.(*Peer).APIUrl, tools.PEER) + "/status" // Format the URL
state, services := api.CheckRemotePeer(url)
res.(*Peer).ServicesState = services // Update the services states of the peer
access.UpdateOne(res, peerID) // Update the peer in the db
access.UpdateOne(res.Serialize(res), peerID) // Update the peer in the db
return res.(*Peer), state != tools.DEAD && services[appName] == 0 // Return the peer and its status
}
// LaunchPeerExecution launches an execution on a peer
// The method contacts the path described by : peer.Url + datatype path (from enums) + replacement of id by dataID
func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
fmt.Println("Launching peer execution on", caller.URLS, dt, method)
methods := caller.URLS[dt] // Get the methods url of the data type
dt tools.DataType, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (map[string]interface{}, error) {
fmt.Println("Launching peer execution on", caller.GetUrls(), dt, method)
methods := caller.GetUrls()[dt] // Get the methods url of the data type
if m, ok := methods[method]; !ok || m == "" {
return nil, errors.New("Requested method " + method.String() + " not declared in HTTPCaller")
return map[string]interface{}{}, errors.New("Requested method " + method.String() + " not declared in HTTPCaller")
}
path := methods[method] // Get the path corresponding to the action we want to execute
path = strings.ReplaceAll(path, ":id", dataID) // Replace the id in the path in case of a DELETE / UPDATE method (it's a standard naming in OC)
url := ""
// Check the status of the peer
if mypeer, ok := p.checkPeerStatus(peerID, dt.API()); !ok && mypeer != nil {
if mypeer, ok := CheckPeerStatus(peerID, dt.String()); !ok && mypeer != nil {
// If the peer is not reachable, add the execution to the failed executions list
pexec := &PeerExecution{
Method: method.String(),
Url: p.urlFormat((mypeer.Url), dt) + path, // the url is constitued of : host URL + resource path + action path (ex : mypeer.com/datacenter/resourcetype/path/to/action)
Url: urlFormat((mypeer.APIUrl), dt) + path, // the url is constitued of : host URL + resource path + action path (ex : mypeer.com/datacenter/resourcetype/path/to/action)
Body: body,
DataType: dt.EnumIndex(),
DataID: dataID,
}
mypeer.AddExecution(*pexec)
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
return nil, errors.New("peer is " + peerID + " not reachable")
NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db
return map[string]interface{}{}, errors.New("peer is " + peerID + " not reachable")
} else {
if mypeer == nil {
return nil, errors.New("peer " + peerID + " not found")
return map[string]interface{}{}, errors.New("peer " + peerID + " not found")
}
// If the peer is reachable, launch the execution
url = p.urlFormat((mypeer.Url), dt) + path // Format the URL
tmp := mypeer.FailedExecution // Get the failed executions list
mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
for _, v := range tmp { // Retry the failed executions
go p.exec(v.Url, tools.ToMethod(v.Method), v.Body, caller)
url = urlFormat((mypeer.APIUrl), dt) + path // Format the URL
tmp := mypeer.FailedExecution // Get the failed executions list
mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list
NewShallowAccessor().UpdateOne(mypeer.Serialize(mypeer), peerID) // Update the peer in the db
for _, v := range tmp { // Retry the failed executions
go p.Exec(v.Url, tools.ToMethod(v.Method), v.Body, caller)
}
}
return nil, p.exec(url, method, body, caller) // Execute the method
return p.Exec(url, method, body, caller) // Execute the method
}
// exec executes the method on the peer
func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) error {
func (p *PeerCache) Exec(url string, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (map[string]interface{}, error) {
var b []byte
var err error
if method == tools.POST { // Execute the POST method if it's a POST method
@@ -123,16 +102,16 @@ func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, call
if method == tools.DELETE { // Execute the DELETE method if it's a DELETE method
b, err = caller.CallDelete(url, "")
}
if err != nil {
return err
}
var m map[string]interface{}
if err != nil {
return m, err
}
err = json.Unmarshal(b, &m)
if err != nil {
return err
return m, err
}
if e, ok := m["error"]; ok && e != "<nil>" && e != "" { // Check if there is an error in the response
return errors.New(fmt.Sprintf("%v", m["error"]))
return m, errors.New(fmt.Sprintf("%v", m["error"]))
}
return nil
return m, nil
}
+35 -46
View File
@@ -10,80 +10,68 @@ import (
)
type peerMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
overrideAuth bool
utils.AbstractAccessor[*Peer] // AbstractAccessor contains the basic fields of an accessor (model, caller)
OverrideAuth bool
}
// New creates a new instance of the peerMongoAccessor
func NewShallowAccessor() *peerMongoAccessor {
return &peerMongoAccessor{
overrideAuth: true,
AbstractAccessor: utils.AbstractAccessor{
OverrideAuth: true,
AbstractAccessor: utils.AbstractAccessor[*Peer]{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Type: tools.PEER,
New: func() *Peer { return &Peer{} },
},
}
}
func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
return &peerMongoAccessor{
overrideAuth: false,
AbstractAccessor: utils.AbstractAccessor{
OverrideAuth: false,
AbstractAccessor: utils.AbstractAccessor[*Peer]{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Request: request,
Type: tools.PEER,
New: func() *Peer { return &Peer{} },
},
}
}
func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool {
return !wfa.overrideAuth
return !wfa.OverrideAuth
}
/*
TODO : organization_ID est un peer_ID duquel on se revendique faire parti.
Ca implique une clé d'organisation + une demande d'intégration.
Slave-Master IOT
*/
func (dca *peerMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
pp, _ := utils.GetMySelf(NewAccessor(&tools.APIRequest{Admin: true}))
if data != nil {
d := data.(*Peer)
if pp != nil && d.OrganizationID != "" && d.OrganizationID == pp.(*Peer).OrganizationID {
d.Relation = PARTNER // defaulting on partner if same organization.
}
}
return utils.GenericStoreOne(data, dca)
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (wfa *peerMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, wfa)
}
func (wfa *peerMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set.(*Peer), id, wfa, &Peer{})
}
func (wfa *peerMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*Peer), wfa)
}
func (wfa *peerMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, wfa)
}
func (dca *peerMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*Peer](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, dca)
}
func (wfa *peerMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*Peer](func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (wfa *peerMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Peer](filters, search, wfa.getDefaultFilter(search),
func(d utils.DBObject) utils.ShallowDBObject {
return d
}, isDraft, wfa)
}
func (a *peerMongoAccessor) getDefaultFilter(search string) *dbs.Filters {
func (a *peerMongoAccessor) GetObjectFilters(search string) *dbs.Filters {
if i, err := strconv.Atoi(search); err == nil {
m := map[string][]dbs.Filter{ // search by name if no filters are provided
"relation": {{Operator: dbs.EQUAL.String(), Value: i}},
}
if i == PARTNER.EnumIndex() {
m["verify"] = []dbs.Filter{{Operator: dbs.EQUAL.String(), Value: false}}
}
return &dbs.Filters{
Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"state": {{Operator: dbs.EQUAL.String(), Value: i}},
},
Or: m,
}
} else {
if search == "*" {
@@ -93,6 +81,7 @@ func (a *peerMongoAccessor) getDefaultFilter(search string) *dbs.Filters {
Or: map[string][]dbs.Filter{ // search by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
"url": {{Operator: dbs.LIKE.String(), Value: search}},
"peer_id": {{Operator: dbs.LIKE.String(), Value: search}},
},
}
}
+109
View File
@@ -0,0 +1,109 @@
package peer_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- PeerRelation ----
func TestPeerRelation_String(t *testing.T) {
assert.Equal(t, "UNKNOWN", peer.NONE.String())
assert.Equal(t, "SELF", peer.SELF.String())
assert.Equal(t, "PARTNER", peer.PARTNER.String())
assert.Equal(t, "BLACKLIST", peer.BLACKLIST.String())
}
func TestPeerRelation_Path(t *testing.T) {
assert.Equal(t, "unknown", peer.NONE.Path())
assert.Equal(t, "self", peer.SELF.Path())
assert.Equal(t, "partner", peer.PARTNER.Path())
assert.Equal(t, "blacklist", peer.BLACKLIST.Path())
}
func TestPeerRelation_EnumIndex(t *testing.T) {
assert.Equal(t, 0, peer.NONE.EnumIndex())
assert.Equal(t, 1, peer.SELF.EnumIndex())
assert.Equal(t, 2, peer.PARTNER.EnumIndex())
assert.Equal(t, 3, peer.BLACKLIST.EnumIndex())
assert.Equal(t, 4, peer.PENDING_PARTNER.EnumIndex())
}
func TestGetRelationPath(t *testing.T) {
assert.Equal(t, 1, peer.GetRelationPath("self"))
assert.Equal(t, 2, peer.GetRelationPath("partner"))
assert.Equal(t, 3, peer.GetRelationPath("blacklist"))
assert.Equal(t, -1, peer.GetRelationPath("nonexistent"))
}
// ---- Peer model ----
func TestPeer_VerifyAuth(t *testing.T) {
p := &peer.Peer{}
assert.True(t, p.VerifyAuth("get", nil))
assert.True(t, p.VerifyAuth("delete", &tools.APIRequest{}))
}
func TestPeer_CanDelete(t *testing.T) {
p := &peer.Peer{}
assert.False(t, p.CanDelete())
}
func TestPeer_GetAccessor(t *testing.T) {
p := &peer.Peer{}
req := &tools.APIRequest{}
acc := p.GetAccessor(req)
assert.NotNil(t, acc)
}
func TestPeer_AddExecution_Deduplication(t *testing.T) {
p := &peer.Peer{}
exec := peer.PeerExecution{Method: "POST", Url: "http://peer/data", Body: "body1"}
p.AddExecution(exec)
assert.Len(t, p.FailedExecution, 1)
// Second add of same execution should not duplicate
p.AddExecution(exec)
assert.Len(t, p.FailedExecution, 1)
// Different execution should be added
exec2 := peer.PeerExecution{Method: "GET", Url: "http://peer/data", Body: nil}
p.AddExecution(exec2)
assert.Len(t, p.FailedExecution, 2)
}
func TestPeer_RemoveExecution(t *testing.T) {
p := &peer.Peer{}
exec1 := peer.PeerExecution{Method: "POST", Url: "http://peer/a", Body: nil}
exec2 := peer.PeerExecution{Method: "DELETE", Url: "http://peer/b", Body: nil}
p.AddExecution(exec1)
p.AddExecution(exec2)
assert.Len(t, p.FailedExecution, 2)
p.RemoveExecution(exec1)
assert.Len(t, p.FailedExecution, 1)
assert.Equal(t, exec2, p.FailedExecution[0])
}
func TestPeer_RemoveExecution_NotFound(t *testing.T) {
p := &peer.Peer{}
exec := peer.PeerExecution{Method: "POST", Url: "http://peer/x", Body: nil}
p.AddExecution(exec)
other := peer.PeerExecution{Method: "DELETE", Url: "http://other/x", Body: nil}
p.RemoveExecution(other)
assert.Len(t, p.FailedExecution, 1) // unchanged
}
func TestPeer_RemoveExecution_Empty(t *testing.T) {
p := &peer.Peer{}
// Should not panic on empty list
exec := peer.PeerExecution{Method: "GET", Url: "http://peer/x", Body: nil}
p.RemoveExecution(exec)
assert.Empty(t, p.FailedExecution)
}
Regular → Executable
+94 -43
View File
@@ -2,14 +2,17 @@ package resources
import (
"errors"
"fmt"
"strings"
"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"
)
/*
@@ -23,47 +26,63 @@ 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 {
return tools.COMPUTE_RESOURCE.String()
}
func (abs *ComputeResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *ComputeResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.COMPUTE_RESOURCE {
return nil
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Compute")
}
p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
priced := p.(*PricedResource)
p, err := ConvertToPricedResource[*ComputeResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*ComputeResourcePricingProfile])
return &PricedComputeResource{
PricedResource: *priced,
}
}
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
}, nil
}
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]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
type ComputeResourcePartnership struct {
ResourcePartnerShip[*ComputeResourcePricingProfile]
MaxAllowedCPUsCores map[string]int `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MinGaranteedCPUsCores map[string]float64 `json:"garanteed_cpus,omitempty" bson:"garanteed_cpus,omitempty"`
MinGaranteedGPUsMemoryGB map[string]float64 `json:"garanteed_gpus,omitempty" bson:"garanteed_gpus,omitempty"`
MinGaranteedRAMSize float64 `json:"garanteed_ram,omitempty" bson:"garanteed_ram,omitempty"`
MaxAllowedCPUsCores map[string]float64 `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MaxAllowedGPUsMemoryGB map[string]float64 `json:"allowed_gpus,omitempty" bson:"allowed_gpus,omitempty"`
MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"`
}
@@ -76,8 +95,20 @@ type ComputeResourcePricingProfile struct {
RAMPrice float64 `json:"ram_price" bson:"ram_price" default:"-1"` // RAMPrice is the price of the RAM
}
func (p *ComputeResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *ComputeResourcePricingProfile) IsPurchasable() bool {
fmt.Println("Buying", p.Pricing.BuyingStrategy)
return p.Pricing.BuyingStrategy == pricing.PERMANENT
}
func (p *ComputeResourcePricingProfile) GetPurchase() pricing.BuyingStrategy {
return p.Pricing.BuyingStrategy
}
func (p *ComputeResourcePricingProfile) IsBooked() bool {
if p.Pricing.BuyingStrategy == pricing.PERMANENT {
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
}
func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
@@ -86,17 +117,20 @@ func (p *ComputeResourcePricingProfile) GetOverrideStrategyValue() int {
// NOT A PROPER QUANTITY
// amountOfData is the number of CPUs, GPUs or RAM dependings on the params
func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, params ...string) (float64, error) {
func (p *ComputeResourcePricingProfile) GetPriceHT(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, variation []*pricing.PricingVariation, params ...string) (float64, error) {
if len(params) < 1 {
return 0, errors.New("params must be set")
}
pp := float64(0)
model := params[1]
model := ""
if len(params) > 1 {
model = params[1]
}
if strings.Contains(params[0], "cpus") && len(params) > 1 {
if _, ok := p.CPUsPrices[model]; ok {
p.Pricing.Price = p.CPUsPrices[model]
}
r, err := p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
r, err := p.Pricing.GetPriceHT(amountOfData, explicitDuration, start, &end, variation)
if err != nil {
return 0, err
}
@@ -107,7 +141,7 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
if _, ok := p.GPUsPrices[model]; ok {
p.Pricing.Price = p.GPUsPrices[model]
}
r, err := p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
r, err := p.Pricing.GetPriceHT(amountOfData, explicitDuration, start, &end, variation)
if err != nil {
return 0, err
}
@@ -117,7 +151,7 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
if p.RAMPrice >= 0 {
p.Pricing.Price = p.RAMPrice
}
r, err := p.Pricing.GetPrice(float64(amountOfData), explicitDuration, start, &end)
r, err := p.Pricing.GetPriceHT(float64(amountOfData), explicitDuration, start, &end, variation)
if err != nil {
return 0, err
}
@@ -127,44 +161,61 @@ func (p *ComputeResourcePricingProfile) GetPrice(amountOfData float64, explicitD
}
type PricedComputeResource struct {
PricedResource
PricedResource[*ComputeResourcePricingProfile]
CPUsLocated map[string]float64 `json:"cpus_in_use" bson:"cpus_in_use"` // CPUsInUse is the list of CPUs in use
GPUsLocated map[string]float64 `json:"gpus_in_use" bson:"gpus_in_use"` // GPUsInUse is the list of GPUs in use
RAMLocated float64 `json:"ram_in_use" bson:"ram_in_use"` // RAMInUse is the RAM in use
}
func (r *PricedComputeResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ComputeResourcePricingProfile{}
}
}
func (r *PricedComputeResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedComputeResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedComputeResource) GetType() tools.DataType {
return tools.COMPUTE_RESOURCE
}
func (r *PricedComputeResource) GetPrice() (float64, error) {
func (r *PricedComputeResource) GetPriceHT() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
price := float64(0)
for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} {
for model, amountOfData := range l {
cpus, err := pricing.GetPrice(float64(amountOfData), r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd, "cpus", model)
cpus, err := pricing.GetPriceHT(float64(amountOfData),
r.BookingConfiguration.ExplicitBookingDurationS, *r.BookingConfiguration.UsageStart,
*r.BookingConfiguration.UsageEnd, r.Variations, "cpus", model)
if err != nil {
return 0, err
}
price += cpus
}
}
ram, err := pricing.GetPrice(r.RAMLocated, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd, "ram")
ram, err := pricing.GetPriceHT(r.RAMLocated, r.BookingConfiguration.ExplicitBookingDurationS,
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations, "ram")
if err != nil {
return 0, err
}
Regular → Executable
+71 -47
View File
@@ -2,13 +2,13 @@ package resources
import (
"errors"
"fmt"
"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/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
)
/*
@@ -30,46 +30,50 @@ 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 (abs *DataResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
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
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
}
p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
priced := p.(*PricedResource)
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*DataResourcePricingProfile])
return &PricedDataResource{
PricedResource: *priced,
}
}, nil
}
type 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 (ri *DataInstance) StoreDraftDefault() {
found := false
for _, p := range ri.ResourceInstance.Env {
if p.Attr == "source" {
found = true
break
}
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
return &DataInstance{
ResourceInstance: ResourceInstance[*DataResourcePartnership]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
if !found {
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
Attr: "source",
Value: ri.Source,
Readonly: true,
})
}
ri.ResourceInstance.StoreDraftDefault()
}
type DataResourcePartnership struct {
@@ -82,7 +86,7 @@ type DataResourcePartnership struct {
type DataResourcePricingStrategy int
const (
PER_DOWNLOAD DataResourcePricingStrategy = iota
PER_DOWNLOAD DataResourcePricingStrategy = iota + 7
PER_TB_DOWNLOADED
PER_GB_DOWNLOADED
PER_MB_DOWNLOADED
@@ -90,7 +94,9 @@ const (
)
func (t DataResourcePricingStrategy) String() string {
return [...]string{"PER DOWNLOAD", "PER TB DOWNLOADED", "PER GB DOWNLOADED", "PER MB DOWNLOADED", "PER KB DOWNLOADED"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DOWNLOAD", "PER TB DOWNLOADED", "PER GB DOWNLOADED", "PER MB DOWNLOADED", "PER KB DOWNLOADED"}...)
return l[t]
}
func DataResourcePricingStrategyList() []DataResourcePricingStrategy {
@@ -102,7 +108,9 @@ func ToDataResourcePricingStrategy(i int) DataResourcePricingStrategy {
}
func (t DataResourcePricingStrategy) GetStrategy() string {
return [...]string{"PER_DOWNLOAD", "PER_GB", "PER_MB", "PER_KB"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}...)
return l[t]
}
func (t DataResourcePricingStrategy) GetStrategyValue() int {
@@ -133,40 +141,54 @@ func (p *DataResourcePricingProfile) GetOverrideStrategyValue() int {
return p.Pricing.OverrideStrategy.GetStrategyValue()
}
func (p *DataResourcePricingProfile) GetPrice(amountOfData float64, explicitDuration float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.GetPrice(amountOfData, explicitDuration, start, &end)
func (p *DataResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy == pricing.PERMANENT
}
func (p *DataResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *DataResourcePricingProfile) IsBooked() bool {
// TODO WHAT ABOUT PAY PER USE... it's a complicate CASE
return p.Pricing.BuyingStrategy != pricing.PERMANENT
}
type PricedDataResource struct {
PricedResource
PricedResource[*DataResourcePricingProfile]
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
}
func (r *PricedDataResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &DataResourcePricingProfile{}
}
}
func (r *PricedDataResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedDataResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedDataResource) GetType() tools.DataType {
return tools.DATA_RESOURCE
}
func (r *PricedDataResource) GetPrice() (float64, error) {
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
func (r *PricedDataResource) GetPriceHT() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
var err error
amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 {
@@ -175,5 +197,7 @@ func (r *PricedDataResource) GetPrice() (float64, error) {
return 0, err
}
}
return pricing.GetPrice(amountOfData, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd)
return pricing.GetPriceHT(amountOfData, r.BookingConfiguration.ExplicitBookingDurationS,
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
}
+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
}
Regular → Executable
+28 -6
View File
@@ -1,34 +1,56 @@
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"
)
type PricedResourceITF interface {
pricing.PricedItemITF
}
type ResourceInterface interface {
utils.DBObject
Trim()
ConvertToPricedResource(t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF
FilterPeer(peerID string) *dbs.Filters
GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation
ConvertToPricedResource(t tools.DataType, a *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, b *int, request *tools.APIRequest) (pricing.PricedItemITF, error)
GetType() string
GetSelectedInstance() utils.DBObject
ClearEnv() utils.DBObject
SetAllowedInstances(request *tools.APIRequest)
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)
}
type ResourcePartnerITF interface {
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() map[string][]string
ClearPeerGroups()
GetProfile(buying *int, strategy *int) pricing.PricingProfileITF
FilterPartnership(peerID string)
}
Regular → Executable
+21
View File
@@ -11,12 +11,19 @@ type ResourceSet struct {
Processings []string `bson:"processings,omitempty" json:"processings,omitempty"`
Computes []string `bson:"computes,omitempty" json:"computes,omitempty"`
Workflows []string `bson:"workflows,omitempty" json:"workflows,omitempty"`
NativeTool []string `bson:"native,omitempty" json:"native,omitempty"`
Services []string `bson:"services,omitempty" json:"services,omitempty"`
Dynamics []string `bson:"dynamics,omitempty" json:"dynamics,omitempty"`
// DynamicResources 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() {
@@ -25,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) {
@@ -35,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)
@@ -50,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 {
@@ -62,4 +80,7 @@ type ItemResource struct {
Storage *StorageResource `bson:"storage,omitempty" json:"storage,omitempty"`
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"`
}
+81
View File
@@ -0,0 +1,81 @@
package resources
import (
"encoding/json"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources/native_tools"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
/*
* NativeT ools is a struct that represents Native Functionnality of OPENCLOUD
*/
type NativeTool struct {
AbstractResource
Kind int `json:"kind" bson:"kind" validate:"required"`
Params map[string]interface{}
}
func (d *NativeTool) SetName(name string) {
d.Name = name
}
func (d *NativeTool) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*NativeTool](tools.NATIVE_TOOL, request)
}
func (r *NativeTool) AddInstances(instance ResourceInstanceITF) {
}
func (r *NativeTool) GetType() string {
return tools.NATIVE_TOOL.String()
}
func (d *NativeTool) ClearEnv() utils.DBObject {
return d
}
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
// WorkflowResource has no instances, but still carries AEs that must be
// filtered before the resource is returned to a non-owner, non-admin peer.
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
if request != nil {
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
}
func (w *NativeTool) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
return &PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{
Name: w.Name,
Logo: w.Logo,
ResourceID: w.UUID,
ResourceType: t,
Quantity: 1,
CreatorID: w.CreatorID,
}, nil
}
func (r *NativeTool) GetSelectedInstance(selected *int) ResourceInstanceITF {
return nil
}
func InitNative() {
for _, kind := range []native_tools.NativeToolsEnum{native_tools.WORKFLOW_EVENT} {
newNative := &NativeTool{}
access := newNative.GetAccessor(&tools.APIRequest{Admin: true})
l, _, err := access.Search(nil, kind.String(), false, 0, 10)
if err != nil || len(l) == 0 {
newNative.Name = kind.String()
newNative.Kind = int(kind)
b, _ := json.Marshal(kind.Params())
var m map[string]interface{}
json.Unmarshal(b, &m)
newNative.Params = m
access.StoreOne(newNative)
}
}
}
+23
View File
@@ -0,0 +1,23 @@
package native_tools
type NativeToolsEnum int
const (
WORKFLOW_EVENT NativeToolsEnum = iota
)
var Params = [...]interface{}{
WorkflowEventParams{},
}
var Str = [...]string{
"WORKFLOW_EVENT",
}
func (d NativeToolsEnum) Params() interface{} {
return Str[d]
}
func (d NativeToolsEnum) String() string {
return Str[d]
}
@@ -0,0 +1,22 @@
package native_tools
import (
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
)
/*
* Workflow Event is a struct that represents a native functiunality.
*/
type WorkflowEventParams struct {
WorkflowResourceID string `json:"workflow_execution_id" bson:"workflow_execution_id" validate:"required"`
ManualCheck bool `json:"manual_check" bson:"manual_check"`
Input string `json:"input" bson:"input"`
Payload string `json:"payload" bson:"payload"`
BookingMode *booking.BookingMode `json:"booking_mode" bson:"booking_mode"`
}
func (wep *WorkflowEventParams) GetBuyingStrategy() pricing.BillingStrategy {
return pricing.BILL_ONCE
}
+122 -60
View File
@@ -2,92 +2,154 @@ package resources
import (
"errors"
"fmt"
"time"
"cloud.o-forge.io/core/oc-lib/models/booking"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/tools"
)
type PricedResource struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
PricingProfiles []pricing.PricingProfileITF `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
SelectedPricing *pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
type BookingConfiguration struct {
ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
Mode booking.BookingMode `json:"mode,omitempty" bson:"mode,omitempty"`
}
func (abs *PricedResource) GetID() string {
type PricedResource[T pricing.PricingProfileITF] struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
SelectedPricing T `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"`
Quantity int `json:"quantity,omitempty" bson:"quantity,omitempty"`
BookingConfiguration *BookingConfiguration `json:"booking_configuration,omitempty" bson:"booking_configuration,omitempty"`
Variations []*pricing.PricingVariation `json:"pricing_variations" bson:"pricing_variations"`
CreatorID string `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
ResourceID string `json:"resource_id,omitempty" bson:"resource_id,omitempty"`
InstanceID string `json:"instance_id,omitempty" bson:"resource_id,omitempty"`
ResourceType tools.DataType `json:"resource_type,omitempty" bson:"resource_type,omitempty"`
}
func (abs *PricedResource[T]) GetQuantity() int {
return abs.Quantity
}
func (abs *PricedResource[T]) AddQuantity(amount int) {
abs.Quantity += amount
}
func (abs *PricedResource[T]) SelectPricing() pricing.PricingProfileITF {
return abs.SelectedPricing
}
func (abs *PricedResource[T]) GetID() string {
return abs.ResourceID
}
func (abs *PricedResource) GetType() tools.DataType {
func (abs *PricedResource[T]) GetName() string {
return abs.Name
}
func (abs *PricedResource[T]) GetInstanceID() string {
return abs.InstanceID
}
func (abs *PricedResource[T]) GetType() tools.DataType {
return abs.ResourceType
}
func (abs *PricedResource) GetCreatorID() string {
func (abs *PricedResource[T]) GetCreatorID() string {
return abs.CreatorID
}
func (abs *PricedResource) IsPurchased() bool {
if abs.SelectedPricing == nil {
// IsPurchasable and IsBooked fall back to false when SelectedPricing is a nil interface.
// Concrete types (PricedComputeResource, etc.) override these and guarantee non-nil pricing.
func (abs *PricedResource[T]) IsPurchasable() bool {
if any(abs.SelectedPricing) == nil {
return false
}
return (*abs.SelectedPricing).IsPurchased()
return abs.SelectedPricing.IsPurchasable()
}
func (abs *PricedResource) GetLocationEnd() *time.Time {
return abs.UsageEnd
}
func (abs *PricedResource) GetLocationStart() *time.Time {
return abs.UsageStart
}
func (abs *PricedResource) SetLocationStart(start time.Time) {
abs.UsageStart = &start
}
func (abs *PricedResource) SetLocationEnd(end time.Time) {
abs.UsageEnd = &end
}
func (abs *PricedResource) GetExplicitDurationInS() float64 {
if abs.ExplicitBookingDurationS == 0 {
if abs.UsageEnd == nil && abs.UsageStart == nil {
return time.Duration(1 * time.Hour).Seconds()
}
if abs.UsageEnd == nil {
add := abs.UsageStart.Add(time.Duration(1 * time.Hour))
abs.UsageEnd = &add
}
return abs.UsageEnd.Sub(*abs.UsageStart).Seconds()
func (abs *PricedResource[T]) IsBooked() bool {
if any(abs.SelectedPricing) == nil {
return false
}
return abs.ExplicitBookingDurationS
return abs.SelectedPricing.IsBooked()
}
func (r *PricedResource) GetPrice() (float64, error) {
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
func (abs *PricedResource[T]) GetLocationEnd() *time.Time {
if abs.BookingConfiguration == nil {
return nil
}
return abs.BookingConfiguration.UsageEnd
}
func (abs *PricedResource[T]) GetLocationStart() *time.Time {
if abs.BookingConfiguration == nil {
return nil
}
return abs.BookingConfiguration.UsageStart
}
func (abs *PricedResource[T]) SetLocationStart(start time.Time) {
if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{}
}
abs.BookingConfiguration.UsageStart = &start
}
func (abs *PricedResource[T]) SetLocationEnd(end time.Time) {
if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{}
}
abs.BookingConfiguration.UsageEnd = &end
}
func (abs *PricedResource[T]) GetBookingMode() booking.BookingMode {
if abs.BookingConfiguration == nil {
return booking.WHEN_POSSIBLE
}
return abs.BookingConfiguration.Mode
}
func (abs *PricedResource[T]) GetExplicitDurationInS() float64 {
if abs.BookingConfiguration == nil {
abs.BookingConfiguration = &BookingConfiguration{}
}
if abs.BookingConfiguration.ExplicitBookingDurationS == 0 {
if abs.BookingConfiguration.UsageEnd == nil && abs.BookingConfiguration.UsageStart == nil {
return (5 * time.Minute).Seconds()
}
if abs.BookingConfiguration.UsageEnd == nil {
add := abs.BookingConfiguration.UsageStart.Add(5 * time.Minute)
abs.BookingConfiguration.UsageEnd = &add
}
d := abs.BookingConfiguration.UsageEnd.Sub(*abs.BookingConfiguration.UsageStart).Seconds()
if d <= 0 {
return (5 * time.Minute).Seconds()
}
return d
}
return abs.BookingConfiguration.ExplicitBookingDurationS
}
func (r *PricedResource[T]) GetPriceHT() (float64, error) {
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add
}
pricing := *r.SelectedPricing
return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd)
if any(r.SelectedPricing) == nil {
return 0, errors.New("pricing profile must be set for resource " + r.ResourceID)
}
pricing := r.SelectedPricing
return pricing.GetPriceHT(1, 0, *r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
}
Regular → Executable
+78 -26
View File
@@ -1,6 +1,7 @@
package resources
import (
"errors"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
@@ -8,6 +9,7 @@ import (
"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"
"github.com/google/uuid"
)
type ProcessingUsage struct {
@@ -26,30 +28,66 @@ type ProcessingUsage struct {
*/
type ProcessingResource struct {
AbstractInstanciatedResource[*ProcessingInstance]
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 {
return &ProcessingInstance{
ResourceInstance: ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
}
type ProcessingResourcePartnership struct {
ResourcePartnerShip[*ProcessingResourcePricingProfile]
}
type PricedProcessingResource struct {
PricedResource
IsService bool
PricedResource[*ProcessingResourcePricingProfile]
}
func (r *PricedProcessingResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &ProcessingResourcePricingProfile{}
}
}
func (r *PricedProcessingResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedProcessingResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedProcessingResource) GetPriceHT() (float64, error) {
r.ensurePricing()
return r.PricedResource.GetPriceHT()
}
func (r *PricedProcessingResource) GetType() tools.DataType {
@@ -57,30 +95,44 @@ func (r *PricedProcessingResource) GetType() tools.DataType {
}
func (a *PricedProcessingResource) GetExplicitDurationInS() float64 {
if a.ExplicitBookingDurationS == 0 {
if a.IsService || a.UsageStart == nil {
if a.IsService {
return -1
}
return time.Duration(1 * time.Hour).Seconds()
}
return a.UsageEnd.Sub(*a.UsageStart).Seconds()
if a.BookingConfiguration == nil {
a.BookingConfiguration = &BookingConfiguration{}
}
return a.ExplicitBookingDurationS
if a.BookingConfiguration.ExplicitBookingDurationS == 0 {
if a.BookingConfiguration.UsageStart == nil {
return (5 * time.Minute).Seconds()
}
return a.BookingConfiguration.UsageEnd.Sub(*a.BookingConfiguration.UsageStart).Seconds()
}
return a.BookingConfiguration.ExplicitBookingDurationS
}
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) {
if t != tools.PROCESSING_RESOURCE {
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Data")
}
p, err := ConvertToPricedResource[*DataResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*DataResourcePricingProfile])
return &PricedDataResource{
PricedResource: *priced,
}, nil
}
type ProcessingResourcePricingProfile struct {
pricing.AccessPricingProfile[pricing.TimePricingStrategy] // AccessPricingProfile is the pricing profile of a data it means that we can access the data for an amount of time
}
func (p *ProcessingResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *ProcessingResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy == pricing.PERMANENT
}
func (p *ProcessingResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.GetPrice(amountOfData, val, start, &end)
func (p *ProcessingResourcePricingProfile) IsBooked() bool {
return p.Pricing.BuyingStrategy != pricing.PERMANENT
}
@@ -9,11 +9,50 @@ import (
type PurchaseResource struct {
utils.AbstractObject
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
FromNano string `json:"from_nano,omitempty" bson:"priced_item,omitempty"`
DestPeerID string `json:"dest_peer_id" bson:"dest_peer_id"`
PricedItem map[string]interface{} `json:"priced_item,omitempty" bson:"priced_item,omitempty" validate:"required"`
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions
EndDate *time.Time `json:"end_buying_date,omitempty" bson:"end_buying_date,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id" validate:"required"`
InstanceID string `json:"instance_id,omitempty" bson:"instance_id,omitempty" validate:"required"` // could be a Compute or a Storage
ResourceType tools.DataType `json:"resource_type" bson:"resource_type" validate:"required"`
// Authorization: identifies who created this draft and the Check session it belongs to.
SchedulerPeerID string `json:"scheduler_peer_id,omitempty" bson:"scheduler_peer_id,omitempty"`
}
func (ri *PurchaseResource) Extend(typ ...string) map[string][]tools.DataType {
ext := ri.AbstractObject.Extend(typ...)
for _, t := range typ {
switch t {
case "dest_peer", "scheduler_peer":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.PEER)
case "execution":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW_EXECUTION)
case "resource":
if _, ok := ext[t]; !ok {
ext[t] = []tools.DataType{}
}
ext[t] = append(ext[t], tools.WORKFLOW_RESOURCE)
ext[t] = append(ext[t], tools.DATA_RESOURCE)
ext[t] = append(ext[t], tools.COMPUTE_RESOURCE)
ext[t] = append(ext[t], tools.STORAGE_RESOURCE)
ext[t] = append(ext[t], tools.PROCESSING_RESOURCE)
ext[t] = append(ext[t], tools.SERVICE_RESOURCE)
}
}
return ext
}
func (d *PurchaseResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}
@@ -3,23 +3,23 @@ package purchase_resource
import (
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
type purchaseResourceMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
type PurchaseResourceMongoAccessor struct {
utils.AbstractAccessor[*PurchaseResource] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
return &purchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
return &PurchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor[*PurchaseResource]{
Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type
Request: request,
Type: tools.PURCHASE_RESOURCE,
New: func() *PurchaseResource { return &PurchaseResource{} },
},
}
}
@@ -27,46 +27,26 @@ func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *purchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *purchaseResourceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
return utils.GenericUpdateOne(set, id, a, &PurchaseResource{})
}
func (a *purchaseResourceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *purchaseResourceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data, a)
}
func (a *purchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*PurchaseResource](id, func(d utils.DBObject) (utils.DBObject, int, error) {
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
}, a)
}
func (a *purchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*PurchaseResource](a.getExec(), isDraft, a)
}
func (a *purchaseResourceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*PurchaseResource](filters, search, (&PurchaseResource{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *purchaseResourceMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
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
}
@@ -0,0 +1,56 @@
package purchase_resource_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
)
func TestGetAccessor(t *testing.T) {
req := &tools.APIRequest{}
res := &purchase_resource.PurchaseResource{}
accessor := res.GetAccessor(req)
assert.NotNil(t, accessor)
assert.Equal(t, tools.PURCHASE_RESOURCE, accessor.(*purchase_resource.PurchaseResourceMongoAccessor).Type)
}
func TestCanUpdate(t *testing.T) {
set := &purchase_resource.PurchaseResource{ResourceID: "id"}
r := &purchase_resource.PurchaseResource{
AbstractObject: utils.AbstractObject{IsDraft: true},
}
can, updated := r.CanUpdate(set)
assert.True(t, can)
assert.Equal(t, set, updated)
r.IsDraft = false
can, _ = r.CanUpdate(set)
assert.False(t, can)
}
func TestCanDelete(t *testing.T) {
now := time.Now().UTC()
past := now.Add(-5 * time.Minute)
future := now.Add(5 * time.Minute)
t.Run("nil EndDate", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{}
assert.False(t, r.CanDelete())
})
t.Run("EndDate in past", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{EndDate: &past}
assert.True(t, r.CanDelete())
})
t.Run("EndDate in future", func(t *testing.T) {
r := &purchase_resource.PurchaseResource{EndDate: &future}
assert.False(t, r.CanDelete())
})
}
Regular → Executable
+452 -92
View File
@@ -1,86 +1,230 @@
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)
Type string `json:"type,omitempty" bson:"type,omitempty"` // Type is the type of the resource
Logo string `json:"logo,omitempty" bson:"logo,omitempty" validate:"required"` // Logo is the logo of the resource
Description string `json:"description,omitempty" bson:"description,omitempty"` // Description is the description of the resource
ShortDescription string `json:"short_description,omitempty" bson:"short_description,omitempty" validate:"required"` // ShortDescription is the short description of the resource
Owners []utils.Owner `json:"owners,omitempty" bson:"owners,omitempty"` // Owners is the list of owners of the resource
UsageRestrictions string `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
SelectedInstanceIndex *int `json:"selected_instance_index,omitempty" bson:"selected_instance_index,omitempty"` // SelectedInstance is the selected instance
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"`
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 (r *AbstractResource) GetSelectedInstance() utils.DBObject {
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{
booking.PLANNED: {
Percentage: 0,
}, booking.WHEN_POSSIBLE: {
Percentage: 0,
},
}
}
return r.AllowedBookingModes
}
func (r *AbstractResource) GetType() string {
return tools.INVALID.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) {
if r.IsDraft != set.IsDrafted() && set.IsDrafted() {
return true, set // only state can be updated
}
return r.IsDraft != set.IsDrafted() && set.IsDrafted(), set
}
func (r *AbstractResource) CanDelete() bool {
return r.IsDraft // only draft bookings can be deleted
fmt.Println("IsDrafted", r.IsDraft, set.IsDrafted())
return r.IsDraft || set.IsDrafted(), set
}
type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
AbstractResource // AbstractResource contains the basic fields of an object (id, name)
Instances []T `json:"instances,omitempty" bson:"instances,omitempty"` // Bill is the bill of the resource // Bill is the bill of the resource
AbstractResource // AbstractResource contains the basic fields of an object (id, name)
Instances []T `json:"instances,omitempty" bson:"instances,omitempty"`
}
func (abs *AbstractInstanciatedResource[T]) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *AbstractInstanciatedResource[T]) AddInstances(instance ResourceInstanceITF) {
abs.Instances = append(abs.Instances, instance.(T))
}
func ConvertToPricedResource[T pricing.PricingProfileITF](t tools.DataType,
selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int,
selectedBookingModeIndex *int, abs ResourceInterface, request *tools.APIRequest) (pricing.PricedItemITF, error) {
instances := map[string]string{}
profiles := []pricing.PricingProfileITF{}
for _, instance := range abs.Instances {
instances[instance.GetID()] = instance.GetName()
profiles = instance.GetPricingsProfiles(request.PeerID, request.Groups)
var profile pricing.PricingProfileITF
var inst ResourceInstanceITF
if t := abs.GetSelectedInstance(selectedInstance); t != nil {
inst = t
instances[t.GetID()] = t.GetName()
profile = t.GetProfile(request.PeerID, selectedPartnership, selectedBuyingStrategy, selectedStrategy)
} else {
for i, instance := range abs.SetAllowedInstances(request) { // TODO why it crush before ?
if i == 0 {
inst = instance
}
instances[instance.GetID()] = instance.GetName()
profiles := instance.GetPricingsProfiles(request.PeerID, request.Groups)
if len(profiles) > 0 {
profile = profiles[0]
break
}
}
}
return &PricedResource{
Name: abs.Name,
Logo: abs.Logo,
ResourceID: abs.UUID,
ResourceType: t,
InstancesRefs: instances,
PricingProfiles: profiles,
CreatorID: abs.CreatorID,
if profile == nil {
/*if ok, _ := utils.IsMySelf(request.PeerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {*/
profile = pricing.GetDefaultPricingProfile()
/*} else {
return nil, errors.New("no pricing profile found")
}*/
}
variations := []*pricing.PricingVariation{}
if selectedBookingModeIndex != nil && abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)] != nil {
variations = append(variations, abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)])
}
// Seed the booking configuration with the instance's historical average duration
// so GetExplicitDurationInS() returns a realistic default out of the box.
var bc *BookingConfiguration
if inst != nil {
if avg := inst.GetAverageDurationS(); avg > 0 {
bc = &BookingConfiguration{ExplicitBookingDurationS: avg}
}
}
instanceID := ""
if inst != nil {
instanceID = inst.GetID()
}
selectedPricing, _ := profile.(T)
return &PricedResource[T]{
Name: abs.GetName(),
ResourceID: abs.GetID(),
InstanceID: instanceID,
ResourceType: t,
Quantity: 1,
InstancesRefs: instances,
SelectedPricing: selectedPricing,
Variations: variations,
CreatorID: abs.GetCreatorID(),
BookingConfiguration: bc,
}, nil
}
func (abs *AbstractInstanciatedResource[T]) ClearEnv() utils.DBObject {
for _, instance := range abs.Instances {
instance.ClearEnv()
}
return abs
}
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() utils.DBObject {
if r.SelectedInstanceIndex != nil && len(r.Instances) > *r.SelectedInstanceIndex {
return r.Instances[*r.SelectedInstanceIndex]
func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) ResourceInstanceITF {
if selected != nil && len(r.Instances) > *selected {
return r.Instances[*selected]
}
if len(r.Instances) > 0 {
return r.Instances[0]
@@ -88,41 +232,53 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() utils.DBObject {
return nil
}
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest) {
if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" {
return
}
abs.Instances = verifyAuthAction[T](abs.Instances, request)
}
func (d *AbstractInstanciatedResource[T]) Trim() {
d.Type = d.GetType()
if ok, _ := (&peer.Peer{AbstractObject: utils.AbstractObject{UUID: d.CreatorID}}).IsMySelf(); !ok {
for _, instance := range d.Instances {
instance.ClearPeerGroups()
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
}
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(request *tools.APIRequest) bool {
return len(verifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request)
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(callName string, request *tools.APIRequest) bool {
return len(VerifyAuthAction(abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(callName, request)
}
func verifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T {
func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest, instanceID ...string) []T {
instances := []T{}
for _, instance := range baseInstance {
if len(instanceID) > 0 && !slices.Contains(instanceID, instance.GetID()) {
continue
}
// Structurally peerless instances (no creator, no partnerships, non-empty Ref)
// are freely accessible by any requester.
if instance.IsPeerless() {
instances = append(instances, instance)
continue
}
_, peerGroups := instance.GetPeerGroups()
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...
continue
}
for _, grp := range grps {
if slices.Contains(request.Groups, grp) {
instance.FilterInstance(request.PeerID)
instances = append(instances, instance)
}
}
@@ -132,48 +288,116 @@ 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 Credentials struct {
Login string `json:"login,omitempty" bson:"login,omitempty"`
Pass string `json:"password,omitempty" bson:"password,omitempty"`
}
type ResourceInstance[T ResourcePartnerITF] struct {
utils.AbstractObject
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
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"`
Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
AverageDurationS float64 `json:"average_duration_s,omitempty" bson:"average_duration_s,omitempty"`
AverageDurationSamples int `json:"average_duration_samples,omitempty" bson:"average_duration_samples,omitempty"`
}
func (ri *ResourceInstance[T]) ClearEnv() {
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
// TODO should kicks all selection
func NewInstance[T ResourcePartnerITF](name string) *ResourceInstance[T] {
return &ResourceInstance[T]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
Partnerships: []T{},
}
}
func (ri *ResourceInstance[T]) GetOrigin() OriginMeta {
return ri.Origin
}
// IsPeerless returns true when the instance has no owning peer and a non-empty
// registry reference. This is derived from structural invariants — NOT from the
// self-declared Origin.Type field — to prevent auth bypass via metadata manipulation:
//
// CreatorID == "" ∧ len(Partnerships) == 0 ∧ Origin.Ref != ""
func (ri *ResourceInstance[T]) IsPeerless() bool {
return ri.CreatorID == "" && len(ri.Partnerships) == 0 && ri.Origin.Ref != ""
}
func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
partnerships := []T{}
for _, p := range ri.Partnerships {
if p.GetPeerGroups()["*"] != nil || p.GetPeerGroups()[peerID] != nil {
p.FilterPartnership(peerID)
partnerships = append(partnerships, p)
}
}
ri.Partnerships = partnerships
}
func (ri *ResourceInstance[T]) GetProfile(peerID string, partnershipIndex *int, buyingIndex *int, strategyIndex *int) pricing.PricingProfileITF {
if ri.IsPeerless() {
return pricing.GetDefaultPricingProfile()
}
if partnershipIndex != nil && len(ri.Partnerships) > *partnershipIndex {
prts := ri.Partnerships[*partnershipIndex]
return prts.GetProfile(buyingIndex, strategyIndex)
}
if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {
return pricing.GetDefaultPricingProfile()
}
return nil
}
func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
if ri.IsPeerless() {
return []pricing.PricingProfileITF{pricing.GetDefaultPricingProfile()}
}
pricings := []pricing.PricingProfileITF{}
for _, p := range ri.Partnerships {
pricings = append(pricings, p.GetPricingsProfiles(peerID, groups)...)
}
if len(pricings) == 0 {
if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {
pricings = append(pricings, pricing.GetDefaultPricingProfile())
}
}
return pricings
}
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 {
partners = append(partners, p)
groups = append(groups, p.GetPeerGroups())
}
if len(groups) == 0 {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
}))
if err != nil || pp == nil {
return partners, groups
}
groups = []map[string][]string{
{
pp.GetID(): {"*"},
},
}
// TODO make allow all only for self.
}
return partners, groups
}
@@ -183,40 +407,176 @@ func (ri *ResourceInstance[T]) ClearPeerGroups() {
}
}
func (ri *ResourceInstance[T]) GetAverageDurationS() float64 {
return ri.AverageDurationS
}
func (ri *ResourceInstance[T]) UpdateAverageDuration(actualS float64) {
buffered := actualS * 1.20
n := float64(ri.AverageDurationSamples)
ri.AverageDurationS = (ri.AverageDurationS*n + buffered) / (n + 1)
ri.AverageDurationSamples++
}
func (ri *ResourceInstance[T]) GetPartnerships() []ResourcePartnerITF {
rt := []ResourcePartnerITF{}
for _, p := range ri.Partnerships {
rt = append(rt, p)
}
return rt
}
type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
Namespace string `json:"namespace" bson:"namespace" default:"default-namespace"`
PeerGroups map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
PricingProfiles []T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
PricingProfiles map[int]map[int]T `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
// to upgrade pricing profiles. to be a map BuyingStrategy, map of Strategy
}
func (ri *ResourcePartnerShip[T]) FilterPartnership(peerID string) {
if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil {
ri.PeerGroups = map[string][]string{
"*": {"*"},
}
} else {
ri.PeerGroups = map[string][]string{}
if ri.PeerGroups["*"] != nil {
ri.PeerGroups["*"] = ri.PeerGroups["*"]
}
if ri.PeerGroups[peerID] != nil {
ri.PeerGroups[peerID] = ri.PeerGroups[peerID]
}
}
}
func (ri *ResourcePartnerShip[T]) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
if buying != nil && strategy != nil {
if strat, ok := ri.PricingProfiles[*buying]; ok {
if profile, ok := strat[*strategy]; ok {
return profile
}
}
}
return nil
}
/*
Le pricing doit être selectionné lors d'un scheduling...
le type de paiement défini le type de stratégie de paiement
note : il faut rajouté - une notion de facturation
Une order est l'ensemble de la commande... un booking une réservation, une purchase un acte d'achat.
Une bill (facture) représente alors... l'emission d'une facture à un instant T en but d'être honorée, envoyée ... etc.
*/
func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
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 {
profiles = append(profiles, ri)
for _, i := range ri {
profiles = append(profiles, i)
}
}
if slices.Contains(groups, "*") {
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) || slices.Contains(groups, "*") {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
for _, i := range ri {
profiles = append(profiles, i)
}
}
return profiles
}
for _, p := range ri.PeerGroups[peerID] {
if slices.Contains(groups, p) {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
}
return profiles
}
}
if len(profiles) == 0 {
if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
})); ok {
profiles = append(profiles, pricing.GetDefaultPricingProfile())
}
}
return profiles
}
func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string {
if len(rp.PeerGroups) == 0 {
pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
Admin: true,
}))
if err != nil || pp == nil {
return rp.PeerGroups
}
return map[string][]string{
"*": {"*"},
pp.GetID(): {"*"},
}
}
return rp.PeerGroups
}
func (rp *ResourcePartnerShip[T]) ClearPeerGroups() {
rp.PeerGroups = map[string][]string{}
}
func ToResource(
dt int,
payload []byte,
) (ResourceInterface, error) {
switch dt {
case tools.PROCESSING_RESOURCE.EnumIndex():
var data ProcessingResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.WORKFLOW_RESOURCE.EnumIndex():
var data WorkflowResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.DATA_RESOURCE.EnumIndex():
var data DataResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.STORAGE_RESOURCE.EnumIndex():
var data StorageResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.COMPUTE_RESOURCE.EnumIndex():
var data ComputeResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
case tools.SERVICE_RESOURCE.EnumIndex():
var data ServiceResource
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &data, nil
}
return nil, errors.New("can't found any data resources matching")
}
type ResourceAccess struct {
Source *models.PathSource `json:"source,omitempty" bson:"source,omitempty"`
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
}
// HasSource returns true when the access is source-based (no embedded container).
func (a *ResourceAccess) HasSource() bool {
return a != nil && a.Container == nil && a.Source != nil
}
// HasContainer returns true when an explicit container image is provided.
func (a *ResourceAccess) HasContainer() bool {
return a != nil && a.Container != nil
}
+191 -40
View File
@@ -1,92 +1,243 @@
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 // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject
type ResourceMongoAccessor[T ResourceInterface] struct {
utils.AbstractAccessor[ResourceInterface] // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
func sourceFromAccess(access *ResourceAccess) string {
if access == nil {
return ""
}
if access.Container != nil && access.Container.Source != "" {
return access.Container.Source
}
if access.Source != nil && access.Source.Source != "" {
return access.Source.Source
}
return ""
}
func upsertSourceParam(outputs []models.Param, source string) []models.Param {
for i, p := range outputs {
if p.Attr == "source" {
outputs[i].Value = source
return outputs
}
}
return append(outputs, models.Param{Attr: "source", Value: source, Readonly: true})
}
func applyAccessSourceOutput(data utils.DBObject) {
switch r := data.(type) {
case *ProcessingResource:
for _, inst := range r.Instances {
if src := sourceFromAccess(inst.Access); src != "" {
r.Outputs = upsertSourceParam(r.Outputs, src)
return
}
}
case *DataResource:
for _, inst := range r.Instances {
if src := sourceFromAccess(inst.Access); src != "" {
r.Outputs = upsertSourceParam(r.Outputs, src)
return
}
}
}
}
// New creates a new instance of the computeMongoAccessor
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *resourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE, tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE}, t) {
func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest) *ResourceMongoAccessor[T] {
if !slices.Contains([]tools.DataType{
tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE,
tools.PROCESSING_RESOURCE, tools.SERVICE_RESOURCE,
tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE, tools.NATIVE_TOOL,
}, t) {
return nil
}
return &resourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{
return &ResourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor[ResourceInterface]{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request,
Type: t,
New: func() ResourceInterface {
switch t {
case tools.COMPUTE_RESOURCE:
return &ComputeResource{}
case tools.STORAGE_RESOURCE:
return &StorageResource{}
case tools.PROCESSING_RESOURCE:
return &ProcessingResource{}
case tools.SERVICE_RESOURCE:
return &ServiceResource{}
case tools.WORKFLOW_RESOURCE:
return &WorkflowResource{}
case tools.DATA_RESOURCE:
return &DataResource{}
case tools.NATIVE_TOOL:
return &NativeTool{}
}
return nil
},
},
generateData: g,
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (dca *resourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, dca)
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 utils.DBObject, id string) (utils.DBObject, int, error) {
set.(T).Trim()
return utils.GenericUpdateOne(set, id, dca, dca.generateData())
func (dca *ResourceMongoAccessor[T]) UpdateOne(set map[string]interface{}, id string) (utils.DBObject, int, error) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
delete(set, "architecture")
delete(set, "infrastructure")
} else if dca.GetType() == tools.SERVICE_RESOURCE {
delete(set, "infrastructure")
} else if dca.GetType() == tools.STORAGE_RESOURCE {
delete(set, "storage_type")
}
if dca.GetType() == tools.PROCESSING_RESOURCE || dca.GetType() == tools.DATA_RESOURCE {
if merged, _, _, err := utils.ModelGenericUpdateOne(set, id, dca); err == nil {
applyAccessSourceOutput(merged)
if serialized := merged.Serialize(merged); serialized != nil {
set["outputs"] = serialized["outputs"]
}
}
}
return utils.GenericUpdateOne(set, id, dca)
}
func (dca *resourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
data.(T).Trim()
return utils.GenericStoreOne(data, dca)
func (dca *ResourceMongoAccessor[T]) ShouldVerifyAuth() bool {
return false // TEMP : by pass
}
func (dca *resourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
var i string
idsToUpdate := []string{}
var a utils.Accessor
if dca.GetType() == tools.COMPUTE_RESOURCE {
r := data.(*ComputeResource)
if len(r.Instances) == 0 {
return nil, 404, errors.New("can't create a non existing computing units resource with no instances")
}
a = live.NewAccessor[*live.LiveDatacenter](tools.LIVE_DATACENTER, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID())
fmt.Println(res, r.Instances[0].GetID())
if res == nil {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
}
if !res.(*live.LiveDatacenter).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live computing units target is not compatible")
}
i = res.GetID()
idsToUpdate = res.(*live.LiveDatacenter).ResourcesID
} else if dca.GetType() == tools.SERVICE_RESOURCE {
r := data.(*ServiceResource)
if len(r.Instances) == 0 {
return nil, 404, errors.New("can't create a non existing service resource with no instances")
}
a = live.NewAccessor[*live.LiveService](tools.LIVE_SERVICE, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID())
if res == nil {
return nil, 404, errors.New("can't create a non existing service resource not reported onto service catalog")
}
if !res.(*live.LiveService).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live service target is not compatible")
}
i = res.GetID()
idsToUpdate = res.(*live.LiveService).ResourcesID
} else if dca.GetType() == tools.STORAGE_RESOURCE {
r := data.(*StorageResource)
if len(r.Instances) == 0 {
return nil, 404, errors.New("can't create a non existing storage resource with no instances")
}
a = live.NewAccessor[*live.LiveStorage](tools.LIVE_STORAGE, &tools.APIRequest{Admin: true})
res, _, _ := a.LoadOne(r.Instances[0].GetID())
if res == nil {
return nil, 404, errors.New("can't create a non existing storage resource not reported onto storage catalog")
}
if !res.(*live.LiveStorage).IsCompatible(data.Serialize(data)) {
return nil, 404, errors.New("live storage target is not compatible")
}
i = res.GetID()
idsToUpdate = res.(*live.LiveStorage).ResourcesID
}
applyAccessSourceOutput(data)
res, code, err := utils.GenericStoreOne(data, dca)
if res != nil && i != "" {
idsToUpdate = append(idsToUpdate, res.GetID())
a.UpdateOne(map[string]interface{}{
"resources_id": idsToUpdate,
}, i)
}
return res, code, err
}
func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return dca.StoreOne(data)
}
func (dca *resourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) {
d.(T).SetAllowedInstances(dca.Request)
return d, 200, nil
}, dca)
func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](wfa.GetExec(isDraft), isDraft, wfa, offset, limit)
}
func (wfa *resourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request)
return d
}, isDraft, wfa)
}
func (wfa *resourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, 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.getResourceFilter(search),
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 (abs *resourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters {
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
}
}
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()
}
Regular → Executable
+86 -53
View File
@@ -2,7 +2,6 @@ package resources
import (
"errors"
"fmt"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/enum"
@@ -10,8 +9,20 @@ import (
"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"
"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,29 +34,40 @@ 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 {
return tools.STORAGE_RESOURCE.String()
}
func (abs *StorageResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
func (abs *StorageResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
if t != tools.STORAGE_RESOURCE {
return nil
return nil, errors.New("not the proper type expected : cannot convert to priced resource : have " + t.String() + " wait Storage")
}
p := abs.AbstractInstanciatedResource.ConvertToPricedResource(t, request)
priced := p.(*PricedResource)
p, err := ConvertToPricedResource[*StorageResourcePricingProfile](t, selectedInstance, selectedPartnership, selectedBuyingStrategy, selectedStrategy, selectedBookingModeIndex, abs, request)
if err != nil {
return nil, err
}
priced := p.(*PricedResource[*StorageResourcePricingProfile])
return &PricedStorageResource{
PricedResource: *priced,
}
}, nil
}
func (ri *StorageResource) StoreDraftDefault() {
ri.AbstractObject.StoreDraftDefault()
ri.Env = append(ri.Env, models.Param{
Attr: "source",
Value: "[resource]instance.source",
Readonly: true,
})
}
type StorageResourceInstance struct {
ResourceInstance[*StorageResourcePartnership]
Credentials *Credentials `json:"credentials,omitempty" bson:"credentials,omitempty"`
Source string `bson:"source,omitempty" json:"source,omitempty"` // Source is the source of the storage
Path string `bson:"path,omitempty" json:"path,omitempty"` // Path is the store folders in the source
Local bool `bson:"local" json:"local"`
SecurityLevel string `bson:"security_level,omitempty" json:"security_level,omitempty"`
SizeType enum.StorageSize `bson:"size_type" json:"size_type" default:"0"` // SizeType is the type of the storage size
@@ -55,29 +77,19 @@ type StorageResourceInstance struct {
Throughput string `bson:"throughput,omitempty" json:"throughput,omitempty"` // Throughput is the throughput of the storage
}
func (ri *StorageResourceInstance) ClearEnv() {
ri.Credentials = nil
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
ri.Outputs = []models.Param{}
}
// 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 (ri *StorageResourceInstance) StoreDraftDefault() {
found := false
for _, p := range ri.ResourceInstance.Env {
if p.Attr == "source" {
found = true
break
}
func NewStorageResourceInstance(name string, peerID string) ResourceInstanceITF {
return &StorageResourceInstance{
ResourceInstance: ResourceInstance[*StorageResourcePartnership]{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: name,
},
},
}
if !found {
ri.ResourceInstance.Env = append(ri.ResourceInstance.Env, models.Param{
Attr: "source",
Value: ri.Source,
Readonly: true,
})
}
ri.ResourceInstance.StoreDraftDefault()
}
type StorageResourcePartnership struct {
@@ -105,7 +117,7 @@ func (t PrivilegeStoragePricingStrategy) String() string {
type StorageResourcePricingStrategy int
const (
PER_DATA_STORED StorageResourcePricingStrategy = iota
PER_DATA_STORED StorageResourcePricingStrategy = iota + 7
PER_TB_STORED
PER_GB_STORED
PER_MB_STORED
@@ -117,11 +129,15 @@ func StorageResourcePricingStrategyList() []StorageResourcePricingStrategy {
}
func (t StorageResourcePricingStrategy) String() string {
return [...]string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}...)
return l[t]
}
func (t StorageResourcePricingStrategy) GetStrategy() string {
return [...]string{"PER_DATA_STORED", "PER_GB_STORED", "PER_MB_STORED", "PER_KB_STORED"}[t]
l := pricing.TimePricingStrategyListStr()
l = append(l, []string{"PER DATA STORED", "PER TB STORED", "PER GB STORED", "PER MB STORED", "PER KB STORED"}...)
return l[t]
}
func (t StorageResourcePricingStrategy) GetStrategyValue() int {
@@ -152,40 +168,56 @@ type StorageResourcePricingProfile struct {
pricing.ExploitPricingProfile[StorageResourcePricingStrategy] // ExploitPricingProfile is the pricing profile of a storage it means that we exploit the resource for an amount of continuous time
}
func (p *StorageResourcePricingProfile) IsPurchased() bool {
return p.Pricing.BuyingStrategy != pricing.PAY_PER_USE
func (p *StorageResourcePricingProfile) IsPurchasable() bool {
return p.Pricing.BuyingStrategy == pricing.PERMANENT
}
func (p *StorageResourcePricingProfile) GetPrice(amountOfData float64, val float64, start time.Time, end time.Time, params ...string) (float64, error) {
return p.Pricing.GetPrice(amountOfData, val, start, &end)
func (p *StorageResourcePricingProfile) IsBooked() bool {
if p.Pricing.BuyingStrategy == pricing.PERMANENT {
p.Pricing.BuyingStrategy = pricing.SUBSCRIPTION
}
return true
}
type PricedStorageResource struct {
PricedResource
PricedResource[*StorageResourcePricingProfile]
UsageStorageGB float64 `json:"storage_gb,omitempty" bson:"storage_gb,omitempty"`
}
func (r *PricedStorageResource) ensurePricing() {
if r.SelectedPricing == nil {
r.SelectedPricing = &StorageResourcePricingProfile{}
}
}
func (r *PricedStorageResource) IsPurchasable() bool {
r.ensurePricing()
return r.SelectedPricing.IsPurchasable()
}
func (r *PricedStorageResource) IsBooked() bool {
r.ensurePricing()
return r.SelectedPricing.IsBooked()
}
func (r *PricedStorageResource) GetType() tools.DataType {
return tools.STORAGE_RESOURCE
}
func (r *PricedStorageResource) GetPrice() (float64, error) {
fmt.Println("GetPrice", r.UsageStart, r.UsageEnd)
func (r *PricedStorageResource) GetPriceHT() (float64, error) {
r.ensurePricing()
if r.BookingConfiguration == nil {
r.BookingConfiguration = &BookingConfiguration{}
}
now := time.Now()
if r.UsageStart == nil {
r.UsageStart = &now
if r.BookingConfiguration.UsageStart == nil {
r.BookingConfiguration.UsageStart = &now
}
if r.UsageEnd == nil {
add := r.UsageStart.Add(time.Duration(1 * time.Hour))
r.UsageEnd = &add
if r.BookingConfiguration.UsageEnd == nil {
add := r.BookingConfiguration.UsageStart.Add(time.Duration(5 * time.Minute))
r.BookingConfiguration.UsageEnd = &add
}
if r.SelectedPricing == nil {
if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID)
}
r.SelectedPricing = &r.PricingProfiles[0]
}
pricing := *r.SelectedPricing
pricing := r.SelectedPricing
var err error
amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 {
@@ -194,5 +226,6 @@ func (r *PricedStorageResource) GetPrice() (float64, error) {
return 0, err
}
}
return pricing.GetPrice(amountOfData, r.ExplicitBookingDurationS, *r.UsageStart, *r.UsageEnd)
return pricing.GetPriceHT(amountOfData, r.BookingConfiguration.ExplicitBookingDurationS,
*r.BookingConfiguration.UsageStart, *r.BookingConfiguration.UsageEnd, r.Variations)
}
+118
View File
@@ -0,0 +1,118 @@
package resources_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestComputeResource_GetType(t *testing.T) {
r := &resources.ComputeResource{}
assert.Equal(t, tools.COMPUTE_RESOURCE.String(), r.GetType())
}
func TestComputeResource_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
cr := &resources.ComputeResource{}
accessor := cr.GetAccessor(req)
assert.NotNil(t, accessor)
}
func TestComputeResource_ConvertToPricedResource(t *testing.T) {
req := &tools.APIRequest{}
cr := &resources.ComputeResource{}
cr.UUID = "comp123"
cr.AbstractInstanciatedResource.UUID = cr.UUID
result, _ := cr.ConvertToPricedResource(tools.COMPUTE_RESOURCE, nil, nil, nil, nil, nil, req)
assert.NotNil(t, result)
assert.IsType(t, &resources.PricedComputeResource{}, result)
}
func TestComputeResourcePricingProfile_GetPriceHT_CPUs(t *testing.T) {
start := time.Now()
end := start.Add(5 * time.Minute)
profile := resources.ComputeResourcePricingProfile{
CPUsPrices: map[string]float64{"Xeon": 2.0},
ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0},
},
},
}
price, err := profile.GetPriceHT(2, 3600, start, end, []*pricing.PricingVariation{}, "cpus", "Xeon")
require.NoError(t, err)
assert.Greater(t, price, float64(0))
}
func TestComputeResourcePricingProfile_GetPriceHT_InvalidParams(t *testing.T) {
profile := resources.ComputeResourcePricingProfile{}
_, err := profile.GetPriceHT(1, 3600, time.Now(), time.Now(), []*pricing.PricingVariation{})
assert.Error(t, err)
assert.Equal(t, "params must be set", err.Error())
}
func TestPricedComputeResource_GetPriceHT(t *testing.T) {
start := time.Now()
end := start.Add(5 * time.Minute)
r := resources.PricedComputeResource{
PricedResource: resources.PricedResource[*resources.ComputeResourcePricingProfile]{
ResourceID: "comp456",
SelectedPricing: &resources.ComputeResourcePricingProfile{
CPUsPrices: map[string]float64{"Xeon": 2.0},
ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0},
},
},
},
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
ExplicitBookingDurationS: 3600,
},
},
CPUsLocated: map[string]float64{"Xeon": 2},
GPUsLocated: map[string]float64{},
RAMLocated: 0,
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Greater(t, price, float64(0))
}
func TestPricedComputeResource_GetPriceHT_MissingProfile(t *testing.T) {
r := resources.PricedComputeResource{
PricedResource: resources.PricedResource[*resources.ComputeResourcePricingProfile]{
ResourceID: "comp789",
},
}
_, err := r.GetPriceHT()
require.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
}
func TestPricedComputeResource_FillWithDefaultProcessingUsage(t *testing.T) {
usage := &resources.ProcessingUsage{
CPUs: map[string]*models.CPU{"t": {Model: "Xeon", Cores: 4}},
GPUs: map[string]*models.GPU{"t1": {Model: "Tesla"}},
RAM: &models.RAM{SizeGb: 16},
}
r := &resources.PricedComputeResource{
CPUsLocated: make(map[string]float64),
GPUsLocated: make(map[string]float64),
RAMLocated: 0,
}
r.FillWithDefaultProcessingUsage(usage)
assert.Equal(t, float64(4), r.CPUsLocated["Xeon"])
assert.Equal(t, float64(1), r.GPUsLocated["Tesla"])
assert.Equal(t, float64(16), r.RAMLocated)
}
+104
View File
@@ -0,0 +1,104 @@
package resources_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDataResource_GetType(t *testing.T) {
d := &resources.DataResource{}
assert.Equal(t, tools.DATA_RESOURCE.String(), d.GetType())
}
func TestDataResource_GetAccessor(t *testing.T) {
req := &tools.APIRequest{}
acc := (&resources.DataResource{}).GetAccessor(req)
assert.NotNil(t, acc)
}
func TestDataResource_ConvertToPricedResource(t *testing.T) {
d := &resources.DataResource{}
d.UUID = "123"
res, _ := d.ConvertToPricedResource(tools.DATA_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.IsType(t, &resources.PricedDataResource{}, res)
nilRes, _ := d.ConvertToPricedResource(tools.PROCESSING_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.Nil(t, nilRes)
}
func TestDataResourcePricingStrategy_GetQuantity(t *testing.T) {
tests := []struct {
strategy resources.DataResourcePricingStrategy
input float64
expected float64
}{
{resources.PER_DOWNLOAD, 1, 1},
{resources.PER_TB_DOWNLOADED, 1, 1000},
{resources.PER_GB_DOWNLOADED, 2.5, 2.5},
{resources.PER_MB_DOWNLOADED, 1, 0.001},
{resources.PER_KB_DOWNLOADED, 1, 0.000001},
}
for _, tt := range tests {
q, err := tt.strategy.GetQuantity(tt.input)
require.NoError(t, err)
assert.InDelta(t, tt.expected, q, 1e-9)
}
_, err := resources.DataResourcePricingStrategy(999).GetQuantity(1)
assert.Error(t, err)
}
func TestDataResourcePricingProfile_IsPurchased(t *testing.T) {
profile := &resources.DataResourcePricingProfile{}
profile.Pricing.BuyingStrategy = pricing.PERMANENT
assert.True(t, profile.IsPurchasable())
}
func TestPricedDataResource_GetPriceHT(t *testing.T) {
now := time.Now()
later := now.Add(5 * time.Minute)
mockPrice := 42.0
pricingProfile := &resources.DataResourcePricingProfile{AccessPricingProfile: pricing.AccessPricingProfile[resources.DataResourcePricingStrategy]{
Pricing: pricing.PricingStrategy[resources.DataResourcePricingStrategy]{Price: 42.0}},
}
pricingProfile.Pricing.OverrideStrategy = resources.PER_GB_DOWNLOADED
r := &resources.PricedDataResource{
PricedResource: resources.PricedResource[*resources.DataResourcePricingProfile]{
SelectedPricing: pricingProfile,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &now,
UsageEnd: &later,
},
},
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Equal(t, mockPrice, price)
}
func TestPricedDataResource_GetPriceHT_NoProfiles(t *testing.T) {
r := &resources.PricedDataResource{
PricedResource: resources.PricedResource[*resources.DataResourcePricingProfile]{
ResourceID: "test-resource",
},
}
_, err := r.GetPriceHT()
assert.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
}
func TestPricedDataResource_GetType(t *testing.T) {
r := &resources.PricedDataResource{}
assert.Equal(t, tools.DATA_RESOURCE, r.GetType())
}
@@ -0,0 +1,149 @@
package resources_test
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
// ---- Mock PricingProfile ----
type MockPricingProfile struct {
pricing.PricingProfileITF
Purchased bool
ReturnErr bool
ReturnCost float64
}
func (m *MockPricingProfile) IsPurchasable() bool {
return m.Purchased
}
func (m *MockPricingProfile) GetPriceHT(amount float64, explicitDuration float64, start time.Time, end time.Time, variations []*pricing.PricingVariation, _ ...string) (float64, error) {
if m.ReturnErr {
return 0, errors.New("mock error")
}
return m.ReturnCost, nil
}
// ---- Tests ----
func TestGetIDAndCreatorAndType(t *testing.T) {
r := resources.PricedResource[pricing.PricingProfileITF]{
ResourceID: "res-123",
CreatorID: "user-abc",
ResourceType: tools.DATA_RESOURCE,
}
assert.Equal(t, "res-123", r.GetID())
assert.Equal(t, "user-abc", r.GetCreatorID())
assert.Equal(t, tools.DATA_RESOURCE, r.GetType())
}
func TestIsPurchased(t *testing.T) {
t.Run("nil selected pricing returns false", func(t *testing.T) {
r := &resources.PricedResource[pricing.PricingProfileITF]{}
assert.False(t, r.IsPurchasable())
})
t.Run("returns true if pricing profile is purchased", func(t *testing.T) {
mock := &MockPricingProfile{Purchased: true}
r := &resources.PricedResource[pricing.PricingProfileITF]{SelectedPricing: mock}
assert.True(t, r.IsPurchasable())
})
}
func TestGetAndSetLocationStartEnd(t *testing.T) {
r := &resources.PricedResource[pricing.PricingProfileITF]{}
now := time.Now()
r.SetLocationStart(now)
r.SetLocationEnd(now.Add(10 * time.Minute))
assert.Equal(t, now, *r.GetLocationStart())
assert.Equal(t, now.Add(2*time.Hour), *r.GetLocationEnd())
}
func TestGetExplicitDurationInS(t *testing.T) {
t.Run("uses explicit duration if set", func(t *testing.T) {
r := &resources.PricedResource[pricing.PricingProfileITF]{BookingConfiguration: &resources.BookingConfiguration{
ExplicitBookingDurationS: 3600,
},
}
assert.Equal(t, 3600.0, r.GetExplicitDurationInS())
})
t.Run("computes duration from start and end", func(t *testing.T) {
start := time.Now()
end := start.Add(10 * time.Minute)
r := &resources.PricedResource[pricing.PricingProfileITF]{
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start, UsageEnd: &end,
},
}
assert.InDelta(t, 7200.0, r.GetExplicitDurationInS(), 0.1)
})
t.Run("defaults to 1 hour when times not set", func(t *testing.T) {
r := &resources.PricedResource[pricing.PricingProfileITF]{}
assert.InDelta(t, 3600.0, r.GetExplicitDurationInS(), 0.1)
})
}
func TestGetPriceHT(t *testing.T) {
t.Run("returns error if no pricing profile", func(t *testing.T) {
r := &resources.PricedResource[pricing.PricingProfileITF]{ResourceID: "no-profile"}
price, err := r.GetPriceHT()
require.Error(t, err)
assert.Contains(t, err.Error(), "pricing profile must be set")
assert.Equal(t, 0.0, price)
})
t.Run("defaults BookingConfiguration when nil", func(t *testing.T) {
mock := &MockPricingProfile{ReturnCost: 42.0}
r := &resources.PricedResource[pricing.PricingProfileITF]{
SelectedPricing: mock,
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Equal(t, 42.0, price)
})
t.Run("returns error if profile GetPriceHT fails", func(t *testing.T) {
start := time.Now()
end := start.Add(5 * time.Minute)
mock := &MockPricingProfile{ReturnErr: true}
r := &resources.PricedResource[pricing.PricingProfileITF]{
SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
},
}
price, err := r.GetPriceHT()
require.Error(t, err)
assert.Equal(t, 0.0, price)
})
t.Run("uses SelectedPricing if set", func(t *testing.T) {
start := time.Now()
end := start.Add(5 * time.Minute)
mock := &MockPricingProfile{ReturnCost: 10.0}
r := &resources.PricedResource[pricing.PricingProfileITF]{
SelectedPricing: mock,
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &start,
UsageEnd: &end,
},
}
price, err := r.GetPriceHT()
require.NoError(t, err)
assert.Equal(t, 10.0, price)
})
}
+106
View File
@@ -0,0 +1,106 @@
package resources_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
. "cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
func TestProcessingResource_GetType(t *testing.T) {
r := &ProcessingResource{}
assert.Equal(t, tools.PROCESSING_RESOURCE.String(), r.GetType())
}
func TestPricedProcessingResource_GetType(t *testing.T) {
r := &PricedProcessingResource{}
assert.Equal(t, tools.PROCESSING_RESOURCE, r.GetType())
}
func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) {
now := time.Now()
after := now.Add(10 * time.Minute)
tests := []struct {
name string
input PricedProcessingResource
expected float64
}{
{
name: "Nil start time, non-service",
input: PricedProcessingResource{
PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: nil,
},
},
},
expected: float64((5 * time.Minute).Seconds()),
},
{
name: "Duration computed from start and end",
input: PricedProcessingResource{
PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
BookingConfiguration: &resources.BookingConfiguration{
UsageStart: &now,
UsageEnd: &after,
},
},
},
expected: float64((10 * time.Minute).Seconds()),
},
{
name: "Explicit duration takes precedence",
input: PricedProcessingResource{
PricedResource: PricedResource[*ProcessingResourcePricingProfile]{
BookingConfiguration: &resources.BookingConfiguration{
ExplicitBookingDurationS: 1337,
},
},
},
expected: 1337,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, test.input.GetExplicitDurationInS())
})
}
}
func TestProcessingResource_GetAccessor(t *testing.T) {
request := &tools.APIRequest{}
r := &ProcessingResource{}
acc := r.GetAccessor(request)
assert.NotNil(t, acc)
}
func TestProcessingResourcePricingProfile_GetPriceHT(t *testing.T) {
start := time.Now()
end := start.Add(10 * time.Minute)
mockPricing := pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{
Price: 100.0,
},
}
profile := &ProcessingResourcePricingProfile{AccessPricingProfile: mockPricing}
price, err := profile.GetPriceHT(1, 0, start, end, []*pricing.PricingVariation{})
assert.NoError(t, err)
assert.Equal(t, 100.0, price)
}
func TestProcessingResourcePricingProfile_IsPurchased(t *testing.T) {
purchased := &ProcessingResourcePricingProfile{
AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{
Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{
BuyingStrategy: pricing.PERMANENT,
},
},
}
assert.True(t, purchased.IsPurchasable())
}
+119
View File
@@ -0,0 +1,119 @@
package resources_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/common/pricing"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
type MockInstance struct {
ID string
Name string
resources.ResourceInstance[*MockPartner]
}
func (m *MockInstance) GetID() string { return m.ID }
func (m *MockInstance) GetName() string { return m.Name }
func (m *MockInstance) ClearEnv() {}
func (m *MockInstance) ClearPeerGroups() {}
func (m *MockPartner) FilterPartnership(peerID string) {}
func (m *MockInstance) GetProfile(peerID string, a *int, b *int, c *int) pricing.PricingProfileITF {
return nil
}
func (m *MockInstance) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
return nil
}
func (m *MockInstance) GetPeerGroups() ([]resources.ResourcePartnerITF, []map[string][]string) {
return nil, []map[string][]string{
{"peer1": {"group1"}},
}
}
type MockPartner struct {
groups map[string][]string
}
func (m *MockPartner) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
return nil
}
func (m *MockPartner) GetPeerGroups() map[string][]string {
return m.groups
}
func (m *MockPartner) ClearPeerGroups() {}
func (m *MockPartner) GetPricingsProfiles(string, []string) []pricing.PricingProfileITF {
return nil
}
type MockDBObject struct {
utils.AbstractObject
isDraft bool
}
func (m *MockDBObject) IsDrafted() bool {
return m.isDraft
}
func TestGetSelectedInstance_WithValidIndex(t *testing.T) {
index := 1
inst1 := &MockInstance{ID: "1"}
inst2 := &MockInstance{ID: "2"}
resource := &resources.AbstractInstanciatedResource[*MockInstance]{
AbstractResource: resources.AbstractResource{},
Instances: []*MockInstance{inst1, inst2},
}
result := resource.GetSelectedInstance(&index)
assert.Equal(t, inst2, result)
}
func TestGetSelectedInstance_NoIndex(t *testing.T) {
inst := &MockInstance{ID: "1"}
resource := &resources.AbstractInstanciatedResource[*MockInstance]{
Instances: []*MockInstance{inst},
}
result := resource.GetSelectedInstance(nil)
assert.Equal(t, inst, result)
}
func TestCanUpdate_WhenOnlyStateDiffers(t *testing.T) {
resource := &resources.AbstractResource{AbstractObject: utils.AbstractObject{IsDraft: true}}
set := &MockDBObject{isDraft: false}
canUpdate, updated := resource.CanUpdate(set)
assert.True(t, canUpdate)
assert.Equal(t, set, updated)
}
func TestVerifyAuthAction_WithMatchingGroup(t *testing.T) {
inst := &MockInstance{
ResourceInstance: resources.ResourceInstance[*MockPartner]{
Partnerships: []*MockPartner{
{groups: map[string][]string{"peer1": {"group1"}}},
},
},
}
req := &tools.APIRequest{PeerID: "peer1", Groups: []string{"group1"}}
result := resources.VerifyAuthAction([]*MockInstance{inst}, req)
assert.Len(t, result, 1)
}
type FakeResource struct {
resources.AbstractInstanciatedResource[*MockInstance]
}
func (f *FakeResource) SetAllowedInstances(req *tools.APIRequest, instance_id ...string) []resources.ResourceInstanceITF {
return nil
}
func (f *FakeResource) ConvertToPricedResource(t tools.DataType, a *int, b *int, c *int, d *int, e *int, req *tools.APIRequest) (pricing.PricedItemITF, error) {
return nil, nil
}
func (f *FakeResource) VerifyAuth(string, *tools.APIRequest) bool { return true }
func TestNewAccessor_ReturnsValid(t *testing.T) {
acc := resources.NewAccessor[*FakeResource](tools.COMPUTE_RESOURCE, &tools.APIRequest{})
assert.NotNil(t, acc)
}
+74
View File
@@ -0,0 +1,74 @@
package resources_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources"
)
func TestStorageResource_GetType(t *testing.T) {
res := &resources.StorageResource{}
assert.Equal(t, tools.STORAGE_RESOURCE.String(), res.GetType())
}
func TestStorageResource_GetAccessor(t *testing.T) {
res := &resources.StorageResource{}
req := &tools.APIRequest{}
accessor := res.GetAccessor(req)
assert.NotNil(t, accessor)
}
func TestStorageResource_ConvertToPricedResource_ValidType(t *testing.T) {
res := &resources.StorageResource{}
res.AbstractInstanciatedResource.CreatorID = "creator"
res.AbstractInstanciatedResource.UUID = "res-id"
priced, _ := res.ConvertToPricedResource(tools.STORAGE_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.NotNil(t, priced)
assert.IsType(t, &resources.PricedStorageResource{}, priced)
}
func TestStorageResource_ConvertToPricedResource_InvalidType(t *testing.T) {
res := &resources.StorageResource{}
priced, _ := res.ConvertToPricedResource(tools.COMPUTE_RESOURCE, nil, nil, nil, nil, nil, &tools.APIRequest{})
assert.Nil(t, priced)
}
func TestStorageResourcePricingStrategy_GetQuantity(t *testing.T) {
tests := []struct {
strategy resources.StorageResourcePricingStrategy
dataGB float64
expect float64
}{
{resources.PER_DATA_STORED, 1.2, 1.2},
{resources.PER_TB_STORED, 1.2, 1200},
{resources.PER_GB_STORED, 2.5, 2.5},
{resources.PER_MB_STORED, 1.0, 1000},
{resources.PER_KB_STORED, 0.1, 100000},
}
for _, tt := range tests {
q, err := tt.strategy.GetQuantity(tt.dataGB)
assert.NoError(t, err)
assert.Equal(t, tt.expect, q)
}
}
func TestStorageResourcePricingStrategy_GetQuantity_Invalid(t *testing.T) {
invalid := resources.StorageResourcePricingStrategy(99)
q, err := invalid.GetQuantity(1.0)
assert.Error(t, err)
assert.Equal(t, 0.0, q)
}
func TestPricedStorageResource_GetPriceHT_NoProfiles(t *testing.T) {
res := &resources.PricedStorageResource{
PricedResource: resources.PricedResource[*resources.StorageResourcePricingProfile]{
ResourceID: "res-id",
},
}
_, err := res.GetPriceHT()
assert.Error(t, err)
}
+55
View File
@@ -0,0 +1,55 @@
package resources_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources"
)
func TestWorkflowResource_GetType(t *testing.T) {
w := &resources.WorkflowResource{}
assert.Equal(t, tools.WORKFLOW_RESOURCE.String(), w.GetType())
}
func TestWorkflowResource_ConvertToPricedResource(t *testing.T) {
w := &resources.WorkflowResource{
AbstractResource: resources.AbstractResource{
AbstractObject: utils.AbstractObject{
Name: "Test Workflow",
UUID: "workflow-uuid",
CreatorID: "creator-id",
},
Logo: "logo.png",
},
}
req := &tools.APIRequest{
PeerID: "peer-1",
Groups: []string{"group1"},
}
pr, _ := w.ConvertToPricedResource(tools.WORKFLOW_RESOURCE, nil, nil, nil, nil, nil, req)
assert.Equal(t, "creator-id", pr.GetCreatorID())
assert.Equal(t, tools.WORKFLOW_RESOURCE, pr.GetType())
}
func TestWorkflowResource_ClearEnv(t *testing.T) {
w := &resources.WorkflowResource{}
assert.Equal(t, w, w.ClearEnv())
}
func TestWorkflowResource_SetAllowedInstances(t *testing.T) {
w := &resources.WorkflowResource{}
w.SetAllowedInstances(&tools.APIRequest{})
// no-op; just confirm no crash
}
func TestWorkflowResource_GetAccessor(t *testing.T) {
w := &resources.WorkflowResource{}
request := &tools.APIRequest{}
accessor := w.GetAccessor(request)
assert.NotNil(t, accessor)
}
Regular → Executable
+23 -10
View File
@@ -16,7 +16,10 @@ type WorkflowResource struct {
}
func (d *WorkflowResource) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor[*ComputeResource](tools.WORKFLOW_RESOURCE, request, func() utils.DBObject { return &WorkflowResource{} })
return NewAccessor[*WorkflowResource](tools.WORKFLOW_RESOURCE, request)
}
func (r *WorkflowResource) AddInstances(instance ResourceInstanceITF) {
}
func (r *WorkflowResource) GetType() string {
@@ -27,20 +30,30 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
return d
}
func (d *WorkflowResource) Trim() {
/* EMPTY */
}
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest) {
/* EMPTY */
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
// WorkflowResource has no instances, but still carries AEs that must be
// filtered before the resource is returned to a non-owner, non-admin peer.
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
if request != nil {
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
}
func (w *WorkflowResource) ConvertToPricedResource(
t tools.DataType, request *tools.APIRequest) pricing.PricedItemITF {
return &PricedResource{
func (r *WorkflowResource) GetSelectedInstance(selected *int) ResourceInstanceITF {
return nil
}
func (w *WorkflowResource) ConvertToPricedResource(t tools.DataType, selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int, selectedBookingModeIndex *int, request *tools.APIRequest) (pricing.PricedItemITF, error) {
return &PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{
Name: w.Name,
Logo: w.Logo,
ResourceID: w.UUID,
ResourceType: t,
Quantity: 1,
CreatorID: w.CreatorID,
}
}, nil // TODO ???
}
// TODO : as instanciated resource !
+38
View File
@@ -0,0 +1,38 @@
package models
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
func TestModel_ReturnsValidInstances(t *testing.T) {
for name := range models.ModelsCatalog {
t.Run(name, func(t *testing.T) {
modelInt := tools.FromString(name)
obj := models.Model(modelInt)
assert.NotNil(t, obj, "Model() returned nil for valid model name %s", name)
})
}
}
func TestModel_UnknownModelReturnsNil(t *testing.T) {
invalidModelInt := -9999 // unlikely to be valid
obj := models.Model(invalidModelInt)
assert.Nil(t, obj)
}
func TestGetModelsNames_ReturnsAllKeys(t *testing.T) {
names := models.GetModelsNames()
assert.Len(t, names, len(models.ModelsCatalog))
seen := make(map[string]bool)
for _, name := range names {
seen[name] = true
}
for key := range models.ModelsCatalog {
assert.Contains(t, seen, key)
}
}
Regular → Executable
+139 -12
View File
@@ -1,7 +1,10 @@
package utils
import (
"crypto/sha256"
"encoding/json"
"errors"
"slices"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
@@ -28,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"`
@@ -37,11 +41,70 @@ type AbstractObject struct {
UpdaterID string `json:"updater_id,omitempty" bson:"updater_id,omitempty"`
UserUpdaterID string `json:"user_updater_id,omitempty" bson:"user_updater_id,omitempty"`
AccessMode AccessMode `json:"access_mode" bson:"access_mode" default:"0"`
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
}
func (r *AbstractObject) Sign() {
priv, err := tools.LoadKeyFromFilePrivate() // your node private key
if err != nil {
return
}
b, _ := json.Marshal(r.DeepCopy())
hash := sha256.Sum256(b)
r.Signature, err = priv.Sign(hash[:])
}
func (r *AbstractObject) SetID(id string) {
r.UUID = id
}
func (r *AbstractObject) DeepCopy() *AbstractObject {
var obj AbstractObject
b, err := json.Marshal(r)
if err != nil {
return nil
}
if err := json.Unmarshal(b, &obj); err != nil {
return nil
}
return &obj
}
func (r *AbstractObject) SetDraft(draft bool) {
r.IsDraft = draft
}
func (r *AbstractObject) SetName(name string) {
r.Name = name
}
func (r *AbstractObject) GenerateID() {
if r.UUID == "" {
@@ -70,6 +133,10 @@ func (ao AbstractObject) GetID() string {
return ao.UUID
}
func (ao AbstractObject) GetSignature() []byte {
return ao.Signature
}
// GetName implements ShallowDBObject.
func (ao AbstractObject) GetName() string {
return ao.Name
@@ -83,17 +150,19 @@ func (ao *AbstractObject) UpToDate(user string, peer string, create bool) {
ao.UpdateDate = time.Now()
ao.UpdaterID = peer
ao.UserUpdaterID = user
if create {
if create && ao.CreatorID != "" {
ao.CreationDate = time.Now()
ao.CreatorID = peer
ao.UserCreatorID = user
}
}
func (ao *AbstractObject) VerifyAuth(request *tools.APIRequest) bool {
return ao.AccessMode == Public || (request != nil && ao.CreatorID == request.PeerID && request.PeerID != "")
func (ao *AbstractObject) VerifyAuth(callName string, request *tools.APIRequest) bool {
return (ao.AccessMode == Public && callName == "get") || (request != nil && (request.Admin || (ao.CreatorID == request.PeerID && request.PeerID != "")))
}
// TODO : check write per auth
func (ao *AbstractObject) GetObjectFilters(search string) *dbs.Filters {
if search == "*" {
search = ""
@@ -123,50 +192,108 @@ func (dma *AbstractObject) Serialize(obj DBObject) map[string]interface{} {
return m
}
type AbstractAccessor struct {
type AbstractAccessor[T DBObject] struct {
Logger zerolog.Logger // Logger is the logger of the accessor, it's a specilized logger for the accessor
Type tools.DataType // Type is the data type of the accessor
Request *tools.APIRequest // Caller is the http caller of the accessor (optionnal) only need in a peer connection
ResourceModelAccessor Accessor
New func() T
NotImplemented []string
}
func (r *AbstractAccessor) ShouldVerifyAuth() bool {
func (r *AbstractAccessor[T]) NewObj() DBObject {
return r.New()
}
func (r *AbstractAccessor[T]) ShouldVerifyAuth() bool {
return true
}
func (r *AbstractAccessor) GetRequest() *tools.APIRequest {
func (r *AbstractAccessor[T]) GetRequest() *tools.APIRequest {
return r.Request
}
func (dma *AbstractAccessor) GetUser() string {
func (dma *AbstractAccessor[T]) GetUser() string {
if dma.Request == nil {
return ""
}
return dma.Request.Username
}
func (dma *AbstractAccessor) GetPeerID() string {
func (dma *AbstractAccessor[T]) GetPeerID() string {
if dma.Request == nil {
return ""
}
return dma.Request.PeerID
}
func (dma *AbstractAccessor) GetGroups() []string {
func (dma *AbstractAccessor[T]) GetGroups() []string {
if dma.Request == nil {
return []string{}
}
return dma.Request.Groups
}
func (dma *AbstractAccessor) GetLogger() *zerolog.Logger {
func (dma *AbstractAccessor[T]) GetLogger() *zerolog.Logger {
return &dma.Logger
}
func (dma *AbstractAccessor) GetType() tools.DataType {
func (dma *AbstractAccessor[T]) GetType() tools.DataType {
return dma.Type
}
func (dma *AbstractAccessor) GetCaller() *tools.HTTPCaller {
func (dma *AbstractAccessor[T]) GetCaller() *tools.HTTPCaller {
if dma.Request == nil {
return nil
}
return dma.Request.Caller
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *AbstractAccessor[T]) DeleteOne(id string) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "DeleteOne") {
return nil, 404, errors.New("not implemented")
}
return GenericDeleteOne(id, a)
}
func (a *AbstractAccessor[T]) UpdateOne(set map[string]interface{}, id string) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "UpdateOne") {
return nil, 404, errors.New("not implemented")
}
// should verify if a source is existing...
return GenericUpdateOne(set, id, a)
}
func (a *AbstractAccessor[T]) StoreOne(data DBObject) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "StoreOne") {
return nil, 404, errors.New("not implemented")
}
return GenericStoreOne(data.(T), a)
}
func (a *AbstractAccessor[T]) CopyOne(data DBObject) (DBObject, int, error) {
if len(a.NotImplemented) > 0 && slices.Contains(a.NotImplemented, "CopyOne") {
return nil, 404, errors.New("not implemented")
}
return GenericStoreOne(data.(T), a)
}
func (a *AbstractAccessor[T]) LoadOne(id string) (DBObject, int, error) {
return GenericLoadOne(id, a.New(), func(d DBObject) (DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *AbstractAccessor[T]) LoadAll(isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error) {
return GenericLoadAll[T](a.GetExec(isDraft), isDraft, a, offset, limit)
}
func (a *AbstractAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool, offset int64, limit int64) ([]ShallowDBObject, int, error) {
return GenericSearch[T](filters, search, a.New().GetObjectFilters(search), a.GetExec(isDraft), isDraft, a, offset, limit)
}
func (a *AbstractAccessor[T]) GetExec(isDraft bool) func(DBObject) ShallowDBObject {
return func(d DBObject) ShallowDBObject {
return d
}
}
+60
View File
@@ -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:
}
}
}
Regular → Executable
+150 -41
View File
@@ -1,10 +1,13 @@
package utils
import (
"encoding/json"
"errors"
"os"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/dbs/mongo"
"github.com/google/uuid"
mgb "go.mongodb.org/mongo-driver/mongo"
)
@@ -14,11 +17,14 @@ 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
}
if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
return errors.New("you are not allowed to access :" + a.GetType().String())
}
return nil
@@ -26,9 +32,13 @@ func VerifyAccess(a Accessor, id string) error {
// GenericLoadOne loads one object from the database (generic)
func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
data.GenerateID()
if data.GetID() == "" {
data.GenerateID()
}
data.StoreDraftDefault()
data.UpToDate(a.GetUser(), a.GetPeerID(), true)
data.Unsign()
data.Sign()
f := dbs.Filters{
Or: map[string][]dbs.Filter{
"abstractresource.abstractobject.name": {{
@@ -41,82 +51,128 @@ func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
}},
},
}
if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
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)
if err != nil {
return nil, 422, err
return nil, 422, errors.New("error when validating the received struct: " + err.Error())
}
id, code, err := mongo.MONGOService.StoreOne(data, data.GetID(), a.GetType().String())
if err != nil {
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)
func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
res, code, err := a.LoadOne(id)
if err != nil {
return res, code, err
}
if res == nil {
return res, code, errors.New("not found")
}
return GenericDelete(res, a)
}
func GenericDelete(res DBObject, a Accessor) (DBObject, int, error) {
if !res.CanDelete() {
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(a.GetRequest()) {
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
}
func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, map[string]interface{}, int, error) {
r, c, err := a.LoadOne(id)
if err != nil {
return nil, nil, c, err
}
obj := a.NewObj()
b, _ := json.Marshal(r)
json.Unmarshal(b, obj)
if change["is_draft"] == true {
obj.SetDraft(change["is_draft"] == true)
}
if !a.GetRequest().Admin {
var ok bool
ok, r = r.CanUpdate(obj)
if !ok {
return nil, nil, 403, errors.New("you are not allowed to update :" + a.GetType().String())
}
if a.ShouldVerifyAuth() && !r.VerifyAuth("update", a.GetRequest()) {
return nil, nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
}
}
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
if a.GetPeerID() == r.GetCreatorID() {
r.Unsign()
r.Sign()
}
loaded := r.Serialize(r) // get the loaded object
for k, v := range change { // apply the changes, with a flatten method
loaded[k] = v
}
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(set DBObject, id string, a Accessor, new DBObject) (DBObject, int, error) {
r, c, err := a.LoadOne(id)
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
}
ok, newSet := r.CanUpdate(set)
if !ok {
return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
}
set = newSet
r.UpToDate(a.GetUser(), a.GetPeerID(), false)
if a.ShouldVerifyAuth() && !r.VerifyAuth(a.GetRequest()) {
return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
}
change := set.Serialize(set) // get the changes
loaded := r.Serialize(r) // get the loaded object
for k, v := range change { // apply the changes, with a flatten method
loaded[k] = v
}
id, code, err := mongo.MONGOService.UpdateOne(new.Deserialize(loaded, new), id, a.GetType().String())
id, code, err := mongo.MONGOService.UpdateOne(obj.Deserialize(loaded, obj), id, a.GetType().String())
if err != nil {
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, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
var data T
func GenericLoadOne[T DBObject](id string, data T, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String())
if err != nil {
return nil, code, err
}
res_mongo.Decode(&data)
if a.ShouldVerifyAuth() && !data.VerifyAuth(a.GetRequest()) {
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())
}
return f(data)
@@ -132,7 +188,7 @@ func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft
return nil, 404, err
}
for _, r := range results {
if (a.ShouldVerifyAuth() && !r.VerifyAuth(a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) {
if (a.ShouldVerifyAuth() && !r.VerifyAuth("get", a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) {
continue
}
objs = append(objs, f(r))
@@ -140,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)
}
@@ -162,5 +228,48 @@ 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) {
datas, _, _ := wfa.Search(&dbs.Filters{
And: map[string][]dbs.Filter{
"relation": {{Operator: dbs.EQUAL.String(), Value: 1}},
},
}, "", false, 0, 1)
if len(datas) > 0 && datas[0] != nil {
return datas[0], nil
}
return nil, errors.New("peer not found")
}
func IsMySelf(peerID string, wfa Accessor) (bool, string) {
pp, err := GetMySelf(wfa)
if err != nil || pp == nil {
return false, ""
}
return peerID == pp.GetID(), pp.GetID()
}
func GenerateNodeID() (string, error) {
folderStatic := "/var/lib/opencloud-node"
if _, err := os.Stat(folderStatic); err == nil {
os.MkdirAll(folderStatic, 0644)
}
folderStatic += "/node_id"
if _, err := os.Stat(folderStatic); os.IsNotExist(err) {
hostname, err := os.Hostname()
if err != nil {
return "", err
}
id := uuid.NewSHA1(uuid.NameSpaceOID, []byte("oc-"+hostname))
err = os.WriteFile(folderStatic, []byte(id.String()), 0644)
return id.String(), err
}
data, err := os.ReadFile(folderStatic)
return string(data), err
}
Regular → Executable
+17 -4
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,22 +19,33 @@ 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()
GetCreatorID() string
UpToDate(user string, peer string, create bool)
CanUpdate(set DBObject) (bool, DBObject)
VerifyAuth(request *tools.APIRequest) bool
VerifyAuth(callName string, request *tools.APIRequest) bool
Serialize(obj DBObject) map[string]interface{}
GetAccessor(request *tools.APIRequest) Accessor
Deserialize(j map[string]interface{}, obj DBObject) DBObject
Sign()
Unsign()
GetSignature() []byte
GetObjectFilters(search string) *dbs.Filters
}
// Accessor is an interface that defines the basic methods for an Accessor
type Accessor interface {
NewObj() DBObject
GetUser() string
GetPeerID() string
GetGroups() []string
@@ -46,7 +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)
UpdateOne(set DBObject, id string) (DBObject, int, error)
Search(filters *dbs.Filters, search string, 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, offset int64, limit int64) ([]ShallowDBObject, int, error)
GetExec(isDraft bool) func(DBObject) ShallowDBObject
}
+263
View File
@@ -0,0 +1,263 @@
package utils_test
import (
"testing"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
// ---- AbstractObject ----
func TestAbstractObject_GetID(t *testing.T) {
obj := &utils.AbstractObject{UUID: "abc-123"}
assert.Equal(t, "abc-123", obj.GetID())
}
func TestAbstractObject_GetName(t *testing.T) {
obj := &utils.AbstractObject{Name: "test-name"}
assert.Equal(t, "test-name", obj.GetName())
}
func TestAbstractObject_GetCreatorID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "peer-xyz"}
assert.Equal(t, "peer-xyz", obj.GetCreatorID())
}
func TestAbstractObject_SetID(t *testing.T) {
obj := &utils.AbstractObject{}
obj.SetID("new-id")
assert.Equal(t, "new-id", obj.UUID)
}
func TestAbstractObject_SetName(t *testing.T) {
obj := &utils.AbstractObject{}
obj.SetName("hello")
assert.Equal(t, "hello", obj.Name)
}
func TestAbstractObject_GenerateID_WhenEmpty(t *testing.T) {
obj := &utils.AbstractObject{}
obj.GenerateID()
assert.NotEmpty(t, obj.UUID)
}
func TestAbstractObject_GenerateID_KeepsExisting(t *testing.T) {
obj := &utils.AbstractObject{UUID: "existing-id"}
obj.GenerateID()
assert.Equal(t, "existing-id", obj.UUID)
}
func TestAbstractObject_StoreDraftDefault(t *testing.T) {
obj := &utils.AbstractObject{IsDraft: true}
obj.StoreDraftDefault()
assert.False(t, obj.IsDraft)
}
func TestAbstractObject_IsDrafted(t *testing.T) {
obj := &utils.AbstractObject{IsDraft: true}
assert.True(t, obj.IsDrafted())
obj.IsDraft = false
assert.False(t, obj.IsDrafted())
}
func TestAbstractObject_CanDelete(t *testing.T) {
obj := &utils.AbstractObject{}
assert.True(t, obj.CanDelete())
}
func TestAbstractObject_CanUpdate(t *testing.T) {
obj := &utils.AbstractObject{UUID: "id-1"}
other := &utils.AbstractObject{UUID: "id-2"}
ok, returned := obj.CanUpdate(other)
assert.True(t, ok)
assert.Equal(t, other, returned)
}
func TestAbstractObject_Unsign(t *testing.T) {
obj := &utils.AbstractObject{Signature: []byte("sig")}
obj.Unsign()
assert.Nil(t, obj.Signature)
}
func TestAbstractObject_GetSignature(t *testing.T) {
obj := &utils.AbstractObject{Signature: []byte("sig")}
assert.Equal(t, []byte("sig"), obj.GetSignature())
}
func TestAbstractObject_DeepCopy(t *testing.T) {
obj := &utils.AbstractObject{UUID: "id-1", Name: "original"}
copy := obj.DeepCopy()
assert.NotNil(t, copy)
assert.Equal(t, obj.UUID, copy.UUID)
assert.Equal(t, obj.Name, copy.Name)
// Mutating the copy should not affect the original
copy.Name = "modified"
assert.Equal(t, "original", obj.Name)
}
func TestAbstractObject_UpToDate_Create(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: ""}
obj.UpToDate("user1", "peer1", true)
assert.Equal(t, "peer1", obj.UpdaterID)
assert.Equal(t, "user1", obj.UserUpdaterID)
// CreatorID was empty so create branch is skipped
assert.Empty(t, obj.CreatorID)
}
func TestAbstractObject_UpToDate_CreateWithExistingCreator(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "existing-peer"}
obj.UpToDate("user1", "peer1", true)
assert.Equal(t, "peer1", obj.CreatorID)
assert.Equal(t, "user1", obj.UserCreatorID)
}
func TestAbstractObject_UpToDate_Update(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "original-peer"}
obj.UpToDate("user2", "peer2", false)
assert.Equal(t, "peer2", obj.UpdaterID)
assert.Equal(t, "original-peer", obj.CreatorID) // unchanged
}
// ---- VerifyAuth ----
func TestAbstractObject_VerifyAuth_NilRequest_GetPublic(t *testing.T) {
obj := &utils.AbstractObject{AccessMode: 1} // Public = 1
assert.True(t, obj.VerifyAuth("get", nil))
}
func TestAbstractObject_VerifyAuth_NilRequest_DeletePublic(t *testing.T) {
obj := &utils.AbstractObject{AccessMode: 1} // Public = 1
// non-"get" call with nil request → false
assert.False(t, obj.VerifyAuth("delete", nil))
}
func TestAbstractObject_VerifyAuth_NilRequest_Private(t *testing.T) {
obj := &utils.AbstractObject{AccessMode: 0} // Private
assert.False(t, obj.VerifyAuth("get", nil))
}
func TestAbstractObject_VerifyAuth_AdminRequest(t *testing.T) {
obj := &utils.AbstractObject{}
req := &tools.APIRequest{Admin: true}
assert.True(t, obj.VerifyAuth("get", req))
assert.True(t, obj.VerifyAuth("delete", req))
}
func TestAbstractObject_VerifyAuth_MatchingPeerID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "peer-abc"}
req := &tools.APIRequest{PeerID: "peer-abc"}
assert.True(t, obj.VerifyAuth("get", req))
}
func TestAbstractObject_VerifyAuth_MismatchedPeerID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: "peer-abc"}
req := &tools.APIRequest{PeerID: "peer-xyz"}
assert.False(t, obj.VerifyAuth("get", req))
}
func TestAbstractObject_VerifyAuth_EmptyPeerID(t *testing.T) {
obj := &utils.AbstractObject{CreatorID: ""}
req := &tools.APIRequest{PeerID: ""}
// both empty → condition `ao.CreatorID == request.PeerID && request.PeerID != ""` is false
assert.False(t, obj.VerifyAuth("get", req))
}
// ---- GetObjectFilters ----
func TestAbstractObject_GetObjectFilters_Star(t *testing.T) {
obj := &utils.AbstractObject{}
f := obj.GetObjectFilters("*")
assert.NotNil(t, f)
}
func TestAbstractObject_GetObjectFilters_Search(t *testing.T) {
obj := &utils.AbstractObject{}
f := obj.GetObjectFilters("my-search")
assert.NotNil(t, f)
}
// ---- Serialize / Deserialize ----
func TestAbstractObject_SerializeDeserialize(t *testing.T) {
obj := &utils.AbstractObject{UUID: "serial-id", Name: "serial-name"}
m := obj.Serialize(obj)
assert.NotNil(t, m)
dst := &utils.AbstractObject{}
result := obj.Deserialize(m, dst)
assert.NotNil(t, result)
assert.Equal(t, "serial-id", result.GetID())
}
// ---- GetAccessor ----
func TestAbstractObject_GetAccessor_ReturnsNil(t *testing.T) {
obj := &utils.AbstractObject{}
acc := obj.GetAccessor(nil)
assert.Nil(t, acc)
}
// ---- AbstractAccessor ----
func TestAbstractAccessor_GetUser_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Equal(t, "", acc.GetUser())
}
func TestAbstractAccessor_GetUser_WithRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Request: &tools.APIRequest{Username: "alice"},
}
assert.Equal(t, "alice", acc.GetUser())
}
func TestAbstractAccessor_GetPeerID_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Equal(t, "", acc.GetPeerID())
}
func TestAbstractAccessor_GetPeerID_WithRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Request: &tools.APIRequest{PeerID: "peer-42"},
}
assert.Equal(t, "peer-42", acc.GetPeerID())
}
func TestAbstractAccessor_GetGroups_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Equal(t, []string{}, acc.GetGroups())
}
func TestAbstractAccessor_GetGroups_WithRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Request: &tools.APIRequest{Groups: []string{"g1", "g2"}},
}
assert.Equal(t, []string{"g1", "g2"}, acc.GetGroups())
}
func TestAbstractAccessor_ShouldVerifyAuth(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{}
assert.True(t, acc.ShouldVerifyAuth())
}
func TestAbstractAccessor_GetType(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{
Type: tools.WORKFLOW,
}
assert.Equal(t, tools.WORKFLOW, acc.GetType())
}
func TestAbstractAccessor_GetRequest(t *testing.T) {
req := &tools.APIRequest{Admin: true}
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: req}
assert.Equal(t, req, acc.GetRequest())
}
func TestAbstractAccessor_GetCaller_NilRequest(t *testing.T) {
acc := &utils.AbstractAccessor[*utils.AbstractObject]{Request: nil}
assert.Nil(t, acc.GetCaller())
}
+100 -36
View File
@@ -1,8 +1,6 @@
package graph
import (
"time"
"cloud.o-forge.io/core/oc-lib/models/resources"
"cloud.o-forge.io/core/oc-lib/tools"
)
@@ -15,6 +13,15 @@ type Graph struct {
Links []GraphLink `bson:"links" json:"links" default:"{}" validate:"required"` // Links is the list of links between elements in the graph
}
func NewGraph() *Graph {
return &Graph{
Partial: false,
Zoom: 1,
Items: map[string]GraphItem{},
Links: []GraphLink{},
}
}
func (g *Graph) Clear(id string) {
realItems := map[string]GraphItem{}
for k, it := range g.Items {
@@ -38,6 +45,14 @@ 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
}
func (wf *Graph) IsCompute(item GraphItem) bool {
return item.Compute != nil
}
@@ -54,49 +69,43 @@ func (wf *Graph) IsWorkflow(item GraphItem) bool {
return item.Workflow != nil
}
func (g *Graph) GetAverageTimeRelatedToProcessingActivity(start time.Time, processings []*resources.ProcessingResource, resource resources.ResourceInterface,
f func(GraphItem) resources.ResourceInterface, request *tools.APIRequest) (float64, float64) {
nearestStart := float64(10000000000)
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
longestDuration := float64(0)
for _, link := range g.Links {
for _, processing := range processings {
var source string // source is the source of the link
if link.Destination.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID() { // if the destination is the processing and the source is not a compute
source = link.Source.ID
} else if link.Source.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID() { // if the source is the processing and the destination is not a compute
source = link.Destination.ID
if !(link.Destination.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID()) &&
!(link.Source.ID == processing.GetID() && f(g.Items[link.Source.ID]) != nil && f(g.Items[link.Source.ID]).GetID() == resource.GetID()) {
continue
}
priced := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, request)
if source != "" {
if priced.GetLocationStart() != nil {
near := float64(priced.GetLocationStart().Sub(start).Seconds())
if near < nearestStart {
nearestStart = near
}
}
if priced.GetLocationEnd() != nil {
duration := float64(priced.GetLocationEnd().Sub(*priced.GetLocationStart()).Seconds())
if longestDuration < duration {
longestDuration = duration
}
} else {
oneIsInfinite = true
}
priced, err := processing.ConvertToPricedResource(tools.PROCESSING_RESOURCE, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil {
return 0, 0, err
}
duration := priced.GetExplicitDurationInS()
if duration < 0 {
oneIsInfinite = true
} else if longestDuration < duration {
longestDuration = duration
}
}
}
if oneIsInfinite {
return nearestStart, -1
return 0, -1, nil
}
return nearestStart, longestDuration
return 0, longestDuration, nil
}
/*
* GetAverageTimeBeforeStart is a function that returns the average time before the start of a processing
*/
func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingID string, request *tools.APIRequest) float64 {
func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingID string,
instance int, partnership int, buying int, strategy int, bookingMode int, request *tools.APIRequest) (float64, error) {
currents := []float64{} // list of current time
for _, link := range g.Links { // for each link
var source string // source is the source of the link
@@ -112,13 +121,20 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
if r == nil { // if item is nil, continue
continue
}
priced := r.ConvertToPricedResource(dt, request)
priced, err := r.ConvertToPricedResource(dt, &instance, &partnership, &buying, &strategy, &bookingMode, request)
if err != nil {
return 0, err
}
current := priced.GetExplicitDurationInS() // get the explicit duration of the item
if current < 0 { // if current is negative, its means that duration of a before could be infinite continue
return current
return current, nil
}
current += g.GetAverageTimeProcessingBeforeStart(current, source, request) // get the average time before start of the source
currents = append(currents, current) // append the current to the currents
add, err := g.GetAverageTimeProcessingBeforeStart(current, source, instance, partnership, buying, strategy, bookingMode, request)
if err != nil {
return 0, err
}
current += add // get the average time before start of the source
currents = append(currents, current) // append the current to the currents
}
var max float64 // get the max time to wait dependancies to finish
for _, current := range currents {
@@ -126,12 +142,58 @@ func (g *Graph) GetAverageTimeProcessingBeforeStart(average float64, processingI
max = current
}
}
return max
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.Data != nil {
if item.NativeTool != nil {
return tools.NATIVE_TOOL, item.NativeTool
} else if item.Data != nil {
return tools.DATA_RESOURCE, item.Data
} else if item.Compute != nil {
return tools.COMPUTE_RESOURCE, item.Compute
@@ -141,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
}

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