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