omni_orchestrator/schemas/v1/api/
apps.rs

1//! Application management module for handling CRUD operations on applications.
2//!
3//! This module provides a REST API for managing applications, including:
4//! - Listing applications
5//! - Creating new applications
6//! - Updating existing applications
7//! - Getting application details and statistics
8//! - Starting and stopping applications
9//! - Scaling applications
10//! - Deleting applications
11//! - Releasing new versions of applications
12
13use super::super::db::queries as db;
14use rocket::http::Status;
15use rocket::serde::json::{json, Json, Value};
16use rocket::{delete, get, http::ContentType, post, put, Data, State};
17use serde::{Deserialize, Serialize};
18use std::sync::Arc;
19
20// Import DatabaseManager
21use crate::DatabaseManager;
22
23use libomni::types::db::v1 as types;
24use types::app::{App, AppWithInstances};
25
26// TODO: @tristanpoland Review if we actually need this or should drop in favor of using a central struct. Regardless we will need to move these to the modals module and eventually to LibOmni.
27
28/// Represents an application in the system.
29#[derive(Debug, Serialize, Deserialize, Clone)]
30pub struct Application {
31    /// Unique identifier for the application
32    id: String,
33    /// Name of the application
34    name: String,
35    /// Owner of the application
36    owner: String,
37    /// Number of running instances
38    instances: i64,
39    /// Memory allocation in MB
40    memory: i64,
41    /// Current status of the application
42    status: String,
43    /// Creation timestamp
44    created_at: chrono::DateTime<chrono::Utc>,
45    /// Last update timestamp
46    updated_at: chrono::DateTime<chrono::Utc>,
47}
48
49/// Request data for scaling an application.
50#[derive(Debug, Serialize, Deserialize)]
51pub struct ScaleRequest {
52    /// Number of instances to scale to
53    instances: i32,
54    /// Memory allocation in MB to scale to
55    memory: i32,
56}
57
58/// Statistics for an application's resource usage and performance.
59#[derive(Debug, Serialize, Deserialize)]
60pub struct AppStats {
61    /// CPU usage as a percentage
62    cpu_usage: f64,
63    /// Memory usage in bytes
64    memory_usage: i64,
65    /// Disk usage in bytes
66    disk_usage: i64,
67    /// Average number of requests per second
68    requests_per_second: f64,
69    /// Average response time in milliseconds
70    response_time_ms: i64,
71}
72
73/// Request data for creating a new application.
74#[derive(Debug, Serialize, Deserialize)]
75pub struct CreateAppRequest {
76    /// Name of the application
77    name: String,
78    /// Memory allocation in MB
79    memory: i64,
80    /// Number of instances
81    instances: i64,
82    /// Organization ID that owns the application
83    org_id: i64,
84}
85
86/// Request data for updating an existing application.
87#[derive(Debug, Serialize, Deserialize)]
88pub struct UpdateAppRequest {
89    /// New name for the application
90    name: String,
91    /// New memory allocation in MB
92    memory: i64,
93    /// New number of instances
94    instances: i64,
95    /// Organization ID that owns the application
96    org_id: i64,
97}
98
99/// List all applications with pagination support.
100///
101/// # Arguments
102///
103/// * `platform_id` - Platform identifier
104/// * `page` - Required page number for pagination
105/// * `per_page` - Required number of items per page
106/// * `db_manager` - Database manager for accessing platform-specific pools
107///
108/// # Returns
109///
110/// A JSON array of applications or an error if pagination parameters are missing
111#[get("/platform/<platform_id>/apps?<page>&<per_page>")]
112pub async fn list_apps(
113    platform_id: i64,
114    page: Option<i64>,
115    per_page: Option<i64>,
116    db_manager: &State<Arc<DatabaseManager>>,
117) -> Result<Json<Value>, (Status, Json<Value>)> {
118    // Get platform information from main database
119    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
120        Ok(platform) => platform,
121        Err(_) => {
122            return Err((
123                Status::NotFound,
124                Json(json!({
125                    "error": "Platform not found",
126                    "message": format!("Platform with ID {} does not exist", platform_id)
127                }))
128            ));
129        }
130    };
131
132    // Get platform-specific database pool
133    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
134        Ok(pool) => pool,
135        Err(_) => {
136            return Err((
137                Status::InternalServerError,
138                Json(json!({
139                    "error": "Database error",
140                    "message": "Failed to connect to platform database"
141                }))
142            ));
143        }
144    };
145
146    match (page, per_page) {
147        (Some(p), Some(pp)) => {
148            let apps = match db::app::list_apps(&pool, p, pp).await {
149                Ok(apps) => apps,
150                Err(_) => {
151                    return Err((
152                        Status::InternalServerError,
153                        Json(json!({
154                            "error": "Database error",
155                            "message": "Failed to retrieve applications"
156                        }))
157                    ));
158                }
159            };
160            
161            let total_count = match db::app::count_apps(&pool).await {
162                Ok(count) => count,
163                Err(_) => {
164                    return Err((
165                        Status::InternalServerError,
166                        Json(json!({
167                            "error": "Database error",
168                            "message": "Failed to count applications"
169                        }))
170                    ));
171                }
172            };
173            
174            let total_pages = (total_count / pp);
175
176            let response = json!({
177                "apps": apps,
178                "pagination": {
179                    "page": p,
180                    "per_page": pp,
181                    "total_count": total_count,
182                    "total_pages": total_pages
183                }
184            });
185
186            Ok(Json(response))
187        }
188        _ => Err((
189            Status::BadRequest,
190            Json(json!({
191                "error": "Missing pagination parameters",
192                "message": "Please provide both 'page' and 'per_page' parameters"
193            }))
194        ))
195    }
196}
197
198/// Get app with instances
199#[get("/platform/<platform_id>/app_with_instances/<app_id>")]
200pub async fn get_app_with_instances(
201    db_manager: &State<Arc<DatabaseManager>>, 
202    platform_id: i64,
203    app_id: i64
204) -> Result<Json<AppWithInstances>, (Status, Json<Value>)> {
205    // Get platform information
206    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
207        Ok(platform) => platform,
208        Err(_) => {
209            return Err((
210                Status::NotFound,
211                Json(json!({
212                    "error": "Platform not found",
213                    "message": format!("Platform with ID {} does not exist", platform_id)
214                }))
215            ));
216        }
217    };
218
219    // Get platform-specific database pool
220    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
221        Ok(pool) => pool,
222        Err(_) => {
223            return Err((
224                Status::InternalServerError,
225                Json(json!({
226                    "error": "Database error",
227                    "message": "Failed to connect to platform database"
228                }))
229            ));
230        }
231    };
232
233    match db::app::get_app_with_instances(&pool, app_id).await {
234        Ok(app_with_instances) => {
235            Ok(Json(app_with_instances))
236        }
237        Err(_) => Err((
238            Status::InternalServerError,
239            Json(json!({
240                "error": "Failed to fetch app with instances",
241                "message": "An error occurred while retrieving the application data"
242            })),
243        )),
244    }
245}
246
247/// Count the total number of applications.
248#[get("/platform/<platform_id>/app-count")]
249pub async fn count_apps(
250    platform_id: i64,
251    db_manager: &State<Arc<DatabaseManager>>
252) -> Result<Json<i64>, (Status, Json<Value>)> {
253    // Get platform information
254    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
255        Ok(platform) => platform,
256        Err(_) => {
257            return Err((
258                Status::NotFound,
259                Json(json!({
260                    "error": "Platform not found",
261                    "message": format!("Platform with ID {} does not exist", platform_id)
262                }))
263            ));
264        }
265    };
266
267    // Get platform-specific database pool
268    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
269        Ok(pool) => pool,
270        Err(_) => {
271            return Err((
272                Status::InternalServerError,
273                Json(json!({
274                    "error": "Database error",
275                    "message": "Failed to connect to platform database"
276                }))
277            ));
278        }
279    };
280
281    match db::app::count_apps(&pool).await {
282        Ok(count) => Ok(Json(count)),
283        Err(_) => Err((
284            Status::InternalServerError,
285            Json(json!({
286                "error": "Database error",
287                "message": "Failed to count applications"
288            }))
289        )),
290    }
291}
292
293/// Get a specific application by ID.
294///
295/// # Arguments
296///
297/// * `platform_id` - Platform identifier
298/// * `app_id` - The ID of the application to retrieve
299/// * `db_manager` - Database manager for accessing platform-specific pools
300///
301/// # Returns
302///
303/// The application if found, or None if not found
304#[get("/platform/<platform_id>/apps/<app_id>")]
305pub async fn get_app(
306    platform_id: i64,
307    app_id: i64, 
308    db_manager: &State<Arc<DatabaseManager>>
309) -> Result<Json<App>, (Status, Json<Value>)> {
310    // Get platform information
311    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
312        Ok(platform) => platform,
313        Err(_) => {
314            return Err((
315                Status::NotFound,
316                Json(json!({
317                    "error": "Platform not found",
318                    "message": format!("Platform with ID {} does not exist", platform_id)
319                }))
320            ));
321        }
322    };
323
324    // Get platform-specific database pool
325    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
326        Ok(pool) => pool,
327        Err(_) => {
328            return Err((
329                Status::InternalServerError,
330                Json(json!({
331                    "error": "Database error",
332                    "message": "Failed to connect to platform database"
333                }))
334            ));
335        }
336    };
337
338    match db::app::get_app_by_id(&pool, app_id).await {
339        Ok(app) => Ok(Json(app)),
340        Err(_) => {
341            Err((
342                Status::NotFound,
343                Json(json!({
344                    "error": "App not found",
345                    "message": format!("App with ID {} could not be found", app_id)
346                }))
347            ))
348        }
349    }
350}
351
352/// Create a new application.
353///
354/// # Arguments
355///
356/// * `platform_id` - Platform identifier
357/// * `app_request` - JSON data containing application details
358/// * `db_manager` - Database manager for accessing platform-specific pools
359///
360/// # Returns
361///
362/// The newly created application
363#[post("/platform/<platform_id>/apps", format = "json", data = "<app_request>")]
364pub async fn create_app(
365    platform_id: i64,
366    app_request: Json<CreateAppRequest>,
367    db_manager: &State<Arc<DatabaseManager>>,
368) -> Result<Json<App>, (Status, Json<Value>)> {
369    // Get platform information
370    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
371        Ok(platform) => platform,
372        Err(_) => {
373            return Err((
374                Status::NotFound,
375                Json(json!({
376                    "error": "Platform not found",
377                    "message": format!("Platform with ID {} does not exist", platform_id)
378                }))
379            ));
380        }
381    };
382
383    // Get platform-specific database pool
384    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
385        Ok(pool) => pool,
386        Err(_) => {
387            return Err((
388                Status::InternalServerError,
389                Json(json!({
390                    "error": "Database error",
391                    "message": "Failed to connect to platform database"
392                }))
393            ));
394        }
395    };
396
397    match db::app::create_app(
398        &pool,
399        &app_request.name,
400        app_request.org_id,
401        None,
402        None,
403        None,
404        None,
405    ).await {
406        Ok(app) => Ok(Json(app)),
407        Err(_) => {
408            Err((
409                Status::InternalServerError,
410                Json(json!({
411                    "error": "Database error",
412                    "message": "Failed to create application"
413                }))
414            ))
415        }
416    }
417}
418
419/// Update an existing application.
420///
421/// # Arguments
422///
423/// * `platform_id` - Platform identifier
424/// * `app_request` - JSON data containing updated application details
425/// * `db_manager` - Database manager for accessing platform-specific pools
426/// * `app_id` - The ID of the application to update
427///
428/// # Returns
429///
430/// The updated application
431#[post("/platform/<platform_id>/apps/<app_id>", format = "json", data = "<app_request>")]
432pub async fn update_app(
433    platform_id: i64,
434    app_request: Json<UpdateAppRequest>,
435    db_manager: &State<Arc<DatabaseManager>>,
436    app_id: i64,
437) -> Result<Json<App>, (Status, Json<Value>)> {
438    // Get platform information
439    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
440        Ok(platform) => platform,
441        Err(_) => {
442            return Err((
443                Status::NotFound,
444                Json(json!({
445                    "error": "Platform not found",
446                    "message": format!("Platform with ID {} does not exist", platform_id)
447                }))
448            ));
449        }
450    };
451
452    // Get platform-specific database pool
453    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
454        Ok(pool) => pool,
455        Err(_) => {
456            return Err((
457                Status::InternalServerError,
458                Json(json!({
459                    "error": "Database error",
460                    "message": "Failed to connect to platform database"
461                }))
462            ));
463        }
464    };
465
466    match db::app::update_app(
467        &pool,
468        app_id,
469        Some(&app_request.name),
470        None,
471        None,
472        None,
473        None,
474        None,
475    ).await {
476        Ok(app) => Ok(Json(app)),
477        Err(_) => {
478            Err((
479                Status::InternalServerError,
480                Json(json!({
481                    "error": "Database error",
482                    "message": "Failed to update application"
483                }))
484            ))
485        }
486    }
487}
488
489/// Get statistics for a specific application.
490///
491/// # Arguments
492///
493/// * `platform_id` - Platform identifier
494/// * `app_id` - The ID of the application to get statistics for
495/// * `db_manager` - Database manager for accessing platform-specific pools
496///
497/// # Returns
498///
499/// Statistics for the application
500#[get("/platform/<platform_id>/apps/<app_id>/stats")]
501pub async fn get_app_stats(
502    platform_id: i64,
503    app_id: String, 
504    db_manager: &State<Arc<DatabaseManager>>
505) -> Result<Json<AppStats>, (Status, Json<Value>)> {
506    // Get platform information
507    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
508        Ok(platform) => platform,
509        Err(_) => {
510            return Err((
511                Status::NotFound,
512                Json(json!({
513                    "error": "Platform not found",
514                    "message": format!("Platform with ID {} does not exist", platform_id)
515                }))
516            ));
517        }
518    };
519
520    // Get platform-specific database pool (we'll need this for future implementations)
521    let _pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
522        Ok(pool) => pool,
523        Err(_) => {
524            return Err((
525                Status::InternalServerError,
526                Json(json!({
527                    "error": "Database error",
528                    "message": "Failed to connect to platform database"
529                }))
530            ));
531        }
532    };
533
534    // For now, return placeholder stats as in the original implementation
535    let app_stats = AppStats {
536        cpu_usage: 0.0,
537        memory_usage: 0,
538        disk_usage: 0,
539        requests_per_second: 0.0,
540        response_time_ms: 0,
541    };
542    Ok(Json(app_stats))
543}
544
545/// Start a specific application.
546///
547/// # Arguments
548///
549/// * `platform_id` - Platform identifier
550/// * `app_id` - The ID of the application to start
551/// * `db_manager` - Database manager for accessing platform-specific pools
552///
553/// # Returns
554///
555/// The updated application if found, or None if not found
556#[put("/platform/<platform_id>/apps/<app_id>/start")]
557pub async fn start_app(
558    platform_id: i64,
559    app_id: String,
560    db_manager: &State<Arc<DatabaseManager>>
561) -> Result<Json<Application>, (Status, Json<Value>)> {
562    // This function is already marked as todo!, but we need to add platform-specific
563    // handling for future implementation
564    todo!()
565}
566
567/// Stop a specific application.
568///
569/// # Arguments
570///
571/// * `platform_id` - Platform identifier
572/// * `app_id` - The ID of the application to stop
573/// * `db_manager` - Database manager for accessing platform-specific pools
574///
575/// # Returns
576///
577/// The updated application if found, or None if not found
578#[put("/platform/<platform_id>/apps/<app_id>/stop")]
579pub async fn stop_app(
580    platform_id: i64,
581    app_id: String,
582    db_manager: &State<Arc<DatabaseManager>>
583) -> Result<Json<Application>, (Status, Json<Value>)> {
584    // This function is already marked as todo!, but we need to add platform-specific
585    // handling for future implementation
586    todo!()
587}
588
589/// Scale a specific application.
590///
591/// # Arguments
592///
593/// * `platform_id` - Platform identifier
594/// * `app_id` - The ID of the application to scale
595/// * `scale` - JSON data containing scaling parameters
596/// * `db_manager` - Database manager for accessing platform-specific pools
597///
598/// # Returns
599///
600/// The updated application if found, or None if not found
601#[put("/platform/<platform_id>/apps/<app_id>/scale", format = "json", data = "<scale>")]
602pub async fn scale_app(
603    platform_id: i64,
604    app_id: String, 
605    scale: Json<ScaleRequest>,
606    db_manager: &State<Arc<DatabaseManager>>
607) -> Result<Json<Application>, (Status, Json<Value>)> {
608    // This function is already marked as todo!, but we need to add platform-specific
609    // handling for future implementation
610    todo!()
611}
612
613/// Delete a specific application.
614///
615/// # Arguments
616///
617/// * `platform_id` - Platform identifier
618/// * `app_id` - The ID of the application to delete
619/// * `db_manager` - Database manager for accessing platform-specific pools
620///
621/// # Returns
622///
623/// A JSON response indicating success or an error message
624#[delete("/platform/<platform_id>/apps/<app_id>")]
625pub async fn delete_app(
626    platform_id: i64,
627    app_id: String,
628    db_manager: &State<Arc<DatabaseManager>>,
629) -> Result<Json<Value>, (Status, Json<Value>)> {
630    // Get platform information
631    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
632        Ok(platform) => platform,
633        Err(_) => {
634            return Err((
635                Status::NotFound,
636                Json(json!({
637                    "error": "Platform not found",
638                    "message": format!("Platform with ID {} does not exist", platform_id)
639                }))
640            ));
641        }
642    };
643
644    // Get platform-specific database pool
645    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
646        Ok(pool) => pool,
647        Err(_) => {
648            return Err((
649                Status::InternalServerError,
650                Json(json!({
651                    "error": "Database error",
652                    "message": "Failed to connect to platform database"
653                }))
654            ));
655        }
656    };
657
658    match app_id.parse::<i64>() {
659        Ok(id) => {
660            match db::app::delete_app(&pool, id).await {
661                Ok(_) => Ok(Json(json!({ "status": "deleted" }))),
662                Err(_) => {
663                    Err((
664                        Status::InternalServerError,
665                        Json(json!({
666                            "error": "Database error",
667                            "message": "Failed to delete application"
668                        }))
669                    ))
670                }
671            }
672        }
673        Err(e) => {
674            Err((
675                Status::BadRequest,
676                Json(json!({
677                    "error": "Invalid ID format",
678                    "message": format!("The application ID must be a valid integer: {}", e)
679                }))
680            ))
681        }
682    }
683}
684
685/// Releases a new version of the target application by uploading an artifact.
686/// TODO: @tristanpoland Review if we actually need this or should drop in favor
687///       of using the deploy route.
688/// 
689/// # Arguments
690///
691/// * `platform_id` - Platform identifier
692/// * `app_id` - The ID of the application to release a new version for
693/// * `release_version` - The version tag for this release
694/// * `content_type` - The content type of the data being uploaded
695/// * `data` - The data stream of the artifact being uploaded
696/// * `db_manager` - Database manager for accessing platform-specific pools
697///
698/// # Returns
699///
700/// * `Status::Ok` - If the artifact is successfully uploaded and added to the build jobs list
701/// * `Status::BadRequest` - If there is an error in the upload process
702///
703/// # Details
704///
705/// This route handles the release of a new version of an application by:
706/// 1. Uploading the provided artifact to the build artifacts list.
707/// 2. Adding the artifact to the list of build jobs for the Forge instances to pick up and process.
708///
709/// The actual implementation of the release process is delegated to the `helpers::release::release`
710/// function, as it is quite extensive.
711#[post(
712    "/platform/<platform_id>/apps/<app_id>/releases/<release_version>/upload",
713    format = "multipart/form-data",
714    data = "<data>"
715)]
716pub async fn release(
717    platform_id: i64,
718    app_id: String,
719    release_version: String,
720    content_type: &ContentType,
721    data: Data<'_>,
722    db_manager: &State<Arc<DatabaseManager>>,
723) -> Result<Status, Status> {
724    // We need to modify the helper function to work with platform-specific DBs
725    // For now, we'll just pass the helper what it needs, but ideally the helper should be updated to use platform pools
726
727    // Get platform info and pass to helper
728    match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
729        Ok(_) => {
730            // We found the platform, proceed with release
731            super::helpers::release::release(app_id, release_version, content_type, data).await
732        },
733        Err(_) => {
734            // Platform not found
735            Err(Status::NotFound)
736        }
737    }
738}
739
740// List all instances for an application with pagination
741#[get("/platform/<platform_id>/apps/<app_id>/instances?<page>&<per_page>")]
742pub async fn list_instances(
743    platform_id: i64,
744    app_id: i64,
745    page: Option<i64>,
746    per_page: Option<i64>,
747    db_manager: &State<Arc<DatabaseManager>>,
748) -> Result<Json<Value>, (Status, Json<Value>)> {
749    // Get platform information
750    let platform = match db::platforms::get_platform_by_id(db_manager.get_main_pool(), platform_id).await {
751        Ok(platform) => platform,
752        Err(_) => {
753            return Err((
754                Status::NotFound,
755                Json(json!({
756                    "error": "Platform not found",
757                    "message": format!("Platform with ID {} does not exist", platform_id)
758                }))
759            ));
760        }
761    };
762
763    // Get platform-specific database pool
764    let pool = match db_manager.get_platform_pool(&platform.name, platform_id).await {
765        Ok(pool) => pool,
766        Err(_) => {
767            return Err((
768                Status::InternalServerError,
769                Json(json!({
770                    "error": "Database error",
771                    "message": "Failed to connect to platform database"
772                }))
773            ));
774        }
775    };
776
777    match (page, per_page) {
778        (Some(p), Some(pp)) => {
779            let instances = match db::app::list_instances(&pool, app_id, p, pp).await {
780                Ok(instances) => instances,
781                Err(_) => {
782                    return Err((
783                        Status::InternalServerError,
784                        Json(json!({
785                            "error": "Database error",
786                            "message": "Failed to retrieve instances"
787                        }))
788                    ));
789                }
790            };
791            
792            let total_count = match db::app::count_instances_by_app(&pool, app_id).await {
793                Ok(count) => count,
794                Err(_) => {
795                    return Err((
796                        Status::InternalServerError,
797                        Json(json!({
798                            "error": "Database error",
799                            "message": "Failed to count instances"
800                        }))
801                    ));
802                }
803            };
804            
805            let total_pages = (total_count as f64 / pp as f64).ceil() as i64;
806
807            let response = json!({
808                "instances": instances,
809                "pagination": {
810                    "page": p,
811                    "per_page": pp,
812                    "total_count": total_count,
813                    "total_pages": total_pages
814                }
815            });
816
817            Ok(Json(response))
818        }
819        _ => Err((
820            Status::BadRequest,
821            Json(json!({
822                "error": "Missing pagination parameters",
823                "message": "Please provide both 'page' and 'per_page' parameters"
824            }))
825        ))
826    }
827}