From 17dc63e3ab20c6e3c8ad73958cb10be21c20f2f4 Mon Sep 17 00:00:00 2001 From: secus Date: Thu, 25 Sep 2025 10:20:22 +0700 Subject: [PATCH 01/14] Update deployment events and webhook support.- Refactor webhook event handling to include scaling, start, stop.- Add support for scaled, start failed, and stop failed events --- ...aaa9b173b1d79686df22b11a59882753b186.json} | 5 +- ...eb3a5212e7a7893f7394fc24b3b4052d82426.json | 41 -- ...c7b551e2d601eb1d99da4ad0a14fb8fa0e188.json | 16 - ...4a86928bbff0f879fa67d4f097ada95e0488d.json | 14 + ...7648634532582f2c2084d52e42925128b531.json} | 4 +- ...daaf44a176487cd4ee6cd084b03a8005b4ffa.json | 14 + ...774879acb4a744567ec840b76e84941277999.json | 22 + ...db55d9d32db25df83457d6e8aa60743d4042a.json | 23 + .../src/pages/WebhooksPage.tsx | 5 + .../src/services/webhookService.ts | 30 +- src/handlers/deployment.rs | 201 +++----- src/handlers/logs.rs | 4 +- src/jobs/deployment_job.rs | 71 +++ src/jobs/deployment_worker.rs | 478 ++++++++++++++++-- src/main.rs | 47 +- src/user/webhook_models.rs | 14 +- 16 files changed, 721 insertions(+), 268 deletions(-) rename .sqlx/{query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json => query-3984b7ad9086f0a2c197ffcb9bebaaa9b173b1d79686df22b11a59882753b186.json} (61%) delete mode 100644 .sqlx/query-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json delete mode 100644 .sqlx/query-6498676b2a50fd55ff699dcded4c7b551e2d601eb1d99da4ad0a14fb8fa0e188.json create mode 100644 .sqlx/query-7f4c8eac5661403cd50781fcf994a86928bbff0f879fa67d4f097ada95e0488d.json rename .sqlx/{query-03a5a5802e51c36a16ca9bd7a87f8a28bc0c7615cb537231059018ee61bc298e.json => query-aa373dd91f3436b167a44c4e5cd47648634532582f2c2084d52e42925128b531.json} (56%) create mode 100644 .sqlx/query-baf2e9ba29ddba5d24827efbeacdaaf44a176487cd4ee6cd084b03a8005b4ffa.json create mode 100644 .sqlx/query-c7fb0022cb8510299939f563b58774879acb4a744567ec840b76e84941277999.json create mode 100644 .sqlx/query-fce71b9417a4b55e2300fbe6f47db55d9d32db25df83457d6e8aa60743d4042a.json diff --git a/.sqlx/query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json b/.sqlx/query-3984b7ad9086f0a2c197ffcb9bebaaa9b173b1d79686df22b11a59882753b186.json similarity index 61% rename from .sqlx/query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json rename to .sqlx/query-3984b7ad9086f0a2c197ffcb9bebaaa9b173b1d79686df22b11a59882753b186.json index 2d15cc8..2a55372 100644 --- a/.sqlx/query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json +++ b/.sqlx/query-3984b7ad9086f0a2c197ffcb9bebaaa9b173b1d79686df22b11a59882753b186.json @@ -1,15 +1,14 @@ { "db_name": "PostgreSQL", - "query": "UPDATE deployments SET status = 'failed', error_message = $1, updated_at = NOW() WHERE id = $2", + "query": "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue start operation' WHERE id = $1", "describe": { "columns": [], "parameters": { "Left": [ - "Text", "Uuid" ] }, "nullable": [] }, - "hash": "3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed" + "hash": "3984b7ad9086f0a2c197ffcb9bebaaa9b173b1d79686df22b11a59882753b186" } diff --git a/.sqlx/query-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json b/.sqlx/query-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json deleted file mode 100644 index 4993d48..0000000 --- a/.sqlx/query-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, app_name, status, replicas FROM deployments WHERE id = $1 AND user_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "app_name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "replicas", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426" -} diff --git a/.sqlx/query-6498676b2a50fd55ff699dcded4c7b551e2d601eb1d99da4ad0a14fb8fa0e188.json b/.sqlx/query-6498676b2a50fd55ff699dcded4c7b551e2d601eb1d99da4ad0a14fb8fa0e188.json deleted file mode 100644 index 4058625..0000000 --- a/.sqlx/query-6498676b2a50fd55ff699dcded4c7b551e2d601eb1d99da4ad0a14fb8fa0e188.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE deployments \n SET replicas = $1, status = 'scaling', updated_at = NOW()\n WHERE id = $2 AND user_id = $3\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Uuid", - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "6498676b2a50fd55ff699dcded4c7b551e2d601eb1d99da4ad0a14fb8fa0e188" -} diff --git a/.sqlx/query-7f4c8eac5661403cd50781fcf994a86928bbff0f879fa67d4f097ada95e0488d.json b/.sqlx/query-7f4c8eac5661403cd50781fcf994a86928bbff0f879fa67d4f097ada95e0488d.json new file mode 100644 index 0000000..b5a44b3 --- /dev/null +++ b/.sqlx/query-7f4c8eac5661403cd50781fcf994a86928bbff0f879fa67d4f097ada95e0488d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue scale operation' WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "7f4c8eac5661403cd50781fcf994a86928bbff0f879fa67d4f097ada95e0488d" +} diff --git a/.sqlx/query-03a5a5802e51c36a16ca9bd7a87f8a28bc0c7615cb537231059018ee61bc298e.json b/.sqlx/query-aa373dd91f3436b167a44c4e5cd47648634532582f2c2084d52e42925128b531.json similarity index 56% rename from .sqlx/query-03a5a5802e51c36a16ca9bd7a87f8a28bc0c7615cb537231059018ee61bc298e.json rename to .sqlx/query-aa373dd91f3436b167a44c4e5cd47648634532582f2c2084d52e42925128b531.json index 2cb84b6..f8a0916 100644 --- a/.sqlx/query-03a5a5802e51c36a16ca9bd7a87f8a28bc0c7615cb537231059018ee61bc298e.json +++ b/.sqlx/query-aa373dd91f3436b167a44c4e5cd47648634532582f2c2084d52e42925128b531.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE deployments SET status = 'running', updated_at = NOW() WHERE id = $1", + "query": "UPDATE deployments SET status = 'scaling', updated_at = NOW() WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -10,5 +10,5 @@ }, "nullable": [] }, - "hash": "03a5a5802e51c36a16ca9bd7a87f8a28bc0c7615cb537231059018ee61bc298e" + "hash": "aa373dd91f3436b167a44c4e5cd47648634532582f2c2084d52e42925128b531" } diff --git a/.sqlx/query-baf2e9ba29ddba5d24827efbeacdaaf44a176487cd4ee6cd084b03a8005b4ffa.json b/.sqlx/query-baf2e9ba29ddba5d24827efbeacdaaf44a176487cd4ee6cd084b03a8005b4ffa.json new file mode 100644 index 0000000..020dd22 --- /dev/null +++ b/.sqlx/query-baf2e9ba29ddba5d24827efbeacdaaf44a176487cd4ee6cd084b03a8005b4ffa.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue stop operation' WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "baf2e9ba29ddba5d24827efbeacdaaf44a176487cd4ee6cd084b03a8005b4ffa" +} diff --git a/.sqlx/query-c7fb0022cb8510299939f563b58774879acb4a744567ec840b76e84941277999.json b/.sqlx/query-c7fb0022cb8510299939f563b58774879acb4a744567ec840b76e84941277999.json new file mode 100644 index 0000000..aae77fc --- /dev/null +++ b/.sqlx/query-c7fb0022cb8510299939f563b58774879acb4a744567ec840b76e84941277999.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT replicas FROM deployments WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "replicas", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c7fb0022cb8510299939f563b58774879acb4a744567ec840b76e84941277999" +} diff --git a/.sqlx/query-fce71b9417a4b55e2300fbe6f47db55d9d32db25df83457d6e8aa60743d4042a.json b/.sqlx/query-fce71b9417a4b55e2300fbe6f47db55d9d32db25df83457d6e8aa60743d4042a.json new file mode 100644 index 0000000..297248e --- /dev/null +++ b/.sqlx/query-fce71b9417a4b55e2300fbe6f47db55d9d32db25df83457d6e8aa60743d4042a.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT app_name FROM deployments WHERE id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "app_name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fce71b9417a4b55e2300fbe6f47db55d9d32db25df83457d6e8aa60743d4042a" +} diff --git a/apps/container-engine-frontend/src/pages/WebhooksPage.tsx b/apps/container-engine-frontend/src/pages/WebhooksPage.tsx index a7b2ba9..356490f 100644 --- a/apps/container-engine-frontend/src/pages/WebhooksPage.tsx +++ b/apps/container-engine-frontend/src/pages/WebhooksPage.tsx @@ -26,6 +26,11 @@ const WEBHOOK_EVENTS = [ { value: 'deployment_completed', label: 'Deployment Completed', description: 'When a deployment finishes successfully' }, { value: 'deployment_failed', label: 'Deployment Failed', description: 'When a deployment fails' }, { value: 'deployment_deleted', label: 'Deployment Deleted', description: 'When a deployment is deleted' }, + { value: 'deployment_scaling', label: 'Deployment Scaling', description: 'When a deployment is being scaled' }, + { value: 'deployment_scaled', label: 'Deployment Scaled', description: 'When a deployment scaling is completed' }, + { value: 'deployment_start_failed', label: 'Deployment Start Failed', description: 'When starting a deployment fails' }, + { value: 'deployment_stop_failed', label: 'Deployment Stop Failed', description: 'When stopping a deployment fails' }, + { value: 'deployment_stopped', label: 'Deployment Stopped', description: 'When a deployment is stopped' }, { value: 'all', label: 'All Events', description: 'Subscribe to all webhook events' }, ]; diff --git a/apps/container-engine-frontend/src/services/webhookService.ts b/apps/container-engine-frontend/src/services/webhookService.ts index 4d0b0b4..c5d0d6f 100644 --- a/apps/container-engine-frontend/src/services/webhookService.ts +++ b/apps/container-engine-frontend/src/services/webhookService.ts @@ -31,6 +31,24 @@ export interface WebhookListResponse { total: number; } +// Helper function to transform string events to enum format expected by backend +const transformEventsForBackend = (events: string[]): string[] => { + const eventMap: Record = { + 'deployment_started': 'DeploymentStarted', + 'deployment_completed': 'DeploymentCompleted', + 'deployment_failed': 'DeploymentFailed', + 'deployment_deleted': 'DeploymentDeleted', + 'deployment_scaling': 'DeploymentScaling', + 'deployment_scaled': 'DeploymentScaled', + 'deployment_start_failed': 'DeploymentStartFailed', + 'deployment_stop_failed': 'DeploymentStopFailed', + 'deployment_stopped': 'DeploymentStopped', + 'all': 'All', + }; + + return events.map(event => eventMap[event] || event); +}; + class WebhookService { async listWebhooks() { const res = await api.get('/v1/webhooks'); @@ -43,12 +61,20 @@ class WebhookService { } async createWebhook(webhook: CreateWebhookRequest) { - const res = await api.post('/v1/webhooks', webhook); + const payload = { + ...webhook, + events: transformEventsForBackend(webhook.events), + }; + const res = await api.post('/v1/webhooks', payload); return res.data; } async updateWebhook(id: string, webhook: UpdateWebhookRequest) { - const res = await api.put(`/v1/webhooks/${id}`, webhook); + const payload = { + ...webhook, + events: webhook.events ? transformEventsForBackend(webhook.events) : undefined, + }; + const res = await api.put(`/v1/webhooks/${id}`, payload); return res.data; } diff --git a/src/handlers/deployment.rs b/src/handlers/deployment.rs index 7ba0a29..f0af0cc 100644 --- a/src/handlers/deployment.rs +++ b/src/handlers/deployment.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use tracing::{error, info, warn}; use uuid::Uuid; use validator::Validate; +use crate::jobs::deployment_job::{DeploymentJob, JobType}; use crate::{ auth::AuthUser, @@ -17,7 +18,6 @@ use crate::{ notifications::NotificationType, services::kubernetes::KubernetesService, AppState, - DeploymentJob }; pub async fn create_deployment( @@ -265,57 +265,51 @@ pub async fn scale_deployment( ) -> Result, AppError> { payload.validate()?; - let result = sqlx::query!( - r#" - UPDATE deployments - SET replicas = $1, status = 'scaling', updated_at = NOW() - WHERE id = $2 AND user_id = $3 - "#, - payload.replicas, + // Get deployment info for creating the job + let deployment = sqlx::query!( + "SELECT app_name FROM deployments WHERE id = $1 AND user_id = $2", deployment_id, user.user_id ) + .fetch_optional(&state.db.pool) + .await? + .ok_or_else(|| AppError::not_found("Deployment"))?; + + // Update status to "scaling" in database first + sqlx::query!( + "UPDATE deployments SET status = 'scaling', updated_at = NOW() WHERE id = $1", + deployment_id + ) .execute(&state.db.pool) .await?; - if result.rows_affected() == 0 { - return Err(AppError::not_found("Deployment")); - } - - // Create Kubernetes service for this deployment's namespace - let k8s_service = KubernetesService::for_deployment(&deployment_id, &user.user_id).await?; - - // Scale the deployment - match k8s_service.scale_deployment(&deployment_id, payload.replicas).await { - Ok(_) => { - // Update status to "running" - sqlx::query!( - "UPDATE deployments SET status = 'running', updated_at = NOW() WHERE id = $1", - deployment_id - ) - .execute(&state.db.pool) - .await?; + // Create scale job + let job = DeploymentJob::new_scale( + deployment_id, + user.user_id, + payload.replicas, + deployment.app_name, + ); - Ok(Json(json!({ - "id": deployment_id, - "replicas": payload.replicas, - "status": "running", - "message": "Deployment scaled successfully" - }))) - } - Err(e) => { - // Update status to failed - sqlx::query!( - "UPDATE deployments SET status = 'failed', error_message = $1, updated_at = NOW() WHERE id = $2", - format!("Failed to scale: {}", e), - deployment_id - ) - .execute(&state.db.pool) - .await?; + // Send job to worker queue + if let Err(_) = state.deployment_sender.send(job).await { + // Rollback status on queue failure + let _ = sqlx::query!( + "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue scale operation' WHERE id = $1", + deployment_id + ) + .execute(&state.db.pool) + .await; - Err(AppError::internal(&format!("Failed to scale deployment: {}", e))) - } + return Err(AppError::internal("Failed to queue scale operation")); } + + Ok(Json(json!({ + "id": deployment_id, + "status": "scaling", + "message": "Scale operation queued", + "target_replicas": payload.replicas + }))) } pub async fn start_deployment( @@ -325,7 +319,7 @@ pub async fn start_deployment( ) -> Result, AppError> { // Check if deployment exists and belongs to user let deployment = sqlx::query!( - "SELECT id, app_name, status, replicas FROM deployments WHERE id = $1 AND user_id = $2", + "SELECT app_name FROM deployments WHERE id = $1 AND user_id = $2", deployment_id, user.user_id ) @@ -341,49 +335,34 @@ pub async fn start_deployment( .execute(&state.db.pool) .await?; - // Create Kubernetes service for user's namespace - let k8s_service = KubernetesService::for_deployment(&deployment_id, &user.user_id).await?; - - // Scale deployment back to desired replicas - let target_replicas = if deployment.replicas <= 0 { 1 } else { deployment.replicas }; - - match k8s_service.scale_deployment(&deployment_id, target_replicas).await { - Ok(_) => { - // Update status to "running" - sqlx::query!( - "UPDATE deployments SET status = 'running', replicas = $1, updated_at = NOW() WHERE id = $2", - target_replicas, - deployment_id - ) - .execute(&state.db.pool) - .await?; - - info!("Successfully started deployment: {}", deployment_id); + // Create start job + let job = DeploymentJob::new_start( + deployment_id, + user.user_id, + deployment.app_name, + ); - Ok(Json(json!({ - "id": deployment_id, - "status": "running", - "replicas": target_replicas, - "message": "Deployment started successfully" - }))) - } - Err(e) => { - error!("Failed to start deployment {}: {}", deployment_id, e); - - // Update status to failed - sqlx::query!( - "UPDATE deployments SET status = 'failed', error_message = $1, updated_at = NOW() WHERE id = $2", - format!("Failed to start: {}", e), - deployment_id - ) - .execute(&state.db.pool) - .await?; + // Send job to worker queue + if let Err(_) = state.deployment_sender.send(job).await { + // Rollback status on queue failure + let _ = sqlx::query!( + "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue start operation' WHERE id = $1", + deployment_id + ) + .execute(&state.db.pool) + .await; - Err(AppError::internal(&format!("Failed to start deployment: {}", e))) - } + return Err(AppError::internal("Failed to queue start operation")); } + + Ok(Json(json!({ + "id": deployment_id, + "status": "starting", + "message": "Start operation queued" + }))) } + pub async fn stop_deployment( State(state): State, user: AuthUser, @@ -391,7 +370,7 @@ pub async fn stop_deployment( ) -> Result, AppError> { // Check if deployment exists and belongs to user let deployment = sqlx::query!( - "SELECT id, app_name, status FROM deployments WHERE id = $1 AND user_id = $2", + "SELECT app_name FROM deployments WHERE id = $1 AND user_id = $2", deployment_id, user.user_id ) @@ -407,43 +386,31 @@ pub async fn stop_deployment( .execute(&state.db.pool) .await?; - // Create Kubernetes service for user's namespace - let k8s_service = KubernetesService::for_deployment(&deployment_id, &user.user_id).await?; - - // Scale deployment to 0 replicas to stop it - match k8s_service.scale_deployment(&deployment_id, 0).await { - Ok(_) => { - // Update status to "stopped" - sqlx::query!( - "UPDATE deployments SET status = 'stopped', replicas = 0, updated_at = NOW() WHERE id = $1", - deployment_id - ) - .execute(&state.db.pool) - .await?; - - info!("Successfully stopped deployment: {}", deployment_id); + // Create stop job + let job = DeploymentJob::new_stop( + deployment_id, + user.user_id, + deployment.app_name, + ); - Ok(Json(json!({ - "id": deployment_id, - "status": "stopped", - "message": "Deployment stopped successfully" - }))) - } - Err(e) => { - error!("Failed to stop deployment {}: {}", deployment_id, e); - - // Update status back to previous or failed - sqlx::query!( - "UPDATE deployments SET status = 'failed', error_message = $1, updated_at = NOW() WHERE id = $2", - format!("Failed to stop: {}", e), - deployment_id - ) - .execute(&state.db.pool) - .await?; + // Send job to worker queue + if let Err(_) = state.deployment_sender.send(job).await { + // Rollback status on queue failure + let _ = sqlx::query!( + "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue stop operation' WHERE id = $1", + deployment_id + ) + .execute(&state.db.pool) + .await; - Err(AppError::internal(&format!("Failed to stop deployment: {}", e))) - } + return Err(AppError::internal("Failed to queue stop operation")); } + + Ok(Json(json!({ + "id": deployment_id, + "status": "stopping", + "message": "Stop operation queued" + }))) } pub async fn delete_deployment( diff --git a/src/handlers/logs.rs b/src/handlers/logs.rs index 6fe4d42..ef8ecab 100644 --- a/src/handlers/logs.rs +++ b/src/handlers/logs.rs @@ -29,7 +29,7 @@ pub struct LogsQuery { pub struct LogsResponse { pub logs: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize,Clone)] pub struct PodInfo { pub name: String, pub status: String, @@ -68,7 +68,7 @@ pub async fn get_deployment_pods( let k8s_service = KubernetesService::for_deployment(&deployment_id, &user.user_id).await?; - let k8s_pods = k8s_service.get_deployment_pods(&deployment_id).await?; + let k8s_pods: Vec = k8s_service.get_deployment_pods(&deployment_id).await?; // Convert from k8s PodInfo to our response PodInfo let pods: Vec = k8s_pods.into_iter().map(|pod| PodInfo { diff --git a/src/jobs/deployment_job.rs b/src/jobs/deployment_job.rs index 985cb1e..ccb35b4 100644 --- a/src/jobs/deployment_job.rs +++ b/src/jobs/deployment_job.rs @@ -2,10 +2,19 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use std::collections::HashMap; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum JobType { + Deploy, + Scale { target_replicas: i32 }, + Start, + Stop, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeploymentJob { pub deployment_id: Uuid, pub user_id: Uuid, + pub job_type: JobType, pub app_name: String, pub github_image_tag: String, pub port: i32, @@ -31,6 +40,7 @@ impl DeploymentJob { Self { deployment_id, user_id, + job_type: JobType::Deploy, app_name, github_image_tag, port, @@ -41,4 +51,65 @@ impl DeploymentJob { created_at: chrono::Utc::now(), } } + + pub fn new_scale( + deployment_id: Uuid, + user_id: Uuid, + target_replicas: i32, + app_name: String, // Needed for webhooks + ) -> Self { + Self { + deployment_id, + user_id, + job_type: JobType::Scale { target_replicas }, + app_name, + github_image_tag: String::new(), // Not needed for scale + port: 0, // Not needed for scale + env_vars: HashMap::new(), + replicas: target_replicas, + resources: None, + health_check: None, + created_at: chrono::Utc::now(), + } + } + + pub fn new_start( + deployment_id: Uuid, + user_id: Uuid, + app_name: String, + ) -> Self { + Self { + deployment_id, + user_id, + job_type: JobType::Start, + app_name, + github_image_tag: String::new(), + port: 0, + env_vars: HashMap::new(), + replicas: 1, // Will be determined from DB + resources: None, + health_check: None, + created_at: chrono::Utc::now(), + } + } + + pub fn new_stop( + deployment_id: Uuid, + user_id: Uuid, + app_name: String, + ) -> Self { + Self { + deployment_id, + user_id, + job_type: JobType::Stop, + app_name, + github_image_tag: String::new(), + port: 0, + env_vars: HashMap::new(), + replicas: 0, + resources: None, + health_check: None, + created_at: chrono::Utc::now(), + } + } } \ No newline at end of file diff --git a/src/jobs/deployment_worker.rs b/src/jobs/deployment_worker.rs index db5ae1a..84ad04e 100644 --- a/src/jobs/deployment_worker.rs +++ b/src/jobs/deployment_worker.rs @@ -1,14 +1,17 @@ +use crate::handlers::logs::PodInfo; +use crate::handlers::logs::PodsResponse; +use crate::jobs::deployment_job::{DeploymentJob, JobType}; +use crate::notifications::{NotificationManager, NotificationType}; +use crate::services::kubernetes::KubernetesService; +use crate::services::webhook::WebhookService; + +use axum::response::Json; use sqlx::PgPool; use std::time::Duration; use tokio::sync::mpsc; use tracing::{error, info, warn}; use uuid::Uuid; -use crate::jobs::deployment_job::DeploymentJob; -use crate::notifications::{NotificationManager, NotificationType}; -use crate::services::kubernetes::KubernetesService; -use crate::services::webhook::WebhookService; - pub struct DeploymentWorker { receiver: mpsc::Receiver, db_pool: PgPool, @@ -18,14 +21,14 @@ pub struct DeploymentWorker { impl DeploymentWorker { pub fn new( - receiver: mpsc::Receiver, - db_pool: PgPool, + receiver: mpsc::Receiver, + db_pool: PgPool, notification_manager: NotificationManager, webhook_service: WebhookService, ) -> Self { - Self { - receiver, - db_pool, + Self { + receiver, + db_pool, notification_manager, webhook_service, } @@ -35,31 +38,42 @@ impl DeploymentWorker { info!("Deployment worker started"); while let Some(job) = self.receiver.recv().await { - info!("Processing deployment job: {}", job.deployment_id); - - let k8s_service = match KubernetesService::for_deployment(&job.deployment_id, &job.user_id).await { - Ok(service) => service, - Err(e) => { - error!( - "Failed to create K8s service for deployment {} (user {}): {}", - job.deployment_id, job.user_id, e - ); - if let Err(e) = Self::update_deployment_status( - &self.db_pool, - job.deployment_id, - "failed", - None, - Some(&format!("Failed to initialize Kubernetes service: {}", e)), - ) - .await - { - error!("Failed to update deployment status: {}", e); + info!( + "Processing deployment job: {} (type: {:?})", + job.deployment_id, job.job_type + ); + + let k8s_service = + match KubernetesService::for_deployment(&job.deployment_id, &job.user_id).await { + Ok(service) => service, + Err(e) => { + error!( + "Failed to create K8s service for deployment {} (user {}): {}", + job.deployment_id, job.user_id, e + ); + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Failed to initialize Kubernetes service: {}", e)), + ) + .await + { + error!("Failed to update deployment status: {}", e); + } + continue; } - continue; - } - }; + }; - self.process_deployment(job, k8s_service).await; + match job.job_type.clone() { + JobType::Deploy => self.process_deployment(job, k8s_service).await, + JobType::Scale { target_replicas } => { + self.process_scale(job, k8s_service, target_replicas).await + } + JobType::Start => self.process_start(job, k8s_service).await, + JobType::Stop => self.process_stop(job, k8s_service).await, + } } warn!("Deployment worker stopped"); @@ -86,7 +100,12 @@ impl DeploymentWorker { } // Call user webhooks for deployment started - self.call_user_webhooks(job.user_id, crate::user::webhook_models::WebhookEvent::DeploymentStarted, &job).await; + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentStarted, + &job, + ) + .await; // Send notification that deployment is being processed self.notification_manager @@ -104,7 +123,10 @@ impl DeploymentWorker { // Deploy to Kubernetes match k8s_service.deploy_application(&job).await { Ok(_) => { - info!("Successfully deployed to Kubernetes: {} on port {}", job.deployment_id, job.port); + info!( + "Successfully deployed to Kubernetes: {} on port {}", + job.deployment_id, job.port + ); // Wait a moment for ingress to be ready tokio::time::sleep(Duration::from_secs(5)).await; @@ -132,7 +154,13 @@ impl DeploymentWorker { } // Call user webhooks for successful deployment - self.call_user_webhooks_with_url(job.user_id, crate::user::webhook_models::WebhookEvent::DeploymentCompleted, &job, ingress_url.clone()).await; + self.call_user_webhooks_with_url( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentCompleted, + &job, + ingress_url.clone(), + ) + .await; // Send success notification self.notification_manager @@ -163,7 +191,12 @@ impl DeploymentWorker { error!("Failed to update deployment status: {}", e); } else { // Call user webhooks for completed deployment - self.call_user_webhooks(job.user_id, crate::user::webhook_models::WebhookEvent::DeploymentCompleted, &job).await; + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentCompleted, + &job, + ) + .await; // Send partial success notification self.notification_manager @@ -173,7 +206,10 @@ impl DeploymentWorker { deployment_id: job.deployment_id, status: "running".to_string(), url: None, - error_message: Some("Deployment successful but URL not ready yet".to_string()), + error_message: Some( + "Deployment successful but URL not ready yet" + .to_string(), + ), }, ) .await; @@ -183,12 +219,18 @@ impl DeploymentWorker { } Err(e) => { error!("Failed to deploy to Kubernetes: {}", e); - + // Cleanup namespace on failure - if let Err(cleanup_err) = k8s_service.delete_deployment_namespace(&job.deployment_id).await { - warn!("Failed to cleanup namespace after deployment failure: {}", cleanup_err); + if let Err(cleanup_err) = k8s_service + .delete_deployment_namespace(&job.deployment_id) + .await + { + warn!( + "Failed to cleanup namespace after deployment failure: {}", + cleanup_err + ); } - + // Update deployment with failed status if let Err(db_err) = Self::update_deployment_status( &self.db_pool, @@ -202,7 +244,12 @@ impl DeploymentWorker { error!("Failed to update deployment status to failed: {}", db_err); } else { // Call user webhooks for failed deployment - self.call_user_webhooks(job.user_id, crate::user::webhook_models::WebhookEvent::DeploymentFailed, &job).await; + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentFailed, + &job, + ) + .await; // Send failure notification self.notification_manager @@ -220,6 +267,269 @@ impl DeploymentWorker { } } } + async fn process_scale( + &self, + job: DeploymentJob, + k8s_service: KubernetesService, + target_replicas: i32, + ) { + info!( + "Processing scale job: {} to {} replicas", + job.deployment_id, target_replicas + ); + + // Update status to "scaling" + if let Err(e) = + Self::update_deployment_status(&self.db_pool, job.deployment_id, "scaling", None, None) + .await + { + error!("Failed to update deployment status to scaling: {}", e); + return; + } + + // Call user webhooks for scaling started + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentScaling, + &job, + ) + .await; + + // Send notification that scaling is in progress + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "scaling".to_string(), + url: None, + error_message: None, + }, + ) + .await; + + // Scale deployment + match k8s_service + .scale_deployment(&job.deployment_id, target_replicas) + .await + { + Ok(_) => { + // Update replicas count and status in database + if let Err(e) = sqlx::query!( + "UPDATE deployments SET status = 'running', replicas = $1, updated_at = NOW() WHERE id = $2", + target_replicas, + job.deployment_id + ) + .execute(&self.db_pool) + .await + { + error!("Failed to update deployment replicas: {}", e); + } else { + info!("Successfully scaled deployment {} to {} replicas", job.deployment_id, target_replicas); + + // Call user webhooks for successful scale + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentScaled, + &job, + ) + .await; + + // Send success notification + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "running".to_string(), + url: None, + error_message: None, + }, + ) + .await; + } + } + Err(e) => { + error!("Failed to scale deployment: {}", e); + + // Update status to failed + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Scale failed: {}", e)), + ) + .await + { + error!("Failed to update deployment status: {}", e); + } else { + // Call user webhooks for failed scale + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentFailed, + &job, + ) + .await; + + // Send failure notification + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "failed".to_string(), + url: None, + error_message: Some(format!("Scale failed: {}", e)), + }, + ) + .await; + } + } + } + } + + // Start processing logic + async fn process_start(&self, job: DeploymentJob, k8s_service: KubernetesService) { + info!("Processing start job: {}", job.deployment_id); + + // Get current deployment info from DB + let deployment = match sqlx::query!( + "SELECT replicas FROM deployments WHERE id = $1", + job.deployment_id + ) + .fetch_optional(&self.db_pool) + .await + { + Ok(Some(dep)) => dep, + Ok(None) => { + error!("Deployment not found: {}", job.deployment_id); + return; + } + Err(e) => { + error!("Failed to fetch deployment: {}", e); + return; + } + }; + + let target_replicas = if deployment.replicas <= 0 { + 1 + } else { + deployment.replicas + }; + + // Update status to "starting" + if let Err(e) = + Self::update_deployment_status(&self.db_pool, job.deployment_id, "starting", None, None) + .await + { + error!("Failed to update deployment status to starting: {}", e); + return; + } + + // Scale to target replicas + match k8s_service + .scale_deployment(&job.deployment_id, target_replicas) + .await + { + Ok(_) => { + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "running", + None, + None, + ) + .await + { + // Call user webhooks for successful start + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentStarted, + &job, + ) + .await; + error!("Failed to update deployment status to running: {}", e); + } else { + // Call user webhooks for failed start + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentStartFailed, + &job, + ) + .await; + info!("Successfully started deployment: {}", job.deployment_id); + } + } + Err(e) => { + error!("Failed to start deployment: {}", e); + let _ = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Start failed: {}", e)), + ) + .await; + } + } + } + + // Stop processing logic + async fn process_stop(&self, job: DeploymentJob, k8s_service: KubernetesService) { + info!("Processing stop job: {}", job.deployment_id); + + // Update status to "stopping" + if let Err(e) = + Self::update_deployment_status(&self.db_pool, job.deployment_id, "stopping", None, None) + .await + { + error!("Failed to update deployment status to stopping: {}", e); + return; + } + + // Scale to 0 replicas + match k8s_service.scale_deployment(&job.deployment_id, 0).await { + Ok(_) => { + if let Err(e) = sqlx::query!( + "UPDATE deployments SET status = 'stopped', replicas = 0, updated_at = NOW() WHERE id = $1", + job.deployment_id + ) + .execute(&self.db_pool) + .await + { + // Call user webhooks for failed stop + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentStopFailed, + &job, + ) + .await; + error!("Failed to update deployment status to stopped: {}", e); + } else { + // Call user webhooks for stopped + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentStopped, + &job, + ) + .await; + info!("Successfully stopped deployment: {}", job.deployment_id); + } + } + Err(e) => { + error!("Failed to stop deployment: {}", e); + let _ = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Stop failed: {}", e)), + ) + .await; + } + } + } async fn update_deployment_status( db_pool: &PgPool, @@ -289,7 +599,8 @@ impl DeploymentWorker { event: crate::user::webhook_models::WebhookEvent, deployment_job: &DeploymentJob, ) { - self.call_user_webhooks_with_url(user_id, event, deployment_job, None).await; + self.call_user_webhooks_with_url(user_id, event, deployment_job, None) + .await; } // Call user-configured webhooks with optional URL @@ -323,21 +634,82 @@ impl DeploymentWorker { }; if webhooks.is_empty() { - info!("No active webhooks found for user {} and event {}", user_id, event_str); + info!( + "No active webhooks found for user {} and event {}", + user_id, event_str + ); return; } let client = reqwest::Client::new(); - + // Determine status and URL based on event let (status, url): (&str, Option) = match event { crate::user::webhook_models::WebhookEvent::DeploymentStarted => ("started", None), - crate::user::webhook_models::WebhookEvent::DeploymentCompleted => ("completed", app_url), + crate::user::webhook_models::WebhookEvent::DeploymentCompleted => { + ("completed", app_url) + } crate::user::webhook_models::WebhookEvent::DeploymentFailed => ("failed", None), crate::user::webhook_models::WebhookEvent::DeploymentDeleted => ("deleted", None), - crate::user::webhook_models::WebhookEvent::All => ("unknown", None), // This shouldn't happen in practice + crate::user::webhook_models::WebhookEvent::DeploymentScaling => ("scaling", None), + crate::user::webhook_models::WebhookEvent::DeploymentStartFailed => { + ("start_failed", None) + } + crate::user::webhook_models::WebhookEvent::DeploymentStopFailed => { + ("stop_failed", None) + } + crate::user::webhook_models::WebhookEvent::DeploymentStopped => { + ("stopped", None) + }, + + crate::user::webhook_models::WebhookEvent::DeploymentScaled => { + ("scaled", None) + }, + crate::user::webhook_models::WebhookEvent::All => { + ("unknown", None) + }, // This shouldn't happen in practice }; - + let k8s_service = match KubernetesService::for_deployment( + &deployment_job.deployment_id, + &deployment_job.user_id, + ) + .await + { + Ok(service) => service, + Err(e) => { + eprintln!( + "Failed to create K8s service for deployment {} (user {}): {}", + deployment_job.deployment_id, deployment_job.user_id, e + ); + return; + } + }; + + let k8s_pods = match k8s_service + .get_deployment_pods(&deployment_job.user_id) + .await + { + Ok(pods) => pods, + Err(e) => { + eprintln!( + "Failed to list pods for deployment {} (user {}): {}", + deployment_job.deployment_id, deployment_job.user_id, e + ); + return; + } + }; + let pods: Vec = k8s_pods + .into_iter() + .map(|pod| PodInfo { + name: pod.name, + status: pod.status, + ready: pod.ready, + restart_count: pod.restart_count, + node_name: pod.node_name, + created_at: pod.created_at, + }) + .collect(); + for webhook in webhooks { // Use the same payload format as the old webhook service let webhook_payload = serde_json::json!({ @@ -347,7 +719,8 @@ impl DeploymentWorker { "timestamp": chrono::Utc::now(), "app_name": deployment_job.app_name, "user_id": deployment_job.user_id, - "url": url + "url": url, + "pods": pods }); let mut request = client @@ -367,7 +740,10 @@ impl DeploymentWorker { match request.send().await { Ok(response) => { if response.status().is_success() { - info!("Successfully sent webhook to {} for event {}", webhook.url, event_str); + info!( + "Successfully sent webhook to {} for event {}", + webhook.url, event_str + ); } else { warn!( "Webhook call failed: {} returned status {}", diff --git a/src/main.rs b/src/main.rs index 7c5e3b6..da707c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -311,17 +311,18 @@ fn create_app(state: AppState) -> Router { .route("/v1/auth/forgot-password", post(handlers::auth::forgot_password)) .route("/v1/auth/reset-password", post(handlers::auth::reset_password)) // API Key management - .route("/v1/api-keys", get(handlers::auth::list_api_keys)) - .route("/v1/api-keys", post(handlers::auth::create_api_key)) + .route( + "/v1/api-keys", + get(handlers::auth::list_api_keys).post(handlers::auth::create_api_key), + ) .route( "/v1/api-keys/:key_id", axum::routing::delete(handlers::auth::revoke_api_key), ) // User profile management - .route("/v1/user/profile", get(handlers::user::get_profile)) .route( "/v1/user/profile", - axum::routing::put(handlers::user::update_profile), + get(handlers::user::get_profile).put(handlers::user::update_profile), ) .route( "/v1/user/password", @@ -330,23 +331,13 @@ fn create_app(state: AppState) -> Router { // Deployment management .route( "/v1/deployments", - get(handlers::deployment::list_deployments), - ) - .route( - "/v1/deployments", - post(handlers::deployment::create_deployment), - ) - .route( - "/v1/deployments/:deployment_id", - get(handlers::deployment::get_deployment), - ) - .route( - "/v1/deployments/:deployment_id", - axum::routing::put(handlers::deployment::update_deployment), + get(handlers::deployment::list_deployments).post(handlers::deployment::create_deployment), ) .route( "/v1/deployments/:deployment_id", - axum::routing::delete(handlers::deployment::delete_deployment), + get(handlers::deployment::get_deployment) + .put(handlers::deployment::update_deployment) + .delete(handlers::deployment::delete_deployment), ) .route( "/v1/deployments/:deployment_id/scale", @@ -371,11 +362,7 @@ fn create_app(state: AppState) -> Router { // Domain management .route( "/v1/deployments/:deployment_id/domains", - get(handlers::deployment::list_domains), - ) - .route( - "/v1/deployments/:deployment_id/domains", - post(handlers::deployment::add_domain), + get(handlers::deployment::list_domains).post(handlers::deployment::add_domain), ) .route( "/v1/deployments/:deployment_id/domains/:domain_id", @@ -414,19 +401,15 @@ fn create_app(state: AppState) -> Router { get(handlers::notifications::get_notification_stats), ) // Webhook management - .route("/v1/webhooks", get(handlers::webhooks::list_webhooks)) - .route("/v1/webhooks", post(handlers::webhooks::create_webhook)) .route( - "/v1/webhooks/:webhook_id", - get(handlers::webhooks::get_webhook), - ) - .route( - "/v1/webhooks/:webhook_id", - axum::routing::put(handlers::webhooks::update_webhook), + "/v1/webhooks", + get(handlers::webhooks::list_webhooks).post(handlers::webhooks::create_webhook), ) .route( "/v1/webhooks/:webhook_id", - axum::routing::delete(handlers::webhooks::delete_webhook), + get(handlers::webhooks::get_webhook) + .put(handlers::webhooks::update_webhook) + .delete(handlers::webhooks::delete_webhook), ) .route( "/v1/webhooks/:webhook_id/test", diff --git a/src/user/webhook_models.rs b/src/user/webhook_models.rs index 23c4c58..19f5bbf 100644 --- a/src/user/webhook_models.rs +++ b/src/user/webhook_models.rs @@ -57,16 +57,26 @@ pub enum WebhookEvent { DeploymentCompleted, DeploymentFailed, DeploymentDeleted, + DeploymentScaling, + DeploymentScaled, + DeploymentStartFailed, + DeploymentStopFailed, + DeploymentStopped, All, } impl WebhookEvent { pub fn as_str(&self) -> &'static str { match self { - WebhookEvent::DeploymentStarted => "deployment_started", - WebhookEvent::DeploymentCompleted => "deployment_completed", + WebhookEvent::DeploymentCompleted => "deployment_completed", WebhookEvent::DeploymentFailed => "deployment_failed", WebhookEvent::DeploymentDeleted => "deployment_deleted", + WebhookEvent::DeploymentScaling => "deployment_scaling", + WebhookEvent::DeploymentScaled => "deployment_scaled", + WebhookEvent::DeploymentStarted => "deployment_started", + WebhookEvent::DeploymentStartFailed => "deployment_start_failed", + WebhookEvent::DeploymentStopFailed => "deployment_stop_failed", + WebhookEvent::DeploymentStopped => "deployment_stopped", WebhookEvent::All => "all", } } From b93c04fc6979c7abd71f8161094dfd523c52a5b5 Mon Sep 17 00:00:00 2001 From: secus Date: Thu, 25 Sep 2025 14:51:08 +0700 Subject: [PATCH 02/14] feat: Major UI/UX overhaul and system optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit โœจ New Features: - Complete DocumentationPage redesign with modern, professional interface - Comprehensive README with step-by-step setup instructions - Full mobile-first responsive design for all device sizes - Enhanced navigation with sticky header and mobile menu - Custom xs breakpoint support for extra small screens ๐ŸŽจ UI/UX Improvements: - Modern gradient backgrounds and backdrop blur effects - Card-based layouts with shadows and smooth hover animations - Mobile-optimized typography and component sizing - Professional multi-column footer with gradient accents - Touch-friendly interactive elements and navigation ๐Ÿงน Code Cleanup & Optimization: - Removed all unnecessary debug logs from Rust backend - Eliminated console.log statements from frontend codebase - Optimized imports and removed unused dependencies - Improved code structure and maintainability - Enhanced error handling and validation ๐Ÿ“ฑ Mobile Responsiveness: - Horizontal scrolling navigation for mobile devices - Responsive grid systems with mobile-first approach - Adaptive component sizing and spacing - Optimized touch targets and user interactions - Comprehensive breakpoint management (xs, sm, md, lg, xl) ๐Ÿ“š Documentation: - Complete setup guide with prerequisites and troubleshooting - Command reference table for development workflow - Project structure overview and organization - Step-by-step installation instructions ๐Ÿ”ง Technical Enhancements: - Improved webhook event handling system - Enhanced deployment worker with better error handling - Optimized database queries and API responses - Better authentication and middleware systems - Streamlined notification management --- README.md | 230 +++++++ apps/container-engine-frontend/src/index.css | 14 + .../src/pages/DocumentationPage.tsx | 625 ++++++++++++++---- .../src/pages/WebhooksPage.tsx | 20 +- src/auth/middleware.rs | 10 +- src/handlers/auth.rs | 7 +- src/handlers/deployment.rs | 13 +- src/handlers/logs.rs | 31 +- src/handlers/webhooks.rs | 15 +- src/jobs/deployment_worker.rs | 64 +- src/main.rs | 18 +- src/notifications/manager.rs | 8 +- src/notifications/websocket.rs | 33 +- src/user/webhook_models.rs | 13 + 14 files changed, 823 insertions(+), 278 deletions(-) diff --git a/README.md b/README.md index 1c0b5c6..676bae4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,236 @@ --- +## ๐Ÿš€ Quick Start Guide + +### Prerequisites + +Before getting started, ensure you have the following installed: + +- **Docker** - For containerization +- **Docker Compose** - For managing multi-container applications +- **Minikube** - Local Kubernetes cluster +- **kubectl** - Kubernetes command-line tool +- **Node.js** (v16+) - For the frontend application +- **Rust** (latest stable) - For the backend +- **PostgreSQL** - Database (can be run via Docker Compose) +- **Redis** - For caching (can be run via Docker Compose) + +### Installation & Setup + +#### Step 1: Clone the Repository + +```bash +git clone https://github.com/secus217/Open-Container-Engine.git +cd Open-Container-Engine +``` + +#### Step 2: Initial Setup + +Run the setup script to install dependencies and configure the environment: + +```bash +./setup.sh setup +``` + +This command will: +- Install required system dependencies +- Set up Rust toolchain +- Install Node.js dependencies for the frontend +- Configure Docker and Docker Compose +- Set up PostgreSQL and Redis via Docker Compose +- Initialize the database schema + +#### Step 3: Configure Kubernetes + +Create the Kubernetes configuration file by copying from the test template: + +```bash +cp k8sConfigTest.yaml k8sConfig.yaml +``` + +Edit `k8sConfig.yaml` and replace the paths with your actual system paths: + +```yaml +apiVersion: v1 +clusters: +- cluster: + certificate-authority: /home/YOUR_USERNAME/.minikube/ca.crt + server: https://192.168.49.2:8443 + name: minikube +contexts: +- context: + cluster: minikube + namespace: default + user: minikube + name: minikube +current-context: minikube +kind: Config +users: +- name: minikube + user: + client-certificate: /home/YOUR_USERNAME/.minikube/profiles/minikube/client.crt + client-key: /home/YOUR_USERNAME/.minikube/profiles/minikube/client.key +``` + +#### Step 4: Environment Configuration + +Create your local environment configuration: + +```bash +cp .env.development .env.local +``` + +Edit `.env.local` to match your local setup: + +```bash +# Development environment configuration for Container Engine +DATABASE_URL=postgresql://postgres:password@localhost:5432/container_engine +REDIS_URL=redis://localhost:6379 +PORT=3000 +JWT_SECRET=your-secure-jwt-secret-for-development +JWT_EXPIRES_IN=3600 +API_KEY_PREFIX=ce_dev_ +KUBERNETES_NAMESPACE=container-engine-dev +DOMAIN_SUFFIX=.local.dev +MAILTRAP_SMTP_HOST=your_host +MAILTRAP_SMTP_PORT=587 +MAILTRAP_USERNAME=your_mailtrap_username +MAILTRAP_PASSWORD=your_mailtrap_password +EMAIL_FROM=noreply@containerengine.local +EMAIL_FROM_NAME=Container Engine Dev +RUST_LOG=container_engine=debug,tower_http=debug +KUBECONFIG_PATH=./k8sConfig.yaml +``` + +#### Step 5: Start Minikube & Enable Ingress + +Start your local Kubernetes cluster: + +```bash +# Start Minikube +minikube start + +# Enable the ingress addon +minikube addons enable ingress + +# Verify Minikube is running +minikube status +``` + +#### Step 6: Run the Development Environment + +Start all services in development mode: + +```bash +./setup.sh dev +``` + +This command will: +- Start PostgreSQL and Redis containers +- Run database migrations +- Start the Rust backend server +- Start the React frontend development server +- Set up Kubernetes resources +- Open your browser to `http://localhost:3000` + +### ๐ŸŽฏ Accessing the Application + +Once the setup is complete, you can access: + +- **Frontend Application**: http://localhost:3000 +- **Backend API**: http://localhost:8080 +- **API Documentation**: http://localhost:8080/docs +- **Database**: localhost:5432 (postgres/password) +- **Redis**: localhost:6379 + +### ๐Ÿ”ง Development Commands + +| Command | Description | +|---------|-------------| +| `./setup.sh setup` | Initial project setup and dependency installation | +| `./setup.sh dev` | Start development environment | +| `./setup.sh build` | Build the project for production | +| `./setup.sh test` | Run all tests | +| `./setup.sh clean` | Clean build artifacts and docker containers | +| `./setup.sh logs` | View application logs | + +### ๐Ÿ› Troubleshooting + +#### Common Issues + +**1. Minikube not starting:** +```bash +# Reset Minikube if it fails to start +minikube delete +minikube start --driver=docker +``` + +**2. Port already in use:** +```bash +# Check which process is using port 3000 or 8080 +sudo lsof -i :3000 +sudo lsof -i :8080 + +# Kill the process if needed +sudo kill -9 +``` + +**3. Database connection errors:** +```bash +# Restart PostgreSQL container +docker-compose restart postgres + +# Check database logs +docker-compose logs postgres +``` + +**4. Kubernetes configuration issues:** +```bash +# Verify Minikube status +minikube status + +# Check Kubernetes cluster info +kubectl cluster-info + +# Verify ingress is enabled +minikube addons list | grep ingress +``` + +**5. Frontend build errors:** +```bash +# Clear npm cache and reinstall +cd apps/container-engine-frontend +rm -rf node_modules package-lock.json +npm install +``` + +#### Getting Help + +- Check the [Issues](https://github.com/secus217/Open-Container-Engine/issues) page for known problems +- Review application logs: `./setup.sh logs` +- Ensure all prerequisites are correctly installed +- Verify that all ports (3000, 8080, 5432, 6379) are available + +### ๐Ÿ“ Project Structure + +``` +Open-Container-Engine/ +โ”œโ”€โ”€ apps/ +โ”‚ โ””โ”€โ”€ container-engine-frontend/ # React frontend application +โ”œโ”€โ”€ src/ # Rust backend source code +โ”œโ”€โ”€ migrations/ # Database migration files +โ”œโ”€โ”€ tests/ # Integration tests +โ”œโ”€โ”€ scripts/ # Setup and utility scripts +โ”œโ”€โ”€ k8sConfig.yaml # Kubernetes configuration +โ”œโ”€โ”€ .env.local # Local environment variables +โ”œโ”€โ”€ docker-compose.yml # Docker services configuration +โ”œโ”€โ”€ setup.sh # Main setup script +โ””โ”€โ”€ README.md # This file +``` + +--- + ## Introduction **Container Engine** is an open-source alternative to Google Cloud Run, built with Rust and the Axum framework. This revolutionary service empowers developers to effortlessly deploy containerized applications to the internet with unprecedented simplicity and speed. By intelligently abstracting away the complexity of Kubernetes infrastructure, Container Engine creates a seamless deployment experience that lets you focus entirely on your code and business logic, not on managing infrastructure. diff --git a/apps/container-engine-frontend/src/index.css b/apps/container-engine-frontend/src/index.css index 9d71489..c42afc0 100644 --- a/apps/container-engine-frontend/src/index.css +++ b/apps/container-engine-frontend/src/index.css @@ -1,5 +1,19 @@ @import "tailwindcss"; +/* Custom xs breakpoint for extra small screens (480px) */ +@media (min-width: 480px) { + .xs\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .xs\:w-auto { + width: auto; + } + .xs\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } +} + /* Custom animations for magical effects */ @keyframes blob { 0% { diff --git a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx index dd37a56..cbf0944 100644 --- a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx +++ b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx @@ -1,5 +1,5 @@ // src/pages/DocumentationPage.tsx -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { BookOpenIcon, @@ -11,11 +11,19 @@ import { DocumentTextIcon, ChevronRightIcon, ClipboardDocumentIcon, - CheckIcon + CheckIcon, + Bars3Icon, + XMarkIcon, + SparklesIcon, + ShieldCheckIcon, + BoltIcon, + GlobeAltIcon } from '@heroicons/react/24/outline'; const DocumentationPage: React.FC = () => { const [copiedSection, setCopiedSection] = useState(null); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [activeSection, setActiveSection] = useState('getting-started'); const copyToClipboard = (text: string, section: string) => { navigator.clipboard.writeText(text); @@ -23,6 +31,27 @@ const DocumentationPage: React.FC = () => { setTimeout(() => setCopiedSection(null), 2000); }; + useEffect(() => { + const handleScroll = () => { + const sections = ['getting-started', 'authentication', 'api-reference', 'deployment-guide', 'examples', 'configuration']; + const scrollPosition = window.scrollY + 100; + + for (const sectionId of sections) { + const element = document.getElementById(sectionId); + if (element) { + const { offsetTop, offsetHeight } = element; + if (scrollPosition >= offsetTop && scrollPosition < offsetTop + offsetHeight) { + setActiveSection(sectionId); + break; + } + } + } + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + const sections = [ { id: 'getting-started', @@ -63,129 +92,341 @@ const DocumentationPage: React.FC = () => { ]; return ( -
- {/* Navigation Header */} -
+
+ {/* Modern Navigation Header */} +
-
-
- {/* Sidebar Navigation */} -
-
-

- - Documentation + {/* Hero Section - Mobile Optimized */} +
+
+
+
+
+
+ + Documentation +
+
+

+ Developer Documentation +

+

+ Complete guide to deploying and managing containers with Container Engine. +
+ From quick start to advanced configurations. +

+ +
+
+
+ +
+
+ {/* Enhanced Sidebar Navigation - Mobile Optimized */} +
+ {/* Mobile: Horizontal scrolling navigation */} +
+

+ + Contents

- +
-
- {/* Main Content */} -
-
- {/* Header */} -
-

Documentation

-

- Complete guide to deploying and managing containers with Container Engine -

+ {/* Desktop: Sticky sidebar */} +
+

+ + Contents +

+ + + {/* Quick Actions */} +
+

Quick Actions

+
+ + + Get Started + + + + View API + +
+
+
-
- {/* Getting Started */} -
-
- -

Getting Started

-
- -
-

- Welcome to Container Engine! This guide will help you deploy your first containerized application in under 60 seconds. -

- -
-

Prerequisites

-
    -
  • โ€ข A Docker image (public or private registry)
  • -
  • โ€ข Container Engine account (sign up for free)
  • -
  • โ€ข API key (generated from your dashboard)
  • -
+ {/* Main Content - Mobile Optimized */} +
+
+ {/* Getting Started - Mobile Optimized */} +
+
+
+
+
+ +
+
+

Getting Started

+

Deploy your first container in under 60 seconds

+
+
- -

Quick Start Steps

-
- {[ - { step: 1, title: 'Sign Up', description: 'Create your free Container Engine account' }, - { step: 2, title: 'Get API Key', description: 'Generate an API key from your dashboard' }, - { step: 3, title: 'Deploy Container', description: 'Make a single API call to deploy' }, - { step: 4, title: 'Access Your App', description: 'Visit your auto-generated URL' } - ].map((item) => ( -
-
- {item.step} + +
+
+ {/* Prerequisites */} +
+
+ +

Prerequisites

-
-

{item.title}

-

{item.description}

+
    + {[ + 'Docker image (public or private)', + 'Container Engine account', + 'API key from dashboard' + ].map((item, index) => ( +
  • + + {item} +
  • + ))} +
+
+ + {/* Benefits */} +
+
+ +

Why Container Engine?

+
    + {[ + 'Deploy in seconds, not minutes', + 'Auto-scaling & load balancing', + 'Built-in monitoring & logs' + ].map((item, index) => ( +
  • + + {item} +
  • + ))} +
- ))} +
+ + {/* Quick Start Steps - Mobile Optimized */} +
+

+ + 4-Step Deployment Process +

+
+ {[ + { + step: 1, + title: 'Sign Up', + description: 'Create your free account', + icon: '๐Ÿ‘ค', + color: 'from-blue-500 to-blue-600' + }, + { + step: 2, + title: 'Get API Key', + description: 'Generate ', + icon: '๐Ÿ”‘', + color: 'from-indigo-500 to-indigo-600' + }, + { + step: 3, + title: 'Deploy Container', + description: 'Single API call', + icon: '๐Ÿš€', + color: 'from-purple-500 to-purple-600' + }, + { + step: 4, + title: 'Access Your App', + description: 'Visit generated URL', + icon: '๐ŸŒ', + color: 'from-green-500 to-green-600' + } + ].map((item) => ( +
+
+
+ {item.icon} +
+
+

{item.title}

+

{item.description}

+
+
+ {item.step < 4 && ( +
+ +
+ )} +
+ ))} +
+
{/* Authentication */} -
-
- -

Authentication

-
- -
-
-

User Registration

-
- -
-                          {`curl -X POST https://decenter.run/v1/auth/register \\
+                              className="flex items-center space-x-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
+                            >
+                              {copiedSection === 'register' ? (
+                                <>
+                                  
+                                  Copied!
+                                
+                              ) : (
+                                <>
+                                  
+                                  Copy
+                                
+                              )}
+                            
+                          
+
+                            {`curl -X POST https://decenter.run/v1/auth/register \\
   -H "Content-Type: application/json" \\
   -d '{
     "username": "your_username",
@@ -210,38 +458,79 @@ const DocumentationPage: React.FC = () => {
     "password": "secure_password",
     "confirm_password": "secure_password"
   }'`}
-                        
+ +
-
-
-

API Key Generation

-
- -
-                          {`curl -X POST https://decenter.run/v1/api-keys \\
+                              className="flex items-center space-x-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
+                            >
+                              {copiedSection === 'apikey' ? (
+                                <>
+                                  
+                                  Copied!
+                                
+                              ) : (
+                                <>
+                                  
+                                  Copy
+                                
+                              )}
+                            
+                          
+
+                            {`curl -X POST https://decenter.run/v1/api-keys \\
   -H "Authorization: Bearer " \\
   -H "Content-Type: application/json" \\
   -d '{
     "name": "Production API Key",
     "description": "API key for production deployments"
   }'`}
-                        
+ +
+
+ + {/* Security Notice */} +
+
+ +
+

Security Best Practices

+
    +
  • โ€ข Store API keys securely in environment variables
  • +
  • โ€ข Use different API keys for different environments
  • +
  • โ€ข Rotate API keys regularly for enhanced security
  • +
  • โ€ข Never commit API keys to version control
  • +
+
+
@@ -251,7 +540,7 @@ const DocumentationPage: React.FC = () => {
-

API Reference

+

API Reference

@@ -262,7 +551,7 @@ const DocumentationPage: React.FC = () => {
-
+
{[ // Authentication { method: 'POST', endpoint: '/v1/auth/register', description: 'Register a new user', color: 'green' }, @@ -328,12 +617,12 @@ const DocumentationPage: React.FC = () => {
-

Deployment Guide

+

Deployment Guide

Deploy Your First Container

-
+
-
- {/* Footer */} -
-
-
-
-
- Open Container Engine + {/* Modern Footer */} +
+
+
+ {/* Decorative top border */} +
+ +
+
+
+ {/* Brand Section */} +
+
+
+
+ Container Engine +
+ + Container Engine + +
+

+ The modern, open-source alternative to Google Cloud Run. + Deploy containers instantly with enterprise-grade security and performance. +

+
+ + Get Started Free + + + View on GitHub + +
+
+ + {/* Quick Links */} +
+

Product

+
    +
  • Home
  • +
  • Features
  • +
  • Documentation
  • +
  • Pricing
  • +
+
+ + {/* Support */} +
+

Support

+
    +
  • Help Center
  • +
  • Contact Us
  • +
  • System Status
  • +
  • Privacy Policy
  • +
+
+
+ + {/* Bottom Section */} +
+
+

+ © {new Date().getFullYear()} Container Engine by Decenter.AI. All rights reserved. +

+
+ Built with โค๏ธ for developers +
+
+ All systems operational +
+
+
- Container Engine -
-

- The open-source alternative to Google Cloud Run -

-
- Home - Features - Documentation - Privacy - Terms
-

- © {new Date().getFullYear()} Container Engine by Decenter.AI. All rights reserved. -

diff --git a/apps/container-engine-frontend/src/pages/WebhooksPage.tsx b/apps/container-engine-frontend/src/pages/WebhooksPage.tsx index 356490f..a826956 100644 --- a/apps/container-engine-frontend/src/pages/WebhooksPage.tsx +++ b/apps/container-engine-frontend/src/pages/WebhooksPage.tsx @@ -28,6 +28,7 @@ const WEBHOOK_EVENTS = [ { value: 'deployment_deleted', label: 'Deployment Deleted', description: 'When a deployment is deleted' }, { value: 'deployment_scaling', label: 'Deployment Scaling', description: 'When a deployment is being scaled' }, { value: 'deployment_scaled', label: 'Deployment Scaled', description: 'When a deployment scaling is completed' }, + { value: 'deployment_scale_failed', label: 'Deployment Scale Failed', description: 'When a deployment scaling fails' }, { value: 'deployment_start_failed', label: 'Deployment Start Failed', description: 'When starting a deployment fails' }, { value: 'deployment_stop_failed', label: 'Deployment Stop Failed', description: 'When stopping a deployment fails' }, { value: 'deployment_stopped', label: 'Deployment Stopped', description: 'When a deployment is stopped' }, @@ -42,7 +43,7 @@ export default function WebhooksPage() { const [showCreateModal, setShowCreateModal] = useState(false); const [editingWebhook, setEditingWebhook] = useState(null); const [testingWebhook, setTestingWebhook] = useState(null); - + // Create/Edit form state const [formData, setFormData] = useState({ name: '', @@ -113,7 +114,7 @@ export default function WebhooksPage() { const handleDeleteWebhook = async (id: string) => { if (!confirm('Are you sure you want to delete this webhook?')) return; - + try { await webhookService.deleteWebhook(id); await loadWebhooks(); @@ -191,9 +192,9 @@ export default function WebhooksPage() { return ( -
+
{/* Header */} -
+

Webhooks

Manage your deployment notification webhooks

@@ -226,7 +227,7 @@ export default function WebhooksPage() { )} {/* Webhooks List */} -
+
{webhooks.length === 0 ? (
@@ -261,16 +262,15 @@ export default function WebhooksPage() {

{webhook.name}

- + }`}> {webhook.is_active ? 'Active' : 'Inactive'}
-

+

{webhook.url}

diff --git a/src/auth/middleware.rs b/src/auth/middleware.rs index e7d11b7..fbae1e9 100644 --- a/src/auth/middleware.rs +++ b/src/auth/middleware.rs @@ -20,7 +20,6 @@ impl FromRequestParts for AuthUser { let token = extract_token_from_headers(headers)?; // Check if it's an API key or JWT token - tracing::debug!("Token: {}, API prefix: {}", token, state.config.api_key_prefix); if token.starts_with(&state.config.api_key_prefix) { // API Key authentication let user_id = verify_api_key(&state, &token).await?; @@ -49,7 +48,6 @@ fn extract_token_from_headers(headers: &HeaderMap) -> Result { } pub async fn verify_api_key(state: &AppState, api_key: &str) -> Result { - tracing::debug!("Verifying API key: {}", api_key); // Extract prefix to find the API key let prefix = &api_key[..state.config.api_key_prefix.len().min(api_key.len())]; @@ -66,14 +64,10 @@ pub async fn verify_api_key(state: &AppState, api_key: &str) -> Result { - tracing::debug!("API key verified successfully for user: {}", record.user_id); // Update last_used timestamp sqlx::query!( "UPDATE api_keys SET last_used = NOW() WHERE user_id = $1", @@ -85,7 +79,6 @@ pub async fn verify_api_key(state: &AppState, api_key: &str) -> Result { - tracing::debug!("API key verification failed for user: {}", record.user_id); continue; } Err(e) => { @@ -95,6 +88,5 @@ pub async fn verify_api_key(state: &AppState, api_key: &str) -> Result", user.username, user.email); - tracing::info!("Subject: Password Reset Request"); - tracing::info!("Reset URL: {}", reset_url); - tracing::info!("Reset Token: {}", reset_token); - tracing::info!("================================"); + // Password reset email (demo mode) if let Err(e) = state.email_service.send_password_reset_email(&user.email, &user.username, &reset_token, &reset_url) { tracing::error!("Failed to send password reset email: {}", e); diff --git a/src/handlers/deployment.rs b/src/handlers/deployment.rs index f0af0cc..9a24d02 100644 --- a/src/handlers/deployment.rs +++ b/src/handlers/deployment.rs @@ -28,7 +28,7 @@ pub async fn create_deployment( payload.validate()?; // Check if app name already exists for this user - let existing = sqlx::query!( + let _existing = sqlx::query!( "SELECT id FROM deployments WHERE user_id = $1 AND app_name = $2", user.user_id, payload.app_name @@ -36,13 +36,13 @@ pub async fn create_deployment( .fetch_optional(&state.db.pool) .await?; - if existing.is_some() { + if _existing.is_some() { return Err(AppError::conflict("App name")); } let deployment_id = Uuid::new_v4(); let now = Utc::now(); - let url = format!( + let _url = format!( "https://{}.{}", payload.app_name, state.config.domain_suffix ); @@ -56,7 +56,6 @@ pub async fn create_deployment( .health_check .map(|hc| serde_json::to_value(hc)) .transpose()?; - tracing::debug!("Inserting deployment record into database"); sqlx::query!( r#" @@ -85,9 +84,7 @@ pub async fn create_deployment( ) .execute(&state.db.pool) .await?; - tracing::info!("Successfully inserted deployment record into database"); // TODO: Implement Kubernetes deployment logic here - tracing::debug!("Creating deployment job"); let job = DeploymentJob::new( deployment_id, user.user_id, @@ -99,7 +96,6 @@ pub async fn create_deployment( resources, health_check, ); - tracing::debug!("Sending job to deployment queue"); if let Err(_) = state.deployment_sender.send(job).await { // Rollback the database record let _ = sqlx::query!("DELETE FROM deployments WHERE id = $1", deployment_id) @@ -108,7 +104,6 @@ pub async fn create_deployment( return Err(AppError::internal("Failed to queue deployment")); } - tracing::info!("Deployment job queued successfully"); // Send notification about deployment creation state.notification_manager @@ -208,7 +203,7 @@ pub async fn update_deployment( payload.validate()?; // Check if deployment exists and belongs to user - let existing = sqlx::query!( + let _existing = sqlx::query!( "SELECT id FROM deployments WHERE id = $1 AND user_id = $2", deployment_id, user.user_id diff --git a/src/handlers/logs.rs b/src/handlers/logs.rs index ef8ecab..faf4d69 100644 --- a/src/handlers/logs.rs +++ b/src/handlers/logs.rs @@ -10,7 +10,7 @@ use axum::{ use futures::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use tracing::{error, info}; +use tracing::error; use uuid::Uuid; use crate::auth::middleware::verify_api_key; @@ -58,7 +58,7 @@ pub async fn get_deployment_pods( Path(deployment_id): Path, user: AuthUser, ) -> Result, AppError> { - info!("Getting pods for deployment: {} (user: {})", deployment_id, user.user_id); + // Getting pods for deployment // Verify deployment ownership if !verify_deployment_ownership(&state, deployment_id, user.user_id).await? { @@ -80,7 +80,7 @@ pub async fn get_deployment_pods( created_at: pod.created_at, }).collect(); - info!("Found {} pods for deployment {}", pods.len(), deployment_id); + // Found pods for deployment Ok(Json(PodsResponse { pods })) } pub async fn get_pod_logs( @@ -89,8 +89,7 @@ pub async fn get_pod_logs( Query(query): Query, user: AuthUser, ) -> Result, AppError> { - info!("Getting logs for pod: {} in deployment: {} (user: {})", - pod_name, deployment_id, user.user_id); + // Getting logs for pod // Verify deployment ownership if !verify_deployment_ownership(&state, deployment_id, user.user_id).await? { @@ -103,7 +102,7 @@ pub async fn get_pod_logs( // Get the logs from the specific pod let logs = k8s_service.get_pod_logs(&pod_name, query.tail).await?; - info!("Retrieved logs for pod {} in deployment {}", pod_name, deployment_id); + // Retrieved logs for pod Ok(axum::response::Json(LogsResponse { logs })) } @@ -197,10 +196,7 @@ async fn handle_socket_with_user_internal( .await { Ok(mut log_stream) => { - info!( - "Started log stream for deployment: {} (user: {})", - deployment_id, user_id - ); + // Started log stream for deployment // Send initial logs from stream let mut line_count = 0; @@ -245,7 +241,7 @@ async fn handle_socket_with_user_internal( .send(Message::Text("Log stream ended".to_string())) .await; let _ = sender.send(Message::Close(None)).await; - info!("Log stream ended for deployment: {}", deployment_id); + // Log stream ended for deployment } Err(e) => { error!("Failed to start log stream: {}", e); @@ -270,7 +266,7 @@ async fn authenticate_websocket_user( let user_id = if token.starts_with(&state.config.api_key_prefix) { // API Key authentication let user_id = verify_api_key(&state, &token).await?; - info!("API key authenticated for user {}", user_id); + // API key authenticated user_id } else { // JWT token authentication @@ -285,7 +281,7 @@ async fn authenticate_websocket_user( AppError::auth("Invalid token format") })?; - info!("JWT authenticated for user {}", user_id); + // JWT authenticated user_id }; @@ -303,7 +299,7 @@ async fn authenticate_websocket_user( match user_exists { Some(_) => { - info!("User {} authenticated successfully for WebSocket", user_id); + // User authenticated successfully Ok(user_id) } None => { @@ -454,10 +450,7 @@ async fn handle_pod_socket_with_user_internal( .await { Ok(mut log_stream) => { - info!( - "Started log stream for pod: {} in deployment: {} (user: {})", - pod_name, deployment_id, user_id - ); + // Started log stream for pod // Send initial logs from stream let mut line_count = 0; @@ -502,7 +495,7 @@ async fn handle_pod_socket_with_user_internal( .send(Message::Text(format!("Log stream ended for pod: {}", pod_name))) .await; let _ = sender.send(Message::Close(None)).await; - info!("Log stream ended for pod: {} in deployment: {}", pod_name, deployment_id); + // Log stream ended for pod } Err(e) => { error!("Failed to start log stream for pod {}: {}", pod_name, e); diff --git a/src/handlers/webhooks.rs b/src/handlers/webhooks.rs index 757e26f..4b1d5e1 100644 --- a/src/handlers/webhooks.rs +++ b/src/handlers/webhooks.rs @@ -5,7 +5,7 @@ use axum::{ use chrono::Utc; use serde_json::{json, Value}; use std::time::Instant; -use tracing::{error, info}; +use tracing::error; use uuid::Uuid; use validator::Validate; @@ -21,7 +21,7 @@ pub async fn create_webhook( user: AuthUser, Json(payload): Json, ) -> Result, AppError> { - tracing::info!("Creating webhook for user {}: {:?}", user.user_id, payload); + // Creating webhook for user // Validate the payload let validation_result = payload.validate(); @@ -29,7 +29,7 @@ pub async fn create_webhook( tracing::error!("Validation failed: {:?}", e); return Err(AppError::bad_request(&format!("Validation error: {}", e))); } - tracing::info!("Validation passed for webhook creation"); + // Validation passed for webhook creation // Check if webhook name already exists for this user let existing = sqlx::query!( @@ -68,10 +68,7 @@ pub async fn create_webhook( .execute(&state.db.pool) .await?; - info!( - "User {} created webhook: {} -> {}", - user.user_id, payload.name, payload.url - ); + // Webhook created successfully Ok(Json(WebhookResponse { id: webhook_id, @@ -151,7 +148,7 @@ pub async fn update_webhook( payload.validate()?; // Check if webhook exists - let existing = sqlx::query!( + let _existing = sqlx::query!( "SELECT id FROM user_webhooks WHERE id = $1 AND user_id = $2", webhook_id, user.user_id @@ -239,7 +236,7 @@ pub async fn delete_webhook( return Err(AppError::not_found("Webhook")); } - info!("User {} deleted webhook: {}", user.user_id, webhook_id); + // User deleted webhook Ok(Json(json!({ "message": "Webhook deleted successfully", diff --git a/src/jobs/deployment_worker.rs b/src/jobs/deployment_worker.rs index 84ad04e..4da2984 100644 --- a/src/jobs/deployment_worker.rs +++ b/src/jobs/deployment_worker.rs @@ -1,15 +1,12 @@ use crate::handlers::logs::PodInfo; -use crate::handlers::logs::PodsResponse; use crate::jobs::deployment_job::{DeploymentJob, JobType}; use crate::notifications::{NotificationManager, NotificationType}; use crate::services::kubernetes::KubernetesService; use crate::services::webhook::WebhookService; - -use axum::response::Json; use sqlx::PgPool; use std::time::Duration; use tokio::sync::mpsc; -use tracing::{error, info, warn}; +use tracing::{error, warn}; use uuid::Uuid; pub struct DeploymentWorker { @@ -35,13 +32,10 @@ impl DeploymentWorker { } pub async fn start(mut self) { - info!("Deployment worker started"); + // Deployment worker started while let Some(job) = self.receiver.recv().await { - info!( - "Processing deployment job: {} (type: {:?})", - job.deployment_id, job.job_type - ); + // Processing deployment job let k8s_service = match KubernetesService::for_deployment(&job.deployment_id, &job.user_id).await { @@ -80,10 +74,7 @@ impl DeploymentWorker { } async fn process_deployment(&self, job: DeploymentJob, k8s_service: KubernetesService) { - info!( - "Processing deployment: {} ({}) on port {}", - job.deployment_id, job.app_name, job.port - ); + // Processing deployment // Update status to "deploying" if let Err(e) = Self::update_deployment_status( @@ -123,10 +114,7 @@ impl DeploymentWorker { // Deploy to Kubernetes match k8s_service.deploy_application(&job).await { Ok(_) => { - info!( - "Successfully deployed to Kubernetes: {} on port {}", - job.deployment_id, job.port - ); + // Successfully deployed to Kubernetes // Wait a moment for ingress to be ready tokio::time::sleep(Duration::from_secs(5)).await; @@ -134,7 +122,7 @@ impl DeploymentWorker { // Get the ingress URL after successful deployment match k8s_service.get_ingress_url(&job.deployment_id).await { Ok(ingress_url) => { - info!("Retrieved ingress URL: {:?}", ingress_url); + // Retrieved ingress URL // Update deployment with success status and URL if let Err(e) = Self::update_deployment_status( @@ -148,9 +136,9 @@ impl DeploymentWorker { { error!("Failed to update deployment status to running: {}", e); } else { - info!("Deployment {} completed successfully", job.deployment_id); - if let Some(url) = &ingress_url { - info!("Application accessible at: {}", url); + // Deployment completed successfully + if let Some(_url) = &ingress_url { + // Application accessible } // Call user webhooks for successful deployment @@ -273,10 +261,7 @@ impl DeploymentWorker { k8s_service: KubernetesService, target_replicas: i32, ) { - info!( - "Processing scale job: {} to {} replicas", - job.deployment_id, target_replicas - ); + // Processing scale job // Update status to "scaling" if let Err(e) = @@ -325,7 +310,7 @@ impl DeploymentWorker { { error!("Failed to update deployment replicas: {}", e); } else { - info!("Successfully scaled deployment {} to {} replicas", job.deployment_id, target_replicas); + // Successfully scaled deployment // Call user webhooks for successful scale self.call_user_webhooks( @@ -367,7 +352,7 @@ impl DeploymentWorker { // Call user webhooks for failed scale self.call_user_webhooks( job.user_id, - crate::user::webhook_models::WebhookEvent::DeploymentFailed, + crate::user::webhook_models::WebhookEvent::DeploymentScaleFailed, &job, ) .await; @@ -391,7 +376,7 @@ impl DeploymentWorker { // Start processing logic async fn process_start(&self, job: DeploymentJob, k8s_service: KubernetesService) { - info!("Processing start job: {}", job.deployment_id); + // Processing start job // Get current deployment info from DB let deployment = match sqlx::query!( @@ -458,7 +443,7 @@ impl DeploymentWorker { &job, ) .await; - info!("Successfully started deployment: {}", job.deployment_id); + // Successfully started deployment } } Err(e) => { @@ -477,7 +462,7 @@ impl DeploymentWorker { // Stop processing logic async fn process_stop(&self, job: DeploymentJob, k8s_service: KubernetesService) { - info!("Processing stop job: {}", job.deployment_id); + // Processing stop job // Update status to "stopping" if let Err(e) = @@ -514,7 +499,7 @@ impl DeploymentWorker { &job, ) .await; - info!("Successfully stopped deployment: {}", job.deployment_id); + // Successfully stopped deployment } } Err(e) => { @@ -562,19 +547,19 @@ impl DeploymentWorker { k8s_service: &KubernetesService, deployment_id: Uuid, ) -> Option { - info!("Waiting for external IP for deployment: {}", deployment_id); + // Waiting for external IP for deployment for attempt in 1..=60 { // Wait up to 5 minutes match k8s_service.get_service_external_ip(&deployment_id).await { Ok(Some(ip)) => { - info!("External IP obtained after {} attempts: {}", attempt, ip); + // External IP obtained return Some(ip); } Ok(None) => { if attempt % 12 == 0 { // Log every minute - info!("Still waiting for external IP... (attempt {}/60)", attempt); + // Still waiting for external IP } } Err(e) => { @@ -634,10 +619,7 @@ impl DeploymentWorker { }; if webhooks.is_empty() { - info!( - "No active webhooks found for user {} and event {}", - user_id, event_str - ); + // No active webhooks found return; } @@ -652,6 +634,7 @@ impl DeploymentWorker { crate::user::webhook_models::WebhookEvent::DeploymentFailed => ("failed", None), crate::user::webhook_models::WebhookEvent::DeploymentDeleted => ("deleted", None), crate::user::webhook_models::WebhookEvent::DeploymentScaling => ("scaling", None), + crate::user::webhook_models::WebhookEvent::DeploymentScaleFailed => ("scaled", None), crate::user::webhook_models::WebhookEvent::DeploymentStartFailed => { ("start_failed", None) } @@ -740,10 +723,7 @@ impl DeploymentWorker { match request.send().await { Ok(response) => { if response.status().is_success() { - info!( - "Successfully sent webhook to {} for event {}", - webhook.url, event_str - ); + // Successfully sent webhook } else { warn!( "Webhook call failed: {} returned status {}", diff --git a/src/main.rs b/src/main.rs index da707c4..5baaa0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,7 @@ pub async fn setup_deployment_system( worker.start().await; }); - tracing::info!("Deployment system initialized successfully"); + // Deployment system initialized successfully Ok(deployment_sender) } @@ -129,11 +129,11 @@ async fn open_browser_on_startup(port: u16) { let url = format!("http://localhost:{}", port); - tracing::info!("Opening browser at: {}", url); + // Opening browser println!("\n๐Ÿš€ Opening browser at: {}\n", url); match open::that(&url) { - Ok(()) => tracing::info!("Browser opened successfully"), + Ok(()) => {}, // Browser opened successfully Err(e) => { tracing::warn!("Failed to open browser automatically: {}", e); println!( @@ -160,14 +160,14 @@ async fn main() -> Result<(), Box> { // Load configuration let config = Config::new()?; - tracing::info!("Starting Container Engine API server"); + // Starting Container Engine API server // Initialize database let db = Database::new(&config.database_url).await?; // Run migrations db.migrate().await?; - tracing::info!("Database migrations completed"); + // Database migrations completed // Initialize Redis let redis_client = redis::Client::open(config.redis_url.clone())?; @@ -177,7 +177,7 @@ async fn main() -> Result<(), Box> { redis::cmd("PING") .query_async::<_, String>(&mut redis_conn) .await?; - tracing::info!("Redis connection established"); + // Redis connection established // Setup notification manager let notification_manager = NotificationManager::new(); @@ -208,7 +208,7 @@ async fn main() -> Result<(), Box> { .unwrap_or_else(|_| "Container Engine".to_string()), ) { Ok(service) => { - tracing::info!("Email service initialized successfully with Mailtrap"); + // Email service initialized successfully service }, Err(e) => { @@ -255,7 +255,7 @@ async fn main() -> Result<(), Box> { // Run the server let addr = SocketAddr::from(([0, 0, 0, 0], config.port)); - tracing::info!("Server listening on {}", addr); + // Server listening // Automatically open browser in development mode let is_dev = std::env::var("ENVIRONMENT").unwrap_or_default() != "production"; let auto_open = @@ -283,7 +283,7 @@ fn create_app(state: AppState) -> Router { println!(" cd apps/container-engine-frontend"); println!(" npm install && npm run build\n"); } else { - tracing::info!("Serving frontend from: {}", frontend_path); + // Serving frontend // Check index.html file let index_exists = std::path::Path::new(&format!("{}/index.html", frontend_path)).exists(); diff --git a/src/notifications/manager.rs b/src/notifications/manager.rs index 55df3a9..4910f61 100644 --- a/src/notifications/manager.rs +++ b/src/notifications/manager.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{broadcast, RwLock}; use uuid::Uuid; -use tracing::{debug, error, info}; +use tracing::{error, info}; use super::models::{Notification, NotificationType, WebSocketMessage}; @@ -49,15 +49,15 @@ impl NotificationManager { if let Some(tx) = connections.get(&user_id) { match tx.send(message.clone()) { - Ok(receiver_count) => { - debug!("Sent notification to user {} ({} receivers)", user_id, receiver_count); + Ok(_receiver_count) => { + // Notification sent successfully } Err(e) => { error!("Failed to send notification to user {}: {}", user_id, e); } } } else { - debug!("No active connection for user {}, notification not sent", user_id); + // No active connection for user, notification not sent } } diff --git a/src/notifications/websocket.rs b/src/notifications/websocket.rs index e1d513f..12c5f5e 100644 --- a/src/notifications/websocket.rs +++ b/src/notifications/websocket.rs @@ -9,7 +9,7 @@ use axum::{ use futures::{sink::SinkExt, stream::StreamExt}; use serde::Deserialize; use serde_json; -use tracing::{debug, error, info, warn}; +use tracing::{error, warn}; use uuid::Uuid; use crate::{ @@ -34,7 +34,7 @@ pub async fn websocket_handler( let jwt_manager = JwtManager::new(&state.config.jwt_secret, state.config.jwt_expires_in); let user_id = jwt_manager.extract_user_id(&query.token)?; - info!("User {} connecting via WebSocket", user_id); + // User connecting via WebSocket Ok(ws.on_upgrade(move |socket| { handle_socket(socket, user_id, state.notification_manager) @@ -42,7 +42,7 @@ pub async fn websocket_handler( } async fn handle_socket(socket: WebSocket, user_id: Uuid, notification_manager: NotificationManager) { - info!("WebSocket connection established for user {}", user_id); + // WebSocket connection established // Add user to notification manager and get receiver let mut receiver = notification_manager.add_connection(user_id).await; @@ -54,7 +54,7 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, notification_manager: N let mut send_task = tokio::spawn(async move { // Listen for notifications from the notification manager while let Ok(msg) = receiver.recv().await { - debug!("Sending notification to user {}: {:?}", user_id, msg); + // Sending notification to user match serde_json::to_string(&msg) { Ok(json_str) => { @@ -75,32 +75,29 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, notification_manager: N while let Some(msg) = socket_receiver.next().await { match msg { Ok(Message::Text(text)) => { - debug!("Received text message from user {}: {}", user_id, text); + // Received text message from user // Handle ping/pong or other client messages if needed if text == "ping" { // Client is checking connection - debug!("Ping received from user {}", user_id); + // Ping received from user } } Ok(Message::Binary(_)) => { - debug!("Received binary message from user {}", user_id); + // Received binary message from user } Ok(Message::Close(c)) => { - if let Some(cf) = c { - info!( - "User {} sent close with code {} and reason `{}`", - user_id, cf.code, cf.reason - ); + if let Some(_cf) = c { + // User sent close with code and reason } else { - info!("User {} sent close message", user_id); + // User sent close message } break; } Ok(Message::Pong(_)) => { - debug!("Received pong from user {}", user_id); + // Received pong from user } Ok(Message::Ping(_)) => { - debug!("Received ping from user {}", user_id); + // Received ping from user } Err(e) => { error!("WebSocket error for user {}: {}", user_id, e); @@ -113,18 +110,18 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, notification_manager: N // Wait for either task to finish tokio::select! { _ = (&mut send_task) => { - debug!("Send task completed for user {}", user_id); + // Send task completed recv_task.abort(); }, _ = (&mut recv_task) => { - debug!("Receive task completed for user {}", user_id); + // Receive task completed send_task.abort(); } } // Clean up connection notification_manager.remove_connection(&user_id).await; - info!("WebSocket connection closed for user {}", user_id); + // WebSocket connection closed } // Health check endpoint for WebSocket diff --git a/src/user/webhook_models.rs b/src/user/webhook_models.rs index 19f5bbf..782e04e 100644 --- a/src/user/webhook_models.rs +++ b/src/user/webhook_models.rs @@ -53,15 +53,27 @@ pub struct UpdateWebhookRequest { #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum WebhookEvent { + #[serde(alias = "DeploymentStarted", alias = "deployment-started")] DeploymentStarted, + #[serde(alias = "DeploymentCompleted", alias = "deployment-completed")] DeploymentCompleted, + #[serde(alias = "DeploymentFailed", alias = "deployment-failed")] DeploymentFailed, + #[serde(alias = "DeploymentDeleted", alias = "deployment-deleted")] DeploymentDeleted, + #[serde(alias = "DeploymentScaling", alias = "deployment-scaling")] DeploymentScaling, + #[serde(alias = "DeploymentScaled", alias = "deployment-scaled")] DeploymentScaled, + #[serde(alias = "DeploymentScaleFailed", alias = "deployment-scale-failed")] + DeploymentScaleFailed, + #[serde(alias = "DeploymentStartFailed", alias = "deployment-start-failed")] DeploymentStartFailed, + #[serde(alias = "DeploymentStopFailed", alias = "deployment-stop-failed")] DeploymentStopFailed, + #[serde(alias = "DeploymentStopped", alias = "deployment-stopped")] DeploymentStopped, + #[serde(alias = "All", alias = "ALL")] All, } @@ -73,6 +85,7 @@ impl WebhookEvent { WebhookEvent::DeploymentDeleted => "deployment_deleted", WebhookEvent::DeploymentScaling => "deployment_scaling", WebhookEvent::DeploymentScaled => "deployment_scaled", + WebhookEvent::DeploymentScaleFailed => "deployment_scale_failed", WebhookEvent::DeploymentStarted => "deployment_started", WebhookEvent::DeploymentStartFailed => "deployment_start_failed", WebhookEvent::DeploymentStopFailed => "deployment_stop_failed", From 3b84518a4706e0d8bdd1bc72f6d7c8091c50b202 Mon Sep 17 00:00:00 2001 From: secus Date: Fri, 26 Sep 2025 10:39:22 +0700 Subject: [PATCH 03/14] feat: comprehensive domain management and email system improvements - Frontend improvements: * Implement elegant 'coming soon' UI for domain management with feature preview * Add production-ready API configuration with dynamic base URL detection * Enhanced deployment detail page with comprehensive domain management interface * Remove network error alerts for better UX * Add domain management components with DNS setup instructions - Backend enhancements: * Implement complete domain validator service with DNS validation * Add SSL certificate manager with Let's Encrypt integration (mock for development) * Create comprehensive domain management endpoints (add/list/remove domains) * Add dynamic frontend URL generation for password reset emails * Implement Kubernetes custom domain ingress and SSL certificate management * Add database migrations for SSL certificate and DNS record management - Infrastructure improvements: * Add SSL/TLS dependencies for certificate management (rustls, acme-lib, trust-dns-resolver) * Remove test email services and diagnostic endpoints * Add crypto provider initialization for SSL operations * Create comprehensive domain validation and SSL provisioning workflow - Database schema: * Add SSL certificate management tables (ssl_certificates, dns_records, domain_validations) * Extend domains table with SSL status and validation tracking * Add proper indexes and constraints for domain management - Email system: * Configure dynamic URL generation for production deployments * Support environment-aware frontend URL detection * Remove temporary test endpoints after functionality verification This commit establishes a complete foundation for custom domain management with automated SSL certificate provisioning, ready for production deployment at decenter.run. --- Cargo.toml | 8 + WEBHOOK_SYSTEM.md | 0 apps/container-engine-frontend/src/api/api.ts | 1 - .../src/pages/DeploymentDetailPage.tsx | 380 ++++++++++++++++-- .../20240927000001_domain_ssl_management.sql | 98 +++++ src/deployment/models.rs | 1 + src/handlers/auth.rs | 44 +- src/handlers/deployment.rs | 349 +++++++++++++++- src/main.rs | 5 + src/services/domain_validator.rs | 342 ++++++++++++++++ src/services/kubernetes.rs | 231 +++++++++++ src/services/mod.rs | 4 +- src/services/ssl_certificate_manager.rs | 311 ++++++++++++++ 13 files changed, 1722 insertions(+), 52 deletions(-) delete mode 100644 WEBHOOK_SYSTEM.md create mode 100644 migrations/20240927000001_domain_ssl_management.sql create mode 100644 src/services/domain_validator.rs create mode 100644 src/services/ssl_certificate_manager.rs diff --git a/Cargo.toml b/Cargo.toml index 913b4eb..10d06e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,14 @@ validator = { version = "0.18", features = ["derive"] } # Regex regex = "1.0" +# DNS and SSL certificate management +trust-dns-resolver = "0.23" +rustls = { version = "0.23", features = ["ring"] } +rustls-pemfile = "2.0" +acme-lib = "0.9" +base64 = "0.22" +sha2 = "0.10" + # OpenAPI documentation utoipa = { version = "4.0", features = ["axum_extras", "chrono", "uuid"] } utoipa-swagger-ui = { version = "4.0", features = ["axum"] } diff --git a/WEBHOOK_SYSTEM.md b/WEBHOOK_SYSTEM.md deleted file mode 100644 index e69de29..0000000 diff --git a/apps/container-engine-frontend/src/api/api.ts b/apps/container-engine-frontend/src/api/api.ts index 6d642b0..b327adf 100644 --- a/apps/container-engine-frontend/src/api/api.ts +++ b/apps/container-engine-frontend/src/api/api.ts @@ -37,7 +37,6 @@ api.interceptors.response.use( // Handle network errors if (!error.response) { const errorMessage = 'Network error - please check your connection'; - alert(errorMessage); return Promise.reject(new Error(errorMessage)); } return Promise.reject(error); diff --git a/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx b/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx index d937f4e..5d02624 100644 --- a/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx +++ b/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx @@ -56,12 +56,22 @@ interface DeploymentLogs { message: string; } +interface DomainItem { + id: string; + domain: string; + status: 'pending' | 'validating' | 'verified' | 'failed'; + created_at: string; + verified_at?: string; + ssl_status?: 'pending' | 'issued' | 'failed'; + ssl_expires_at?: string; +} + const DeploymentDetailPage: React.FC = () => { const { deploymentId } = useParams<{ deploymentId: string }>(); - + const domainComingSoon = true; // Set to true to hide Domains tab for now const navigate = useNavigate(); const [deployment, setDeployment] = useState(null); - const [ logs,setLogs] = useState([]); + const [logs, setLogs] = useState([]); console.log(logs); const [loading, setLoading] = useState(true); @@ -122,21 +132,21 @@ const DeploymentDetailPage: React.FC = () => { useEffect(() => { const handleNotification = (message: WebSocketMessage) => { console.log("Received WebSocket message:", message); - + // Parse the notification data let isForCurrentDeployment = false; let notificationData: any = null; - + try { // Backend sends nested data structure: message.data.data contains actual notification data const messageType = message.type; notificationData = message.data.data; // Access nested data - + console.log("Message type:", messageType); console.log("Message data:", notificationData); - + // Check if this notification is for the current deployment - isForCurrentDeployment = + isForCurrentDeployment = (messageType === 'deployment_status_changed' && notificationData.deployment_id === deploymentId) || (messageType === 'deployment_scaled' && notificationData.deployment_id === deploymentId) || (messageType === 'deployment_created' && notificationData.deployment_id === deploymentId) || @@ -308,6 +318,288 @@ const DeploymentDetailPage: React.FC = () => { ); + const DomainsTab: React.FC<{ deploymentId: string | undefined; showToast: (message: string, type?: 'success' | 'error') => void }> = ({ deploymentId, showToast }) => { + const [domains, setDomains] = useState([]); + const [loading, setLoading] = useState(true); + const [addingDomain, setAddingDomain] = useState(false); + const [newDomain, setNewDomain] = useState(''); + const [showAddForm, setShowAddForm] = useState(false); + + const fetchDomains = useCallback(async () => { + if (!deploymentId) return; + try { + setLoading(true); + const response = await api.get(`/v1/deployments/${deploymentId}/domains`); + setDomains(response.data.domains || []); + } catch (err: any) { + console.error('Failed to fetch domains:', err); + setDomains([]); + } finally { + setLoading(false); + } + }, [deploymentId]); + + useEffect(() => { + fetchDomains(); + + // Poll for domain status updates every 10 seconds + const pollInterval = setInterval(() => { + fetchDomains(); + }, 10000); + + return () => clearInterval(pollInterval); + }, [fetchDomains]); + + const handleAddDomain = async () => { + if (!newDomain.trim() || !deploymentId) return; + + try { + setAddingDomain(true); + await api.post(`/v1/deployments/${deploymentId}/domains`, { + domain: newDomain.trim() + }); + showToast('Domain added successfully! SSL certificate provisioning will begin shortly.', 'success'); + setNewDomain(''); + setShowAddForm(false); + fetchDomains(); + } catch (err: any) { + showToast(err.response?.data?.error?.message || 'Failed to add domain', 'error'); + } finally { + setAddingDomain(false); + } + }; + + const handleRemoveDomain = async (domainId: string, domainName: string) => { + if (!window.confirm(`Are you sure you want to remove domain "${domainName}"?`)) { + return; + } + + try { + await api.delete(`/v1/deployments/${deploymentId}/domains/${domainId}`); + showToast('Domain removed successfully', 'success'); + fetchDomains(); + } catch (err: any) { + showToast(err.response?.data?.error?.message || 'Failed to remove domain', 'error'); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'verified': return 'text-green-700 bg-green-100 border-green-200'; + case 'pending': return 'text-yellow-700 bg-yellow-100 border-yellow-200'; + case 'validating': return 'text-blue-700 bg-blue-100 border-blue-200'; + case 'failed': return 'text-red-700 bg-red-100 border-red-200'; + default: return 'text-gray-700 bg-gray-100 border-gray-200'; + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'verified': return ; + case 'pending': return ; + case 'validating': return ; + case 'failed': return ; + default: return ; + } + }; + + return ( +

+
+
+
+ +
+
+

Custom Domains

+

Manage custom domains with automated SSL certificates

+
+
+ +
+ + {/* Add Domain Form */} + {showAddForm && ( +
+

Add Custom Domain

+
+ setNewDomain(e.target.value)} + className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + onKeyPress={(e) => e.key === 'Enter' && handleAddDomain()} + /> + + +
+

+ Make sure your domain points to your deployment URL before adding it. +

+
+ )} + + {/* Domains List */} + {loading ? ( +
+
+

Loading domains...

+
+ ) : domains.length > 0 ? ( +
+ {domains.map((domain) => ( +
+
+
+ +
+
+

{domain.domain}

+
+ + {getStatusIcon(domain.status)} + {domain.status} + + + Added {formatDate(domain.created_at)} + + {domain.verified_at && ( + + Verified {formatDate(domain.verified_at)} + + )} + {domain.ssl_status && ( + + SSL: {domain.ssl_status} + + )} + {domain.ssl_expires_at && ( + + SSL expires {formatDate(domain.ssl_expires_at)} + + )} +
+
+
+
+ {domain.status === 'verified' && ( + + )} + +
+
+ ))} +
+ ) : ( +
+ +

No Custom Domains

+

Add custom domains to access your deployment with your own domain name.

+ +
+ )} + + {/* Information Panel */} +
+
+

Step-by-Step DNS Setup

+
    +
  1. Get IP Address: Run nslookup demo-deployment-dc7c4d37.vinhomes.co.uk
  2. +
  3. Login to Domain Provider: GoDaddy, Namecheap, Cloudflare, etc.
  4. +
  5. Find DNS Management: Look for "DNS", "DNS Records", or "Advanced DNS"
  6. +
  7. Add A Record: Point root domain (@) to the IP address
  8. +
  9. Add CNAME Record: Point www to your root domain
  10. +
  11. Save Changes: DNS propagation takes 5-30 minutes
  12. +
+
+

+ ๐Ÿ”’ SSL certificates are automatically provisioned using Let's Encrypt after DNS verification +

+
+
+ + {deployment && ( +
+

DNS Configuration Required

+

+ Add these DNS records to your domain provider (GoDaddy, Namecheap, Cloudflare, etc.): +

+
+
+
A Record (Root Domain):
+
+
Type: A
+
Name: @ (or leave blank)
+
Value: Get IP from: {deployment.url.replace('https://', '').replace('http://', '')}
+
TTL: 300
+
+
+
+
CNAME Record (WWW Subdomain):
+
+
Type: CNAME
+
Name: www
+
Value: your-domain.com (without www)
+
TTL: 300
+
+
+
+
+

+ ๐Ÿ’ก Tip: Use nslookup {deployment.url.replace('https://', '').replace('http://', '')} to get the IP address +

+
+
+ )} +
+
+ ); + }; + return (
@@ -369,8 +661,8 @@ const DeploymentDetailPage: React.FC = () => { View Live - - + +
+ +
+
+ + +
+

Password Requirements:

+
    +
  • โ€ข At least 8 characters long
  • +
  • โ€ข Must match the confirmation password
  • +
+
+
+
+
+ ); +}; + +export default ResetPasswordPage; \ No newline at end of file diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 21adf3f..b5d4c72 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -20,37 +20,9 @@ use crate::{ error::AppError, }; -// Helper function to get frontend URL from headers or environment -fn get_frontend_url(headers: &HeaderMap) -> String { - // Check if we're in production by looking at production domain env var - if let Ok(production_domain) = std::env::var("PRODUCTION_DOMAIN") { - // Try to get host from request headers - if let Some(host) = headers.get("host") { - if let Ok(host_str) = host.to_str() { - // If host matches production domain (decenter.run), use it - if host_str.contains("decenter.run") { - return production_domain; - } - // If it's localhost or development, use frontend URL - else if host_str.contains("localhost") || host_str.contains("127.0.0.1") { - return std::env::var("FRONTEND_BASE_URL") - .unwrap_or_else(|_| "http://localhost:5173".to_string()); - } - // For other domains (staging, etc), use HTTPS - else { - return format!("https://{}", host_str); - } - } - } - } - - // Development fallback: try frontend base URL from env - if let Ok(frontend_url) = std::env::var("FRONTEND_BASE_URL") { - return frontend_url; - } - - // Final fallback - "http://localhost:5173".to_string() +// Helper function to get frontend URL - always return production domain +fn get_frontend_url(_headers: &HeaderMap) -> String { + "https://decenter.run".to_string() } #[utoipa::path( @@ -465,6 +437,9 @@ pub async fn forgot_password( let frontend_base_url = get_frontend_url(&headers); let reset_url = format!("{}/reset-password?token={}", frontend_base_url, reset_token); + // Log for debugging + tracing::info!("Generated password reset URL: {} for user: {}", reset_url, user.email); + // Send password reset email if let Err(e) = state.email_service.send_password_reset_email(&user.email, &user.username, &reset_token, &reset_url) { From 1f43777d8628f2b175f1cf7662c3d0754f68a25c Mon Sep 17 00:00:00 2001 From: secus Date: Mon, 29 Sep 2025 15:40:01 +0700 Subject: [PATCH 06/14] Update deployment worker with new scale_failed variant --- src/jobs/deployment_worker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jobs/deployment_worker.rs b/src/jobs/deployment_worker.rs index 4da2984..fabef93 100644 --- a/src/jobs/deployment_worker.rs +++ b/src/jobs/deployment_worker.rs @@ -634,7 +634,7 @@ impl DeploymentWorker { crate::user::webhook_models::WebhookEvent::DeploymentFailed => ("failed", None), crate::user::webhook_models::WebhookEvent::DeploymentDeleted => ("deleted", None), crate::user::webhook_models::WebhookEvent::DeploymentScaling => ("scaling", None), - crate::user::webhook_models::WebhookEvent::DeploymentScaleFailed => ("scaled", None), + crate::user::webhook_models::WebhookEvent::DeploymentScaleFailed => ("scale_failed", None), crate::user::webhook_models::WebhookEvent::DeploymentStartFailed => { ("start_failed", None) } From 30a749176f20400c574249af65d506247cd906d6 Mon Sep 17 00:00:00 2001 From: secus Date: Mon, 29 Sep 2025 16:51:26 +0700 Subject: [PATCH 07/14] Add function to get all deployment pods including terminating --- src/handlers/logs.rs | 31 +++++++++++++++++ src/services/kubernetes.rs | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/handlers/logs.rs b/src/handlers/logs.rs index faf4d69..3b29634 100644 --- a/src/handlers/logs.rs +++ b/src/handlers/logs.rs @@ -83,6 +83,37 @@ pub async fn get_deployment_pods( // Found pods for deployment Ok(Json(PodsResponse { pods })) } + +/// Get all pods for deployment including terminating ones (for debugging) +pub async fn get_deployment_pods_debug( + State(state): State, + Path(deployment_id): Path, + user: AuthUser, +) -> Result, AppError> { + // Verify deployment ownership + if !verify_deployment_ownership(&state, deployment_id, user.user_id).await? { + error!("User {} does not own deployment {}", user.user_id, deployment_id); + return Err(AppError::not_found("Deployment not found")); + } + + let k8s_service = KubernetesService::for_deployment(&deployment_id, &user.user_id).await?; + + let k8s_pods: Vec = k8s_service.get_deployment_pods_all(&deployment_id).await?; + + // Convert from k8s PodInfo to our response PodInfo + let pods: Vec = k8s_pods.into_iter().map(|pod| PodInfo { + name: pod.name, + status: pod.status, + ready: pod.ready, + restart_count: pod.restart_count, + node_name: pod.node_name, + created_at: pod.created_at, + }).collect(); + + // Found all pods for deployment (including terminating) + Ok(Json(PodsResponse { pods })) +} + pub async fn get_pod_logs( State(state): State, Path((deployment_id, pod_name)): Path<(Uuid, String)>, diff --git a/src/services/kubernetes.rs b/src/services/kubernetes.rs index e518513..582c742 100644 --- a/src/services/kubernetes.rs +++ b/src/services/kubernetes.rs @@ -1350,6 +1350,11 @@ async fn detect_cluster_type(&self) -> Result { let mut pod_infos = Vec::new(); for pod in pod_list.items { + // Skip pods that are marked for deletion (Terminating state) + if pod.metadata.deletion_timestamp.is_some() { + continue; + } + if let Some(name) = &pod.metadata.name { let status = if let Some(pod_status) = &pod.status { pod_status.phase.clone().unwrap_or_else(|| "Unknown".to_string()) @@ -1357,6 +1362,11 @@ async fn detect_cluster_type(&self) -> Result { "Unknown".to_string() }; + // Only include pods that are not in Terminating state + if status == "Terminating" { + continue; + } + let ready = if let Some(pod_status) = &pod.status { pod_status.container_statuses .as_ref() @@ -1387,6 +1397,65 @@ async fn detect_cluster_type(&self) -> Result { Ok(pod_infos) } + +/// Get all pods for deployment including terminating ones (for debugging) +pub async fn get_deployment_pods_all(&self, deployment_id: &Uuid) -> Result, AppError> { + use k8s_openapi::api::core::v1::Pod; + use kube::api::{Api, ListParams}; + + let pods: Api = Api::namespaced(self.client.clone(), &self.namespace); + let lp = ListParams::default().labels(&format!("deployment-id={}", deployment_id)); + + let pod_list = pods + .list(&lp) + .await + .map_err(|e| AppError::internal(&format!("Failed to list pods: {}", e)))?; + + let mut pod_infos = Vec::new(); + for pod in pod_list.items { + if let Some(name) = &pod.metadata.name { + let status = if let Some(pod_status) = &pod.status { + // Check if pod is marked for deletion + if pod.metadata.deletion_timestamp.is_some() { + "Terminating".to_string() + } else { + pod_status.phase.clone().unwrap_or_else(|| "Unknown".to_string()) + } + } else { + "Unknown".to_string() + }; + + let ready = if let Some(pod_status) = &pod.status { + pod_status.container_statuses + .as_ref() + .map(|statuses| statuses.iter().all(|cs| cs.ready)) + .unwrap_or(false) + } else { + false + }; + + pod_infos.push(PodInfo { + name: name.clone(), + status, + ready, + restart_count: if let Some(pod_status) = &pod.status { + pod_status.container_statuses + .as_ref() + .map(|statuses| statuses.iter().map(|cs| cs.restart_count).sum()) + .unwrap_or(0) + } else { + 0 + }, + node_name: pod.spec.as_ref().and_then(|s| s.node_name.clone()), + created_at: pod.metadata.creation_timestamp + .map(|ts| ts.0.format("%Y-%m-%d %H:%M:%S UTC").to_string()), + }); + } + } + + Ok(pod_infos) +} + pub async fn get_pod_logs( &self, pod_name: &str, From 7427c5ca90665014fb3b943bfc53348c5652a93c Mon Sep 17 00:00:00 2001 From: secus Date: Mon, 6 Oct 2025 17:34:24 +0700 Subject: [PATCH 08/14] Update Kubernetes service container port to job port number --- WEBHOOK_SYSTEM.md | 0 src/services/kubernetes.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 WEBHOOK_SYSTEM.md diff --git a/WEBHOOK_SYSTEM.md b/WEBHOOK_SYSTEM.md new file mode 100644 index 0000000..e69de29 diff --git a/src/services/kubernetes.rs b/src/services/kubernetes.rs index 582c742..e223f9b 100644 --- a/src/services/kubernetes.rs +++ b/src/services/kubernetes.rs @@ -363,7 +363,7 @@ impl KubernetesService { ports: Some(vec![ServicePort { port: job.port, // External port (82) - user requested target_port: Some( - k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int(80), // Container port (80) - actual + k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int(job.port), // Container port (80) - actual ), name: Some("http".to_string()), protocol: Some("TCP".to_string()), @@ -1036,7 +1036,7 @@ async fn detect_cluster_type(&self) -> Result { name: "app".to_string(), image: Some(job.github_image_tag.clone()), ports: Some(vec![ContainerPort { - container_port: 80, // Container actual port + container_port: job.port, // Container actual port name: Some("http".to_string()), protocol: Some("TCP".to_string()), ..Default::default() From 4e800112a7c1d66761093eaf9f41f2a1e97432df Mon Sep 17 00:00:00 2001 From: secus Date: Tue, 14 Oct 2025 17:19:37 +0700 Subject: [PATCH 09/14] fix: Docker build SQLx offline mode and code improvements - Update SQLx query cache with missing deployment queries - Fix unused imports in deployment handlers and kubernetes service - Add .dockerignore to reduce build context size - Remove unused variables and clean up code warnings - Ensure Docker build works with offline SQLx mode - Add comprehensive API documentation with interactive examples --- ...737ecde6dfd594903e7419594ef906477b026.json | 35 + ...12d0b6e972b41627e61c4cadd1a935e121323.json | 14 + ...41f7c026da529c0c97ca5c5d4099651612717.json | 15 + ...0572b6d58d0621d3915e0f90e04486f0b14ef.json | 41 + ...c33a0b9a25c24e306981edcc5e462397a8b9a.json | 46 + ...24eafe37247b434b83d13bc84e1428a3b694c.json | 14 + ...9d1f19b83660b17a5e0b78811fc0977c2888a.json | 14 + Dockerfile | 4 +- .../src/pages/DeploymentDetailPage.tsx | 363 +++++++- .../src/pages/DocumentationPage.tsx | 827 +++++++++++++++++- src/deployment/models.rs | 14 + src/handlers/deployment.rs | 210 ++++- src/jobs/deployment_job.rs | 43 + src/jobs/deployment_worker.rs | 264 +++++- src/main.rs | 14 + src/notifications/models.rs | 7 + src/services/domain_validator.rs | 1 - src/services/kubernetes.rs | 194 +++- src/services/webhook.rs | 2 +- src/user/webhook_models.rs | 6 + 20 files changed, 2081 insertions(+), 47 deletions(-) create mode 100644 .sqlx/query-055295ddcc768b57f0919294344737ecde6dfd594903e7419594ef906477b026.json create mode 100644 .sqlx/query-1abac68b6207080fbaf6922823312d0b6e972b41627e61c4cadd1a935e121323.json create mode 100644 .sqlx/query-214bdb41a8af3dc2044e1e6e65e41f7c026da529c0c97ca5c5d4099651612717.json create mode 100644 .sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json create mode 100644 .sqlx/query-47b7c39b7858d4fce34f6e0408ec33a0b9a25c24e306981edcc5e462397a8b9a.json create mode 100644 .sqlx/query-8d6caeb16d4c8211822724f14e424eafe37247b434b83d13bc84e1428a3b694c.json create mode 100644 .sqlx/query-e9cbac5a768824029bfa941371d9d1f19b83660b17a5e0b78811fc0977c2888a.json diff --git a/.sqlx/query-055295ddcc768b57f0919294344737ecde6dfd594903e7419594ef906477b026.json b/.sqlx/query-055295ddcc768b57f0919294344737ecde6dfd594903e7419594ef906477b026.json new file mode 100644 index 0000000..2b651f0 --- /dev/null +++ b/.sqlx/query-055295ddcc768b57f0919294344737ecde6dfd594903e7419594ef906477b026.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, app_name, env_vars FROM deployments WHERE id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "app_name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "env_vars", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "055295ddcc768b57f0919294344737ecde6dfd594903e7419594ef906477b026" +} diff --git a/.sqlx/query-1abac68b6207080fbaf6922823312d0b6e972b41627e61c4cadd1a935e121323.json b/.sqlx/query-1abac68b6207080fbaf6922823312d0b6e972b41627e61c4cadd1a935e121323.json new file mode 100644 index 0000000..bd17b34 --- /dev/null +++ b/.sqlx/query-1abac68b6207080fbaf6922823312d0b6e972b41627e61c4cadd1a935e121323.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue env vars update' WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "1abac68b6207080fbaf6922823312d0b6e972b41627e61c4cadd1a935e121323" +} diff --git a/.sqlx/query-214bdb41a8af3dc2044e1e6e65e41f7c026da529c0c97ca5c5d4099651612717.json b/.sqlx/query-214bdb41a8af3dc2044e1e6e65e41f7c026da529c0c97ca5c5d4099651612717.json new file mode 100644 index 0000000..5152e0e --- /dev/null +++ b/.sqlx/query-214bdb41a8af3dc2044e1e6e65e41f7c026da529c0c97ca5c5d4099651612717.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE deployments \n SET env_vars = $1,\n status = 'updating',\n updated_at = NOW()\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "214bdb41a8af3dc2044e1e6e65e41f7c026da529c0c97ca5c5d4099651612717" +} diff --git a/.sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json b/.sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json new file mode 100644 index 0000000..b553eab --- /dev/null +++ b/.sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, app_name, env_vars, updated_at FROM deployments WHERE id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "app_name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "env_vars", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef" +} diff --git a/.sqlx/query-47b7c39b7858d4fce34f6e0408ec33a0b9a25c24e306981edcc5e462397a8b9a.json b/.sqlx/query-47b7c39b7858d4fce34f6e0408ec33a0b9a25c24e306981edcc5e462397a8b9a.json new file mode 100644 index 0000000..d818ba1 --- /dev/null +++ b/.sqlx/query-47b7c39b7858d4fce34f6e0408ec33a0b9a25c24e306981edcc5e462397a8b9a.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT image, port, replicas, resources, health_check FROM deployments WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "image", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "replicas", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "resources", + "type_info": "Jsonb" + }, + { + "ordinal": 4, + "name": "health_check", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true + ] + }, + "hash": "47b7c39b7858d4fce34f6e0408ec33a0b9a25c24e306981edcc5e462397a8b9a" +} diff --git a/.sqlx/query-8d6caeb16d4c8211822724f14e424eafe37247b434b83d13bc84e1428a3b694c.json b/.sqlx/query-8d6caeb16d4c8211822724f14e424eafe37247b434b83d13bc84e1428a3b694c.json new file mode 100644 index 0000000..e3439c9 --- /dev/null +++ b/.sqlx/query-8d6caeb16d4c8211822724f14e424eafe37247b434b83d13bc84e1428a3b694c.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue restart operation' WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "8d6caeb16d4c8211822724f14e424eafe37247b434b83d13bc84e1428a3b694c" +} diff --git a/.sqlx/query-e9cbac5a768824029bfa941371d9d1f19b83660b17a5e0b78811fc0977c2888a.json b/.sqlx/query-e9cbac5a768824029bfa941371d9d1f19b83660b17a5e0b78811fc0977c2888a.json new file mode 100644 index 0000000..fd5212e --- /dev/null +++ b/.sqlx/query-e9cbac5a768824029bfa941371d9d1f19b83660b17a5e0b78811fc0977c2888a.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE deployments SET status = 'restarting', updated_at = NOW() WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e9cbac5a768824029bfa941371d9d1f19b83660b17a5e0b78811fc0977c2888a" +} diff --git a/Dockerfile b/Dockerfile index fec90a2..ba2aa05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,8 +48,8 @@ else \ echo "Building with offline mode"; \ SQLX_OFFLINE=true cargo build --release --verbose; \ fi - - # Final stage - Runtime (use Ubuntu 24.04 for newer GLIBC) + +# Final stage - Runtime (use Ubuntu 24.04 for newer GLIBC) FROM ubuntu:24.04 # Install runtime dependencies diff --git a/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx b/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx index 5d02624..f17ec14 100644 --- a/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx +++ b/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx @@ -86,8 +86,18 @@ const DeploymentDetailPage: React.FC = () => { // State for scaling const [scaleReplicas, setScaleReplicas] = useState(1); const [isScaling, setIsScaling] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); const { addNotificationHandler } = useNotifications(); + // Environment Variables state + const [envVars, setEnvVars] = useState<{ [key: string]: string }>({}); + const [isUpdatingEnv, setIsUpdatingEnv] = useState(false); + const [showAddEnvForm, setShowAddEnvForm] = useState(false); + const [newEnvKey, setNewEnvKey] = useState(''); + const [newEnvValue, setNewEnvValue] = useState(''); + const [editingEnvKey, setEditingEnvKey] = useState(null); + const [editingEnvValue, setEditingEnvValue] = useState(''); + const showToast = (message: string, type: 'success' | 'error' = 'success') => { setToast({ show: true, message, type }); setTimeout(() => { @@ -109,13 +119,15 @@ const DeploymentDetailPage: React.FC = () => { try { setLoading(true); - const [detailsRes, logsRes] = await Promise.all([ + const [detailsRes, logsRes, envRes] = await Promise.all([ api.get(`/v1/deployments/${deploymentId}`), - api.get(`/v1/deployments/${deploymentId}/logs`, { params: { tail: 100 } }) + api.get(`/v1/deployments/${deploymentId}/logs`, { params: { tail: 100 } }), + api.get(`/v1/deployments/${deploymentId}/env`) ]); setDeployment(detailsRes.data); setScaleReplicas(detailsRes.data.replicas); setLogs(logsRes.data.logs || []); + setEnvVars(envRes.data.env_vars || {}); setError(null); } catch (err: any) { setError(err.response?.data?.error?.message || 'Failed to fetch deployment details.'); @@ -150,7 +162,9 @@ const DeploymentDetailPage: React.FC = () => { (messageType === 'deployment_status_changed' && notificationData.deployment_id === deploymentId) || (messageType === 'deployment_scaled' && notificationData.deployment_id === deploymentId) || (messageType === 'deployment_created' && notificationData.deployment_id === deploymentId) || - (messageType === 'deployment_deleted' && notificationData.deployment_id === deploymentId); + (messageType === 'deployment_deleted' && notificationData.deployment_id === deploymentId) || + (messageType === 'deployment_updated' && notificationData.deployment_id === deploymentId) || + (messageType === 'deployment_restarted' && notificationData.deployment_id === deploymentId); console.log("Is for current deployment:", isForCurrentDeployment); console.log("Current deployment ID:", deploymentId); @@ -171,6 +185,10 @@ const DeploymentDetailPage: React.FC = () => { showToast(`Deployment ${notificationData.app_name} deleted`, 'error'); // Redirect to deployments page after deletion setTimeout(() => navigate('/deployments'), 2000); + } else if (messageType === 'deployment_updated') { + showToast(`Deployment updated: ${notificationData.changes}`, 'success'); + } else if (messageType === 'deployment_restarted') { + showToast(`Deployment ${notificationData.app_name} restarted successfully`, 'success'); } } } catch (error) { @@ -217,6 +235,113 @@ const DeploymentDetailPage: React.FC = () => { } }; + const handleRestartDeployment = async () => { + if (!window.confirm('Are you sure you want to restart this deployment?')) { + return; + } + try { + setIsRestarting(true); + await api.post(`/v1/deployments/${deploymentId}/restart`); + showToast('Deployment restart initiated successfully!', 'success'); + // Refresh deployment data after a short delay + setTimeout(() => { + fetchData(); + }, 2000); + } catch (err: any) { + showToast(err.response?.data?.error?.message || 'Failed to restart deployment.', 'error'); + } finally { + setIsRestarting(false); + } + }; + + // Environment Variables functions + const handleAddEnvVar = async () => { + if (!newEnvKey.trim() || !newEnvValue.trim()) { + showToast('Both key and value are required', 'error'); + return; + } + + if (envVars.hasOwnProperty(newEnvKey)) { + showToast('Environment variable key already exists', 'error'); + return; + } + + try { + setIsUpdatingEnv(true); + const updatedEnvVars = { ...envVars, [newEnvKey]: newEnvValue }; + await api.patch(`/v1/deployments/${deploymentId}/env`, { + env_vars: { [newEnvKey]: newEnvValue } + }); + + setEnvVars(updatedEnvVars); + setNewEnvKey(''); + setNewEnvValue(''); + setShowAddEnvForm(false); + showToast('Environment variable added successfully!', 'success'); + } catch (err: any) { + showToast(err.response?.data?.error?.message || 'Failed to add environment variable', 'error'); + } finally { + setIsUpdatingEnv(false); + } + }; + + const handleUpdateEnvVar = async (key: string, value: string) => { + if (!value.trim()) { + showToast('Environment variable value cannot be empty', 'error'); + return; + } + + try { + setIsUpdatingEnv(true); + await api.patch(`/v1/deployments/${deploymentId}/env`, { + env_vars: { [key]: value } + }); + + setEnvVars(prev => ({ ...prev, [key]: value })); + setEditingEnvKey(null); + setEditingEnvValue(''); + showToast('Environment variable updated successfully!', 'success'); + } catch (err: any) { + showToast(err.response?.data?.error?.message || 'Failed to update environment variable', 'error'); + } finally { + setIsUpdatingEnv(false); + } + }; + + const handleDeleteEnvVar = async (key: string) => { + if (!window.confirm(`Are you sure you want to delete the environment variable "${key}"?`)) { + return; + } + + try { + setIsUpdatingEnv(true); + const updatedEnvVars = { ...envVars }; + delete updatedEnvVars[key]; + + // Send all remaining env vars to backend (effectively removing the deleted one) + await api.patch(`/v1/deployments/${deploymentId}/env`, { + env_vars: updatedEnvVars + }); + + setEnvVars(updatedEnvVars); + showToast('Environment variable deleted successfully!', 'success'); + } catch (err: any) { + showToast(err.response?.data?.error?.message || 'Failed to delete environment variable', 'error'); + } finally { + setIsUpdatingEnv(false); + } + }; + + const startEditingEnvVar = (key: string, value: string) => { + setEditingEnvKey(key); + setEditingEnvValue(value); + }; + + const cancelEditingEnvVar = () => { + setEditingEnvKey(null); + setEditingEnvValue(''); + }; + const getStatusColor = (status: DeploymentStatus) => { switch (status) { case 'running': return 'text-green-700 bg-green-100 border-green-200'; @@ -960,37 +1085,199 @@ const DeploymentDetailPage: React.FC = () => { {activeTab === 'settings' && (
- {/* Environment Variables */} + {/* Quick Actions */}
-
- +
+
-

Environment Variables

-

Application configuration settings

+

Quick Actions

+

Common deployment operations

+
+
+ +
+ + + +
+
+ + {/* Environment Variables */} +
+
+
+
+ +
+
+

Environment Variables

+

Application configuration settings

+
+
- {Object.keys(deployment.env_vars).length > 0 ? ( + {/* Add Environment Variable Form */} + {showAddEnvForm && ( +
+

Add Environment Variable

+
+
+ + setNewEnvKey(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono" + /> +
+
+ + setNewEnvValue(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono" + /> +
+
+
+ + +
+
+ )} + + {Object.keys(envVars).length > 0 ? (
- {Object.entries(deployment.env_vars).map(([key, value]) => ( + {Object.entries(envVars).map(([key, value]) => (
-
- - {key} - - = - - {value} - -
- + {editingEnvKey === key ? ( + // Edit mode +
+ + {key} + + = + setEditingEnvValue(e.target.value)} + className="flex-1 font-mono text-sm bg-white px-3 py-2 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleUpdateEnvVar(key, editingEnvValue); + } else if (e.key === 'Escape') { + cancelEditingEnvVar(); + } + }} + autoFocus + /> +
+ + +
+
+ ) : ( + // View mode + <> +
+ + {key} + + = + + {value} + +
+
+ + + +
+ + )}
))}
@@ -999,12 +1286,31 @@ const DeploymentDetailPage: React.FC = () => {

No Environment Variables

This deployment doesn't have any environment variables configured.

-
)} + + {/* Information Panel */} +
+
+ +
+

Important Notes

+
    +
  • โ€ข Changes to environment variables will trigger a deployment restart
  • +
  • โ€ข Environment variables are case-sensitive
  • +
  • โ€ข Avoid storing sensitive data like passwords in plain text
  • +
  • โ€ข Changes may take a few minutes to apply
  • +
+
+
+
{/* Health Check Settings */} @@ -1078,7 +1384,10 @@ const DeploymentDetailPage: React.FC = () => {

Delete Deployment

Permanently delete this deployment and all associated data.

- diff --git a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx index cbf0944..e261134 100644 --- a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx +++ b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx @@ -17,7 +17,8 @@ import { SparklesIcon, ShieldCheckIcon, BoltIcon, - GlobeAltIcon + GlobeAltIcon, + BellIcon } from '@heroicons/react/24/outline'; const DocumentationPage: React.FC = () => { @@ -429,10 +430,10 @@ const DocumentationPage: React.FC = () => { onClick={() => copyToClipboard(`curl -X POST https://decenter.run/v1/auth/register \\ -H "Content-Type: application/json" \\ -d '{ - "username": "your_username", - "email": "your@email.com", - "password": "secure_password", - "confirm_password": "secure_password" + "username": "john_doe", + "email": "john.doe@example.com", + "password": "MySecure123!", + "confirm_password": "MySecure123!" }'`, 'register')} className="flex items-center space-x-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors" > @@ -453,20 +454,119 @@ const DocumentationPage: React.FC = () => { {`curl -X POST https://decenter.run/v1/auth/register \\ -H "Content-Type: application/json" \\ -d '{ - "username": "your_username", - "email": "your@email.com", - "password": "secure_password", - "confirm_password": "secure_password" + "username": "john_doe", + "email": "john.doe@example.com", + "password": "MySecure123!", + "confirm_password": "MySecure123!" }'`}
+ +
+
+
Request Body Schema
+
+                              {`{
+  "username": "string",     // 3-50 characters, alphanumeric + underscore
+  "email": "string",        // Valid email format
+  "password": "string",     // Min 8 characters, mix of letters/numbers
+  "confirm_password": "string" // Must match password
+}`}
+                            
+
+ +
+
Success Response (201)
+
+                              {`{
+  "message": "User registered successfully",
+  "user": {
+    "id": "01fdb67e-6732-412b-ac17-de06320a928d",
+    "username": "john_doe",
+    "email": "john.doe@example.com",
+    "created_at": "2025-10-14T10:30:00Z"
+  }
+}`}
+                            
+
+
+
+ + {/* User Login */} +
+
+

+ 2 + User Login +

+ POST +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                            {`curl -X POST https://decenter.run/v1/auth/login \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "username": "john_doe",
+    "password": "MySecure123!"
+  }'`}
+                          
+
+ +
+
Success Response (200)
+
+                            {`{
+  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+  "token_type": "Bearer",
+  "expires_in": 3600,
+  "user": {
+    "id": "01fdb67e-6732-412b-ac17-de06320a928d",
+    "username": "john_doe",
+    "email": "john.doe@example.com"
+  }
+}`}
+                          
+
{/* API Key Generation */}

- 2 + 3 API Key Generation

POST @@ -484,7 +584,7 @@ const DocumentationPage: React.FC = () => {
                             {`curl -X POST https://decenter.run/v1/api-keys \\
-  -H "Authorization: Bearer " \\
+  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \\
   -H "Content-Type: application/json" \\
   -d '{
     "name": "Production API Key",
@@ -515,6 +615,526 @@ const DocumentationPage: React.FC = () => {
   }'`}
                           
+ +
+
+
Request Body Schema
+
+                              {`{
+  "name": "string",          // Required: 1-100 characters
+  "description": "string"    // Optional: Max 500 characters
+}`}
+                            
+
+ +
+
Success Response (201)
+
+                              {`{
+  "id": "key_1a2b3c4d5e6f7g8h",
+  "name": "Production API Key",
+  "description": "API key for production deployments",
+  "key": "sk-proj-abc123def456...",
+  "created_at": "2025-10-14T10:35:00Z",
+  "last_used_at": null
+}`}
+                            
+
+
+ +
+
โš ๏ธ Important
+

+ Save your API key immediately! The full key value is only shown once for security reasons. + Store it securely as you won't be able to retrieve it again. +

+
+
+ + {/* List API Keys */} +
+
+

+ 4 + List API Keys +

+ GET +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                            {`curl -X GET https://decenter.run/v1/api-keys \\
+  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."`}
+                          
+
+ +
+
Success Response (200)
+
+                            {`{
+  "api_keys": [
+    {
+      "id": "key_1a2b3c4d5e6f7g8h",
+      "name": "Production API Key",
+      "description": "API key for production deployments",
+      "key_preview": "sk-proj-abc123...g8h",
+      "created_at": "2025-10-14T10:35:00Z",
+      "last_used_at": "2025-10-14T12:20:15Z"
+    },
+    {
+      "id": "key_9i8h7g6f5e4d3c2b",
+      "name": "Development Key",
+      "description": "Testing and development",
+      "key_preview": "sk-proj-def456...c2b",
+      "created_at": "2025-10-14T09:15:00Z",
+      "last_used_at": null
+    }
+  ]
+}`}
+                          
+
+
+ + {/* Deploy Container */} +
+
+

+ 5 + Deploy Container +

+ POST +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                            {`curl -X POST https://decenter.run/v1/deployments \\
+  -H "Authorization: Bearer sk-proj-abc123def456..." \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "name": "my-web-app",
+    "image": "nginx:1.21-alpine",
+    "domain": "my-app.decenter.run",
+    "replicas": 2,
+    "ports": [80, 443],
+    "env_vars": {
+      "NODE_ENV": "production",
+      "DB_HOST": "postgres.internal",
+      "API_KEY": "secret123",
+      "DEBUG": "false"
+    }
+  }'`}
+                          
+
+ +
+
+
Request Body Schema
+
+                              {`{
+  "name": "string",           // Required: 3-50 chars, alphanumeric
+  "image": "string",          // Required: Docker image
+  "domain": "string",         // Optional: Custom domain
+  "replicas": number,         // Optional: Default 1, max 10
+  "ports": [number],          // Optional: Default [80]
+  "env_vars": {               // Optional: Environment variables
+    "key": "value"
+  }
+}`}
+                            
+
+ +
+
Success Response (201)
+
+                              {`{
+  "id": "dep_9f8e7d6c5b4a3210",
+  "name": "my-web-app",
+  "image": "nginx:1.21-alpine",
+  "domain": "my-app.decenter.run",
+  "status": "pending",
+  "replicas": 2,
+  "ports": [80, 443],
+  "env_vars": {
+    "NODE_ENV": "production",
+    "DB_HOST": "postgres.internal",
+    "API_KEY": "***",
+    "DEBUG": "false"
+  },
+  "created_at": "2025-10-14T10:40:00Z",
+  "updated_at": "2025-10-14T10:40:00Z",
+  "url": "https://my-app.decenter.run"
+}`}
+                            
+
+
+
+ + {/* Get Deployment Details */} +
+
+

+ 6 + Get Deployment Details +

+ GET +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                            {`curl -X GET https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210 \\
+  -H "Authorization: Bearer sk-proj-abc123def456..."`}
+                          
+
+ +
+
Success Response (200)
+
+                            {`{
+  "id": "dep_9f8e7d6c5b4a3210",
+  "name": "my-web-app",
+  "image": "nginx:1.21-alpine",
+  "domain": "my-app.decenter.run",
+  "status": "running",
+  "replicas": 2,
+  "ports": [80, 443],
+  "env_vars": {
+    "NODE_ENV": "production",
+    "DB_HOST": "postgres.internal",
+    "API_KEY": "***",
+    "DEBUG": "false"
+  },
+  "url": "https://my-app.decenter.run",
+  "created_at": "2025-10-14T10:40:00Z",
+  "updated_at": "2025-10-14T10:42:30Z",
+  "logs_url": "/v1/deployments/dep_9f8e7d6c5b4a3210/logs"
+}`}
+                          
+
+
+ + {/* Environment Variables Management */} +
+
+

+ 7 + Environment Variables Management +

+
+ + {/* Get Environment Variables */} +
+
+

Get Environment Variables

+ GET +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                              {`curl -X GET https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210/env \\
+  -H "Authorization: Bearer sk-proj-abc123def456..."`}
+                            
+
+ +
+
Success Response (200)
+
+                              {`{
+  "env_vars": {
+    "NODE_ENV": "production",
+    "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb",
+    "API_KEY": "***",
+    "DEBUG": "false",
+    "PORT": "3000",
+    "REDIS_URL": "redis://localhost:6379"
+  }
+}`}
+                            
+
+
+ + {/* Update Environment Variables */} +
+
+

Update Environment Variables

+ PATCH +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                              {`curl -X PATCH https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210/env \\
+  -H "Authorization: Bearer sk-proj-abc123def456..." \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "env_vars": {
+      "NODE_ENV": "production",
+      "NEW_FEATURE_ENABLED": "true",
+      "API_TIMEOUT": "30000",
+      "CACHE_TTL": "3600"
+    }
+  }'`}
+                            
+
+ +
+
+
Request Body Schema
+
+                                {`{
+  "env_vars": {
+    "KEY_NAME": "string"  // Key-value pairs
+  }
+}`}
+                              
+

+ Note: Only specified variables are updated. Existing variables remain unchanged. +

+
+ +
+
Success Response (200)
+
+                                {`{
+  "message": "Environment variables updated",
+  "env_vars": {
+    "NODE_ENV": "production",
+    "NEW_FEATURE_ENABLED": "true",
+    "API_TIMEOUT": "30000",
+    "CACHE_TTL": "3600"
+  },
+  "updated_at": "2025-10-14T10:45:00Z"
+}`}
+                              
+
+
+
+
+ + {/* Deployment Restart */} +
+
+

+ 8 + Deployment Restart +

+ POST +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                            {`curl -X POST https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210/restart \\
+  -H "Authorization: Bearer sk-proj-abc123def456..."`}
+                          
+
+ +
+
+
Success Response (200)
+
+                              {`{
+  "message": "Deployment restart initiated successfully",
+  "deployment_id": "dep_9f8e7d6c5b4a3210",
+  "status": "restarting",
+  "restart_timestamp": "2025-10-14T10:50:00Z"
+}`}
+                            
+
+ +
+
Restart Process
+
    +
  • โ€ข Rolling restart (zero downtime)
  • +
  • โ€ข Pods restarted one by one
  • +
  • โ€ข Environment variables reloaded
  • +
  • โ€ข Health checks performed
  • +
  • โ€ข WebSocket notification sent
  • +
+
+
{/* Security Notice */} @@ -536,6 +1156,156 @@ const DocumentationPage: React.FC = () => {
+ {/* Webhooks Section */} +
+
+ +

Webhooks

+
+ +
+
+

+ Receive real-time notifications about deployment events via webhooks. +

+
+ +
+ {/* Create Webhook */} +
+
+

+ 9 + Create Webhook +

+ POST +
+ +
+
+
+
+
+
+
+
+ curl +
+ +
+
+                            {`curl -X POST https://decenter.run/v1/webhooks \\
+  -H "Authorization: Bearer sk-proj-abc123def456..." \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "url": "https://your-app.com/webhook",
+    "events": ["deployment.created", "deployment.updated", "deployment.failed"],
+    "secret": "your-webhook-secret",
+    "active": true
+  }'`}
+                          
+
+ +
+
+
Request Body Schema
+
+                              {`{
+  "url": "string",              // Required: Webhook URL
+  "events": ["string"],         // Required: Event types
+  "secret": "string",           // Optional: Webhook secret
+  "active": boolean             // Optional: Default true
+}`}
+                            
+
+ +
+
Success Response (201)
+
+                              {`{
+  "id": "wh_1a2b3c4d5e6f7g8h",
+  "url": "https://your-app.com/webhook",
+  "events": ["deployment.created", "deployment.updated"],
+  "active": true,
+  "created_at": "2025-10-14T10:55:00Z"
+}`}
+                            
+
+
+ +
+
Available Events
+
+
โ€ข deployment.created
+
โ€ข deployment.updated
+
โ€ข deployment.started
+
โ€ข deployment.stopped
+
โ€ข deployment.failed
+
โ€ข deployment.restarted
+
+
+
+
+
+
+ + {/* Quick Start */} +
+
+ +

Quick Start

+
+ +
+
+

+ Get up and running with Container Engine in minutes with our comprehensive examples. +

+
+ +
+ {/* Security Notice */} +
+
+ +
+

Security Best Practices

+
    +
  • โ€ข Store API keys securely in environment variables
  • +
  • โ€ข Use different API keys for different environments
  • +
  • โ€ข Rotate API keys regularly for enhanced security
  • +
  • โ€ข Never commit API keys to version control
  • +
+
+
+
+
+
+
+ {/* API Reference */}
@@ -577,6 +1347,9 @@ const DocumentationPage: React.FC = () => { { method: 'PATCH', endpoint: '/v1/deployments/{deployment_id}/scale', description: 'Scale deployment', color: 'yellow' }, { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/start', description: 'Start deployment', color: 'green' }, { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/stop', description: 'Stop deployment', color: 'red' }, + { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/restart', description: 'Restart deployment', color: 'yellow' }, + { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/env', description: 'Get environment variables', color: 'blue' }, + { method: 'PATCH', endpoint: '/v1/deployments/{deployment_id}/env', description: 'Update environment variables', color: 'yellow' }, { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/metrics', description: 'Get deployment metrics', color: 'blue' }, { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/status', description: 'Get deployment status', color: 'blue' }, // Domains @@ -719,6 +1492,36 @@ const DocumentationPage: React.FC = () => {
+ +
+

Environment Variables Management

+
+
+

Update environment variables:

+
+
+                              {`# Update specific environment variables
+PATCH /v1/deployments/{deployment_id}/env
+{
+  "env_vars": {
+    "DEBUG_MODE": "true",
+    "CACHE_TTL": "3600"
+  }
+}`}
+                            
+
+
+
+

Restart deployment to apply changes:

+
+
+                              {`# Perform rolling restart
+POST /v1/deployments/{deployment_id}/restart`}
+                            
+
+
+
+
diff --git a/src/deployment/models.rs b/src/deployment/models.rs index 660e60c..4ce4cfc 100644 --- a/src/deployment/models.rs +++ b/src/deployment/models.rs @@ -149,6 +149,20 @@ pub struct UpdateDeploymentRequest { pub resources: Option, } +#[derive(Debug, Deserialize, Validate, ToSchema)] +pub struct UpdateEnvVarsRequest { + /// Environment variables to update + pub env_vars: HashMap, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct EnvVarsResponse { + pub deployment_id: Uuid, + pub app_name: String, + pub env_vars: HashMap, + pub updated_at: DateTime, +} + #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct ScaleDeploymentRequest { /// Number of replicas (0-100) diff --git a/src/handlers/deployment.rs b/src/handlers/deployment.rs index 9135f41..937b146 100644 --- a/src/handlers/deployment.rs +++ b/src/handlers/deployment.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use tracing::{error, info, warn}; use uuid::Uuid; use validator::Validate; -use crate::jobs::deployment_job::{DeploymentJob, JobType}; +use crate::jobs::deployment_job::DeploymentJob; use crate::services::domain_validator::DomainValidator; use crate::services::ssl_certificate_manager::{SslCertificateManager, CertificateRequest, ChallengeType}; @@ -254,6 +254,149 @@ pub async fn update_deployment( }))) } +#[utoipa::path( + patch, + path = "/v1/deployments/{deployment_id}/env", + params( + ("deployment_id" = Uuid, Path, description = "Deployment ID") + ), + request_body = UpdateEnvVarsRequest, + responses( + (status = 200, description = "Environment variables updated successfully"), + (status = 404, description = "Deployment not found"), + (status = 401, description = "Unauthorized"), + ), + tag = "Deployments" +)] +pub async fn update_env_vars( + State(state): State, + user: AuthUser, + Path(deployment_id): Path, + Json(payload): Json, +) -> Result, AppError> { + payload.validate()?; + + // Check if deployment exists and belongs to user + let deployment = sqlx::query!( + "SELECT id, app_name, env_vars FROM deployments WHERE id = $1 AND user_id = $2", + deployment_id, + user.user_id + ) + .fetch_optional(&state.db.pool) + .await? + .ok_or_else(|| AppError::not_found("Deployment"))?; + + // Get existing env vars + let mut existing_env_vars: HashMap = + serde_json::from_value(deployment.env_vars).unwrap_or_default(); + + // Merge with new env vars (new values overwrite existing ones) + for (key, value) in payload.env_vars.iter() { + existing_env_vars.insert(key.clone(), value.clone()); + } + + // Convert back to JSON + let updated_env_vars_json = serde_json::to_value(&existing_env_vars)?; + + // Update deployment in database + sqlx::query!( + r#" + UPDATE deployments + SET env_vars = $1, + status = 'updating', + updated_at = NOW() + WHERE id = $2 + "#, + updated_env_vars_json, + deployment_id + ) + .execute(&state.db.pool) + .await?; + + let app_name = deployment.app_name.clone(); + + // Create deployment job to apply env var changes + let job = DeploymentJob::new_env_update( + deployment_id, + user.user_id, + app_name.clone(), + Some(existing_env_vars.clone()), + ); + + // Send job to worker queue + if let Err(_) = state.deployment_sender.send(job).await { + // Rollback status on queue failure + let _ = sqlx::query!( + "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue env vars update' WHERE id = $1", + deployment_id + ) + .execute(&state.db.pool) + .await; + + return Err(AppError::internal("Failed to queue env vars update")); + } + + // Send notification about env vars update + state.notification_manager + .send_to_user( + user.user_id, + NotificationType::DeploymentUpdated { + deployment_id, + app_name: app_name.clone(), + changes: "Environment variables updated".to_string(), + }, + ) + .await; + + Ok(Json(json!({ + "id": deployment_id, + "status": "updating", + "message": "Environment variables update in progress", + "updated_env_vars": existing_env_vars, + "updated_at": Utc::now() + }))) +} + +#[utoipa::path( + get, + path = "/v1/deployments/{deployment_id}/env", + params( + ("deployment_id" = Uuid, Path, description = "Deployment ID") + ), + responses( + (status = 200, description = "Environment variables retrieved successfully", body = EnvVarsResponse), + (status = 404, description = "Deployment not found"), + (status = 401, description = "Unauthorized"), + ), + tag = "Deployments" +)] +pub async fn get_env_vars( + State(state): State, + user: AuthUser, + Path(deployment_id): Path, +) -> Result, AppError> { + // Check if deployment exists and belongs to user + let deployment = sqlx::query!( + "SELECT id, app_name, env_vars, updated_at FROM deployments WHERE id = $1 AND user_id = $2", + deployment_id, + user.user_id + ) + .fetch_optional(&state.db.pool) + .await? + .ok_or_else(|| AppError::not_found("Deployment"))?; + + // Parse env vars from JSON + let env_vars: HashMap = + serde_json::from_value(deployment.env_vars).unwrap_or_default(); + + Ok(Json(EnvVarsResponse { + deployment_id, + app_name: deployment.app_name, + env_vars, + updated_at: deployment.updated_at, + })) +} + pub async fn scale_deployment( State(state): State, user: AuthUser, @@ -410,6 +553,69 @@ pub async fn stop_deployment( }))) } +#[utoipa::path( + post, + path = "/v1/deployments/{deployment_id}/restart", + params( + ("deployment_id" = Uuid, Path, description = "Deployment ID") + ), + responses( + (status = 200, description = "Restart operation queued successfully"), + (status = 404, description = "Deployment not found"), + (status = 401, description = "Unauthorized"), + ), + tag = "Deployments" +)] +pub async fn restart_deployment( + State(state): State, + user: AuthUser, + Path(deployment_id): Path, +) -> Result, AppError> { + // Check if deployment exists and belongs to user + let deployment = sqlx::query!( + "SELECT app_name FROM deployments WHERE id = $1 AND user_id = $2", + deployment_id, + user.user_id + ) + .fetch_optional(&state.db.pool) + .await? + .ok_or_else(|| AppError::not_found("Deployment"))?; + + // Update status to "restarting" + sqlx::query!( + "UPDATE deployments SET status = 'restarting', updated_at = NOW() WHERE id = $1", + deployment_id + ) + .execute(&state.db.pool) + .await?; + + // Create restart job + let job = DeploymentJob::new_restart( + deployment_id, + user.user_id, + deployment.app_name, + ); + + // Send job to worker queue + if let Err(_) = state.deployment_sender.send(job).await { + // Rollback status on queue failure + let _ = sqlx::query!( + "UPDATE deployments SET status = 'failed', error_message = 'Failed to queue restart operation' WHERE id = $1", + deployment_id + ) + .execute(&state.db.pool) + .await; + + return Err(AppError::internal("Failed to queue restart operation")); + } + + Ok(Json(json!({ + "id": deployment_id, + "status": "restarting", + "message": "Restart operation queued" + }))) +} + pub async fn delete_deployment( State(state): State, user: AuthUser, @@ -442,7 +648,7 @@ pub async fn delete_deployment( error!("Failed to create K8s service for deployment {} (user {}): {}", deployment_id, user.user_id, e); // Still try to delete from database even if K8s cleanup fails - let result = sqlx::query!( + let _result = sqlx::query!( "DELETE FROM deployments WHERE id = $1 AND user_id = $2", deployment_id, user.user_id diff --git a/src/jobs/deployment_job.rs b/src/jobs/deployment_job.rs index ccb35b4..8fcc887 100644 --- a/src/jobs/deployment_job.rs +++ b/src/jobs/deployment_job.rs @@ -8,6 +8,8 @@ pub enum JobType { Scale { target_replicas: i32 }, Start, Stop, + UpdateEnvVars, + Restart, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -112,4 +114,45 @@ impl DeploymentJob { created_at: chrono::Utc::now(), } } + + pub fn new_env_update( + deployment_id: Uuid, + user_id: Uuid, + app_name: String, + env_vars: Option>, + ) -> Self { + Self { + deployment_id, + user_id, + job_type: JobType::UpdateEnvVars, + app_name, + github_image_tag: String::new(), + port: 0, + env_vars: env_vars.unwrap_or_default(), + replicas: 1, // Will be determined from DB if needed + resources: None, + health_check: None, + created_at: chrono::Utc::now(), + } + } + + pub fn new_restart( + deployment_id: Uuid, + user_id: Uuid, + app_name: String, + ) -> Self { + Self { + deployment_id, + user_id, + job_type: JobType::Restart, + app_name, + github_image_tag: String::new(), + port: 0, + env_vars: HashMap::new(), + replicas: 1, // Will be determined from DB + resources: None, + health_check: None, + created_at: chrono::Utc::now(), + } + } } \ No newline at end of file diff --git a/src/jobs/deployment_worker.rs b/src/jobs/deployment_worker.rs index fabef93..c15128d 100644 --- a/src/jobs/deployment_worker.rs +++ b/src/jobs/deployment_worker.rs @@ -67,6 +67,8 @@ impl DeploymentWorker { } JobType::Start => self.process_start(job, k8s_service).await, JobType::Stop => self.process_stop(job, k8s_service).await, + JobType::UpdateEnvVars => self.process_env_vars_update(job, k8s_service).await, + JobType::Restart => self.process_restart(job, k8s_service).await, } } @@ -644,7 +646,12 @@ impl DeploymentWorker { crate::user::webhook_models::WebhookEvent::DeploymentStopped => { ("stopped", None) }, - + crate::user::webhook_models::WebhookEvent::DeploymentUpdated => { + ("updated", None) + }, + crate::user::webhook_models::WebhookEvent::DeploymentRestarted => { + ("restarted", None) + }, crate::user::webhook_models::WebhookEvent::DeploymentScaled => { ("scaled", None) }, @@ -738,4 +745,259 @@ impl DeploymentWorker { } } } + + async fn process_env_vars_update(&self, job: DeploymentJob, k8s_service: KubernetesService) { + // Processing environment variables update + + // Update status to "updating" + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "updating", + None, + None, + ) + .await + { + error!("Failed to update deployment status to updating: {}", e); + return; + } + + // Send notification that env vars update is being processed + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "updating".to_string(), + url: None, + error_message: None, + }, + ) + .await; + + // Get current deployment info from database + let deployment_info = match sqlx::query!( + "SELECT image, port, replicas, resources, health_check FROM deployments WHERE id = $1", + job.deployment_id + ) + .fetch_optional(&self.db_pool) + .await + { + Ok(Some(info)) => info, + Ok(None) => { + error!("Deployment not found in database: {}", job.deployment_id); + let _ = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some("Deployment not found in database"), + ) + .await; + return; + } + Err(e) => { + error!("Failed to fetch deployment info: {}", e); + let _ = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Failed to fetch deployment info: {}", e)), + ) + .await; + return; + } + }; + + // Update the deployment with new environment variables + let health_check = deployment_info.health_check + .and_then(|hc| serde_json::from_value::(hc).ok()); + + let resources = serde_json::from_value::( + deployment_info.resources + ).ok(); + + // Update the deployment in Kubernetes with new env vars + match k8s_service + .update_deployment_env_vars( + &job.deployment_id, + &job.app_name, + &deployment_info.image, + deployment_info.port, + &job.env_vars, + deployment_info.replicas, + resources.as_ref(), + health_check.as_ref(), + ) + .await + { + Ok(_) => { + // Successfully updated environment variables + // Call user webhooks for env vars updated + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentUpdated, + &job, + ) + .await; + + // Update status to "running" + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "running", + None, + None, + ) + .await + { + error!("Failed to update deployment status to running: {}", e); + } else { + // Send success notification + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentUpdated { + deployment_id: job.deployment_id, + app_name: job.app_name.clone(), + changes: "Environment variables updated successfully".to_string(), + }, + ) + .await; + } + } + Err(e) => { + error!( + "Failed to update deployment env vars in Kubernetes: {}", + e + ); + // Update status to "failed" + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Failed to update environment variables: {}", e)), + ) + .await + { + error!("Failed to update deployment status to failed: {}", e); + } + + // Send error notification + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "failed".to_string(), + url: None, + error_message: Some(format!("Failed to update environment variables: {}", e)), + }, + ) + .await; + } + } + } + + async fn process_restart(&self, job: DeploymentJob, k8s_service: KubernetesService) { + // Processing deployment restart + + // Update status to "restarting" + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "restarting", + None, + None, + ) + .await + { + error!("Failed to update deployment status to restarting: {}", e); + return; + } + + // Send notification that restart is being processed + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "restarting".to_string(), + url: None, + error_message: None, + }, + ) + .await; + + // Restart the deployment in Kubernetes + match k8s_service.restart_deployment(&job.deployment_id).await { + Ok(_) => { + // Successfully restarted deployment + // Call user webhooks for restart completed + self.call_user_webhooks( + job.user_id, + crate::user::webhook_models::WebhookEvent::DeploymentRestarted, + &job, + ) + .await; + + // Update status to "running" + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "running", + None, + None, + ) + .await + { + error!("Failed to update deployment status to running: {}", e); + } else { + // Send success notification + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "running".to_string(), + url: None, + error_message: None, + }, + ) + .await; + } + } + Err(e) => { + error!("Failed to restart deployment in Kubernetes: {}", e); + // Update status to "failed" + if let Err(e) = Self::update_deployment_status( + &self.db_pool, + job.deployment_id, + "failed", + None, + Some(&format!("Failed to restart deployment: {}", e)), + ) + .await + { + error!("Failed to update deployment status to failed: {}", e); + } + + // Send error notification + self.notification_manager + .send_to_user( + job.user_id, + NotificationType::DeploymentStatusChanged { + deployment_id: job.deployment_id, + status: "failed".to_string(), + url: None, + error_message: Some(format!("Failed to restart deployment: {}", e)), + }, + ) + .await; + } + } + } } diff --git a/src/main.rs b/src/main.rs index 1f98795..38a2f4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,9 @@ use tokio::sync::mpsc; handlers::user::get_profile, handlers::user::update_profile, handlers::user::change_password, + handlers::deployment::get_env_vars, + handlers::deployment::update_env_vars, + handlers::deployment::restart_deployment, health_check, ), components( @@ -69,6 +72,8 @@ use tokio::sync::mpsc; user::models::UserProfile, user::models::UpdateProfileRequest, user::models::ChangePasswordRequest, + deployment::models::UpdateEnvVarsRequest, + deployment::models::EnvVarsResponse, error::ErrorResponse, error::ErrorDetails, ) @@ -77,6 +82,7 @@ use tokio::sync::mpsc; (name = "Authentication", description = "User authentication and authorization"), (name = "API Keys", description = "API key management"), (name = "User", description = "User profile management"), + (name = "Deployments", description = "Container deployment management"), (name = "Health", description = "Health check endpoints"), ), info( @@ -348,6 +354,10 @@ fn create_app(state: AppState) -> Router { "/v1/deployments/:deployment_id/scale", axum::routing::patch(handlers::deployment::scale_deployment), ) + .route( + "/v1/deployments/:deployment_id/env", + get(handlers::deployment::get_env_vars).patch(handlers::deployment::update_env_vars), + ) .route( "/v1/deployments/:deployment_id/start", post(handlers::deployment::start_deployment), @@ -356,6 +366,10 @@ fn create_app(state: AppState) -> Router { "/v1/deployments/:deployment_id/stop", post(handlers::deployment::stop_deployment), ) + .route( + "/v1/deployments/:deployment_id/restart", + post(handlers::deployment::restart_deployment), + ) .route( "/v1/deployments/:deployment_id/metrics", get(handlers::deployment::get_metrics), diff --git a/src/notifications/models.rs b/src/notifications/models.rs index c698fa9..a869d0b 100644 --- a/src/notifications/models.rs +++ b/src/notifications/models.rs @@ -28,6 +28,12 @@ pub enum NotificationType { old_replicas: i32, new_replicas: i32, }, + #[serde(rename = "deployment_updated")] + DeploymentUpdated { + deployment_id: Uuid, + app_name: String, + changes: String, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -67,6 +73,7 @@ impl WebSocketMessage { NotificationType::DeploymentCreated { .. } => "deployment_created", NotificationType::DeploymentDeleted { .. } => "deployment_deleted", NotificationType::DeploymentScaled { .. } => "deployment_scaled", + NotificationType::DeploymentUpdated { .. } => "deployment_updated", }; Self { diff --git a/src/services/domain_validator.rs b/src/services/domain_validator.rs index af667eb..0ab6706 100644 --- a/src/services/domain_validator.rs +++ b/src/services/domain_validator.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use std::collections::HashMap; use std::net::IpAddr; use std::time::Duration; use trust_dns_resolver::config::*; diff --git a/src/services/kubernetes.rs b/src/services/kubernetes.rs index e223f9b..803af03 100644 --- a/src/services/kubernetes.rs +++ b/src/services/kubernetes.rs @@ -13,7 +13,7 @@ use kube::{api::PostParams, Api, Client}; use serde_json::Value; use std::collections::BTreeMap; use tokio::process::Command; -use tracing::{info, warn}; +use tracing::{error, info, warn}; use uuid::Uuid; use crate::jobs::deployment_job::DeploymentJob; @@ -1857,4 +1857,196 @@ pub async fn get_deployment_logs_merged( } } } + + /// Update environment variables of an existing deployment + pub async fn update_deployment_env_vars( + &self, + deployment_id: &Uuid, + app_name: &str, + image: &str, + port: i32, + env_vars: &std::collections::HashMap, + replicas: i32, + resources: Option<&crate::deployment::models::ResourceRequirements>, + health_check: Option<&crate::deployment::models::HealthCheck>, + ) -> Result<(), AppError> { + use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec}; + use k8s_openapi::api::core::v1::{Container, ContainerPort, PodSpec, PodTemplateSpec, EnvVar}; + use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta}; + use std::collections::BTreeMap; + + let deployment_name = self.generate_deployment_name(deployment_id); + + info!( + "Updating environment variables for deployment: {} ({})", + deployment_name, app_name + ); + + // Convert env vars to Kubernetes format + let k8s_env_vars: Vec = env_vars + .iter() + .map(|(k, v)| EnvVar { + name: k.clone(), + value: Some(v.clone()), + ..Default::default() + }) + .collect(); + + // Create resource requirements + let resources_json = resources.map(|r| serde_json::to_value(r).unwrap_or_default()); + let k8s_resources = self.parse_resource_requirements(&resources_json); + + // Create health check probes + let health_check_json = health_check.map(|hc| serde_json::to_value(hc).unwrap_or_default()); + let (liveness_probe, readiness_probe) = self.parse_health_probes(&health_check_json, port); + + // Create container spec + let container = Container { + name: "app".to_string(), + image: Some(image.to_string()), + image_pull_policy: Some("Always".to_string()), + ports: Some(vec![ContainerPort { + container_port: port, + name: Some("http".to_string()), + protocol: Some("TCP".to_string()), + ..Default::default() + }]), + env: Some(k8s_env_vars), + resources: k8s_resources, + liveness_probe, + readiness_probe, + ..Default::default() + }; + + // Create labels + let mut labels = BTreeMap::new(); + labels.insert("app".to_string(), app_name.to_string()); + labels.insert("version".to_string(), "latest".to_string()); + labels.insert("app.kubernetes.io/name".to_string(), app_name.to_string()); + labels.insert("app.kubernetes.io/instance".to_string(), deployment_id.to_string()); + labels.insert("app.kubernetes.io/component".to_string(), "web".to_string()); + labels.insert("app.kubernetes.io/part-of".to_string(), "container-engine".to_string()); + labels.insert("app.kubernetes.io/managed-by".to_string(), "container-engine".to_string()); + labels.insert("container-engine.io/deployment-id".to_string(), deployment_id.to_string()); + + // Update the deployment + let deployment_spec = DeploymentSpec { + replicas: Some(replicas), + selector: LabelSelector { + match_labels: Some({ + let mut selector_labels = BTreeMap::new(); + selector_labels.insert("app".to_string(), app_name.to_string()); + selector_labels.insert("container-engine.io/deployment-id".to_string(), deployment_id.to_string()); + selector_labels + }), + ..Default::default() + }, + template: PodTemplateSpec { + metadata: Some(ObjectMeta { + labels: Some(labels.clone()), + ..Default::default() + }), + spec: Some(PodSpec { + containers: vec![container], + ..Default::default() + }), + }, + ..Default::default() + }; + + let updated_deployment = Deployment { + metadata: ObjectMeta { + name: Some(deployment_name.clone()), + namespace: Some(self.namespace.clone()), + labels: Some(labels), + ..Default::default() + }, + spec: Some(deployment_spec), + ..Default::default() + }; + + // Apply the update + let deployments: Api = Api::namespaced(self.client.clone(), &self.namespace); + + match deployments + .replace(&deployment_name, &Default::default(), &updated_deployment) + .await + { + Ok(_) => { + info!( + "Successfully updated environment variables for deployment: {}", + deployment_name + ); + + // Wait for deployment to rollout + // Note: In a real implementation, you might want to wait for the rollout + // For now, we'll just log success + + Ok(()) + } + Err(e) => { + error!( + "Failed to update deployment {} environment variables: {}", + deployment_name, e + ); + Err(AppError::internal(&format!( + "Failed to update deployment environment variables: {}", + e + ))) + } + } + } + + /// Restart a deployment by performing a rolling restart + pub async fn restart_deployment(&self, deployment_id: &Uuid) -> Result<(), AppError> { + use k8s_openapi::api::apps::v1::Deployment; + + use std::collections::BTreeMap; + + let deployment_name = self.generate_deployment_name(deployment_id); + + info!("Restarting deployment: {}", deployment_name); + + let deployments: Api = Api::namespaced(self.client.clone(), &self.namespace); + + // Get current deployment + let current_deployment = deployments + .get(&deployment_name) + .await + .map_err(|e| AppError::internal(&format!("Failed to get deployment for restart: {}", e)))?; + + // Add restart annotation to trigger rolling restart + let mut deployment = current_deployment.clone(); + let restart_time = chrono::Utc::now().to_rfc3339(); + + // Add restart annotation to metadata + if let Some(ref mut annotations) = deployment.metadata.annotations { + annotations.insert( + "kubectl.kubernetes.io/restartedAt".to_string(), + restart_time, + ); + } else { + let mut annotations = BTreeMap::new(); + annotations.insert( + "kubectl.kubernetes.io/restartedAt".to_string(), + restart_time, + ); + deployment.metadata.annotations = Some(annotations); + } + + // Apply the restart annotation + match deployments + .replace(&deployment_name, &Default::default(), &deployment) + .await + { + Ok(_) => { + info!("Successfully triggered restart for deployment: {}", deployment_name); + Ok(()) + } + Err(e) => { + error!("Failed to restart deployment {}: {}", deployment_name, e); + Err(AppError::internal(&format!("Failed to restart deployment: {}", e))) + } + } + } } diff --git a/src/services/webhook.rs b/src/services/webhook.rs index a457f35..281e781 100644 --- a/src/services/webhook.rs +++ b/src/services/webhook.rs @@ -1,5 +1,5 @@ use reqwest; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use tracing::{error, info, warn}; use uuid::Uuid; diff --git a/src/user/webhook_models.rs b/src/user/webhook_models.rs index 782e04e..a275ea7 100644 --- a/src/user/webhook_models.rs +++ b/src/user/webhook_models.rs @@ -73,6 +73,10 @@ pub enum WebhookEvent { DeploymentStopFailed, #[serde(alias = "DeploymentStopped", alias = "deployment-stopped")] DeploymentStopped, + #[serde(alias = "DeploymentUpdated", alias = "deployment-updated")] + DeploymentUpdated, + #[serde(alias = "DeploymentRestarted", alias = "deployment-restarted")] + DeploymentRestarted, #[serde(alias = "All", alias = "ALL")] All, } @@ -90,6 +94,8 @@ impl WebhookEvent { WebhookEvent::DeploymentStartFailed => "deployment_start_failed", WebhookEvent::DeploymentStopFailed => "deployment_stop_failed", WebhookEvent::DeploymentStopped => "deployment_stopped", + WebhookEvent::DeploymentUpdated => "deployment_updated", + WebhookEvent::DeploymentRestarted => "deployment_restarted", WebhookEvent::All => "all", } } From c9215837bb339ddcbe1fa5d9162357dd623ac53e Mon Sep 17 00:00:00 2001 From: secus Date: Wed, 15 Oct 2025 15:16:29 +0700 Subject: [PATCH 10/14] Fix deployment API and Kubernetes service - Fixed env_vars field name in frontend (envVars -> env_vars) - Fixed API base URL to use localhost for development - Updated documentation page structure and content - Improved Kubernetes service logging and error handling - Fixed deployment handler to pass correct env_vars format - Added migration file for desired_replicas column - Cleaned up verbose Kubernetes logging output - Fixed update_deployment_env_vars to properly update existing deployments --- apps/container-engine-frontend/src/api/api.ts | 3 + .../src/pages/DocumentationPage.tsx | 1944 ++++++----------- .../src/pages/NewDeploymentPage.tsx | 2 +- .../20241015000001_add_desired_replicas.sql | 0 src/handlers/deployment.rs | 6 +- src/jobs/deployment_worker.rs | 30 +- src/services/kubernetes.rs | 537 ++--- 7 files changed, 1026 insertions(+), 1496 deletions(-) create mode 100644 migrations/20241015000001_add_desired_replicas.sql diff --git a/apps/container-engine-frontend/src/api/api.ts b/apps/container-engine-frontend/src/api/api.ts index b327adf..d2954cd 100644 --- a/apps/container-engine-frontend/src/api/api.ts +++ b/apps/container-engine-frontend/src/api/api.ts @@ -3,6 +3,9 @@ import axios from 'axios'; // Base API configuration const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || window.location.origin; // const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3000"; +// const API_BASE_URL = "https://decenter.run"; + + // Create axios instance with default config diff --git a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx index e261134..f6b6c31 100644 --- a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx +++ b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx @@ -8,7 +8,6 @@ import { CogIcon, KeyIcon, ServerIcon, - DocumentTextIcon, ChevronRightIcon, ClipboardDocumentIcon, CheckIcon, @@ -34,8 +33,8 @@ const DocumentationPage: React.FC = () => { useEffect(() => { const handleScroll = () => { - const sections = ['getting-started', 'authentication', 'api-reference', 'deployment-guide', 'examples', 'configuration']; - const scrollPosition = window.scrollY + 100; + const sections = ['getting-started', 'authentication', 'api-reference', 'deployment-guide', 'webhooks', 'configuration']; + const scrollPosition = window.scrollY + 150; // Increased offset for better accuracy for (const sectionId of sections) { const element = document.getElementById(sectionId); @@ -64,31 +63,31 @@ const DocumentationPage: React.FC = () => { id: 'authentication', title: 'Authentication', icon: KeyIcon, - description: 'User accounts and API key management' + description: 'Understanding auth flows and API keys' }, { - id: 'api-reference', - title: 'API Reference', - icon: CodeBracketIcon, - description: 'Complete API endpoint documentation' + id: 'deployment-guide', + title: 'Deployment Guide', + icon: ServerIcon, + description: 'Core concepts of deploying containers' }, { - id: 'deployment-guide', - title: 'Deployment Guide', - icon: ServerIcon, - description: 'Container deployment and management' + id: 'webhooks', + title: 'Webhooks', + icon: BellIcon, + description: 'Receive real-time event notifications' }, { - id: 'examples', - title: 'Examples', - icon: DocumentTextIcon, - description: 'Code examples and use cases' + id: 'configuration', + title: 'Configuration', + icon: CogIcon, + description: 'Advanced service configuration options' }, { - id: 'configuration', - title: 'Configuration', - icon: CogIcon, - description: 'Advanced configuration options' + id: 'api-reference', + title: 'API Reference', + icon: CodeBracketIcon, + description: 'Complete endpoint documentation' } ]; @@ -222,12 +221,13 @@ const DocumentationPage: React.FC = () => {
{/* Desktop: Sticky sidebar */} -
-

- - Contents -

-

{[ - { - step: 1, - title: 'Sign Up', - description: 'Create your free account', - icon: '๐Ÿ‘ค', - color: 'from-blue-500 to-blue-600' - }, - { - step: 2, - title: 'Get API Key', - description: 'Generate ', - icon: '๐Ÿ”‘', - color: 'from-indigo-500 to-indigo-600' - }, - { - step: 3, - title: 'Deploy Container', - description: 'Single API call', - icon: '๐Ÿš€', - color: 'from-purple-500 to-purple-600' - }, - { - step: 4, - title: 'Access Your App', - description: 'Visit generated URL', - icon: '๐ŸŒ', - color: 'from-green-500 to-green-600' - } + { step: 1, title: 'Sign Up', description: 'Create your free account', icon: '๐Ÿ‘ค', color: 'from-blue-500 to-blue-600' }, + { step: 2, title: 'Get API Key', description: 'Generate a key from your dashboard', icon: '๐Ÿ”‘', color: 'from-indigo-500 to-indigo-600' }, + { step: 3, title: 'Deploy Container', description: 'Make a single API call', icon: '๐Ÿš€', color: 'from-purple-500 to-purple-600' }, + { step: 4, title: 'Access Your App', description: 'Visit the provided URL', icon: '๐ŸŒ', color: 'from-green-500 to-green-600' } ].map((item) => (
@@ -385,1209 +363,731 @@ const DocumentationPage: React.FC = () => {
))}
+

+ For detailed API calls, please see the API Reference section. +

- - {/* Authentication */} -
-
-
-
-
- -
-
-

Authentication

-

Secure API access with JWT tokens and API keys

-
-
+ + {/* Authentication Conceptual Guide */} +
+
+ +

Authentication

- -
- {/* User Registration */} -
-
-

- 1 - User Registration -

- POST -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X POST https://decenter.run/v1/auth/register \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "username": "john_doe",
-    "email": "john.doe@example.com",
-    "password": "MySecure123!",
-    "confirm_password": "MySecure123!"
-  }'`}
-                          
-
- -
-
-
Request Body Schema
-
-                              {`{
-  "username": "string",     // 3-50 characters, alphanumeric + underscore
-  "email": "string",        // Valid email format
-  "password": "string",     // Min 8 characters, mix of letters/numbers
-  "confirm_password": "string" // Must match password
-}`}
-                            
-
- -
-
Success Response (201)
-
-                              {`{
-  "message": "User registered successfully",
-  "user": {
-    "id": "01fdb67e-6732-412b-ac17-de06320a928d",
-    "username": "john_doe",
-    "email": "john.doe@example.com",
-    "created_at": "2025-10-14T10:30:00Z"
-  }
-}`}
-                            
-
-
-
- - {/* User Login */} -
-
-

- 2 - User Login -

- POST -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X POST https://decenter.run/v1/auth/login \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "username": "john_doe",
-    "password": "MySecure123!"
-  }'`}
-                          
-
- -
-
Success Response (200)
-
-                            {`{
-  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
-  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
-  "token_type": "Bearer",
-  "expires_in": 3600,
-  "user": {
-    "id": "01fdb67e-6732-412b-ac17-de06320a928d",
-    "username": "john_doe",
-    "email": "john.doe@example.com"
-  }
-}`}
-                          
-
-
- - {/* API Key Generation */} -
-
-

- 3 - API Key Generation -

- POST -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X POST https://decenter.run/v1/api-keys \\
-  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "name": "Production API Key",
-    "description": "API key for production deployments"
-  }'`}
-                          
-
- -
-
-
Request Body Schema
-
-                              {`{
-  "name": "string",          // Required: 1-100 characters
-  "description": "string"    // Optional: Max 500 characters
-}`}
-                            
-
- -
-
Success Response (201)
-
-                              {`{
-  "id": "key_1a2b3c4d5e6f7g8h",
-  "name": "Production API Key",
-  "description": "API key for production deployments",
-  "key": "sk-proj-abc123def456...",
-  "created_at": "2025-10-14T10:35:00Z",
-  "last_used_at": null
-}`}
-                            
-
-
- -
-
โš ๏ธ Important
-

- Save your API key immediately! The full key value is only shown once for security reasons. - Store it securely as you won't be able to retrieve it again. -

-
-
- - {/* List API Keys */} -
-
-

- 4 - List API Keys -

- GET -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X GET https://decenter.run/v1/api-keys \\
-  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."`}
-                          
-
- -
-
Success Response (200)
-
-                            {`{
-  "api_keys": [
-    {
-      "id": "key_1a2b3c4d5e6f7g8h",
-      "name": "Production API Key",
-      "description": "API key for production deployments",
-      "key_preview": "sk-proj-abc123...g8h",
-      "created_at": "2025-10-14T10:35:00Z",
-      "last_used_at": "2025-10-14T12:20:15Z"
-    },
-    {
-      "id": "key_9i8h7g6f5e4d3c2b",
-      "name": "Development Key",
-      "description": "Testing and development",
-      "key_preview": "sk-proj-def456...c2b",
-      "created_at": "2025-10-14T09:15:00Z",
-      "last_used_at": null
-    }
-  ]
-}`}
-                          
-
-
- - {/* Deploy Container */} -
-
-

- 5 - Deploy Container -

- POST -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X POST https://decenter.run/v1/deployments \\
-  -H "Authorization: Bearer sk-proj-abc123def456..." \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "name": "my-web-app",
-    "image": "nginx:1.21-alpine",
-    "domain": "my-app.decenter.run",
-    "replicas": 2,
-    "ports": [80, 443],
-    "env_vars": {
-      "NODE_ENV": "production",
-      "DB_HOST": "postgres.internal",
-      "API_KEY": "secret123",
-      "DEBUG": "false"
-    }
-  }'`}
-                          
-
- -
-
-
Request Body Schema
-
-                              {`{
-  "name": "string",           // Required: 3-50 chars, alphanumeric
-  "image": "string",          // Required: Docker image
-  "domain": "string",         // Optional: Custom domain
-  "replicas": number,         // Optional: Default 1, max 10
-  "ports": [number],          // Optional: Default [80]
-  "env_vars": {               // Optional: Environment variables
-    "key": "value"
-  }
-}`}
-                            
-
- -
-
Success Response (201)
-
-                              {`{
-  "id": "dep_9f8e7d6c5b4a3210",
-  "name": "my-web-app",
-  "image": "nginx:1.21-alpine",
-  "domain": "my-app.decenter.run",
-  "status": "pending",
-  "replicas": 2,
-  "ports": [80, 443],
-  "env_vars": {
-    "NODE_ENV": "production",
-    "DB_HOST": "postgres.internal",
-    "API_KEY": "***",
-    "DEBUG": "false"
-  },
-  "created_at": "2025-10-14T10:40:00Z",
-  "updated_at": "2025-10-14T10:40:00Z",
-  "url": "https://my-app.decenter.run"
-}`}
-                            
-
-
-
- - {/* Get Deployment Details */} -
-
-

- 6 - Get Deployment Details -

- GET -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X GET https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210 \\
-  -H "Authorization: Bearer sk-proj-abc123def456..."`}
-                          
-
- -
-
Success Response (200)
-
-                            {`{
-  "id": "dep_9f8e7d6c5b4a3210",
-  "name": "my-web-app",
-  "image": "nginx:1.21-alpine",
-  "domain": "my-app.decenter.run",
-  "status": "running",
-  "replicas": 2,
-  "ports": [80, 443],
-  "env_vars": {
-    "NODE_ENV": "production",
-    "DB_HOST": "postgres.internal",
-    "API_KEY": "***",
-    "DEBUG": "false"
-  },
-  "url": "https://my-app.decenter.run",
-  "created_at": "2025-10-14T10:40:00Z",
-  "updated_at": "2025-10-14T10:42:30Z",
-  "logs_url": "/v1/deployments/dep_9f8e7d6c5b4a3210/logs"
-}`}
-                          
-
-
- - {/* Environment Variables Management */} -
-
-

- 7 - Environment Variables Management -

-
- - {/* Get Environment Variables */} -
-
-

Get Environment Variables

- GET -
- -
-
-
-
-
-
-
+
+

+ Access to the Container Engine API is controlled by API keys. You can manage these keys through your user dashboard after registering an account. + All API requests must include an `Authorization` header with your API key. +

+

+ There are two main types of authentication: +

+
    +
  • User Account Authentication: To manage your account, billing, and API keys, you first need to register and log in. This process provides you with a JWT (JSON Web Token) that authenticates you for account-level actions, such as creating new API keys.
  • +
  • API Key Authentication: For all programmatic actions, such as deploying containers or managing services, you will use a generated API key. This key is a long-lived token that you should treat like a password. You include it in your requests as a Bearer token in the `Authorization` header.
  • +
+
+
+ +
+

Security Best Practices

+
    +
  • Store API keys securely, preferably in an environment variable or secrets manager.
  • +
  • Use different API keys for different environments (e.g., development, production).
  • +
  • Rotate your API keys periodically to enhance security.
  • +
  • Never commit API keys directly into your version control system (like Git).
  • +
- curl -
-
-
-                              {`curl -X GET https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210/env \\
-  -H "Authorization: Bearer sk-proj-abc123def456..."`}
-                            
-
+
+
+
+ + {/* Deployment Guide Conceptual */} +
+
+ +

Deployment Guide

+
+
+

+ Deploying an application on Container Engine involves providing a container image and some basic configuration. Our system handles the rest, from provisioning resources to networking and scaling. +

+

Core Concepts

+
    +
  • Image: You must provide a publicly or privately accessible container image (e.g., from Docker Hub, GCR, or your own registry). This image contains your application and all its dependencies.
  • +
  • Replicas: This determines how many instances of your container are running. We automatically balance traffic between them for high availability. You can scale this number up or down at any time.
  • +
  • Ports: Specify which port your application listens on inside the container. We will automatically expose it to the internet via HTTP (80) and HTTPS (443).
  • +
  • Environment Variables: A secure way to provide configuration to your application without hardcoding it. You can manage these variables via the API. Any changes will trigger a zero-downtime rolling restart to apply them.
  • +
  • Domain: By default, we provide a `.decenter.run` subdomain for your application. You can also configure custom domains.
  • +
+

+ To see the detailed API calls for creating and managing deployments, please refer to the Deployments API Reference. +

+
+
-
-
Success Response (200)
-
-                              {`{
-  "env_vars": {
-    "NODE_ENV": "production",
-    "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb",
-    "API_KEY": "***",
-    "DEBUG": "false",
-    "PORT": "3000",
-    "REDIS_URL": "redis://localhost:6379"
-  }
-}`}
-                            
-
+ {/* Webhooks Conceptual Guide */} +
+
+ +

Webhooks

+
+
+

+ Webhooks allow you to receive real-time HTTP notifications about events happening with your deployments. You can subscribe to various events, such as when a deployment is created, fails, or successfully starts. +

+

+ To use webhooks, you provide a URL endpoint that we will send `POST` requests to. These requests will contain a JSON payload with details about the event. You can also provide an optional secret to verify that the requests are coming from Container Engine. +

+

Available Events

+
+ {[ + 'deployment.created', 'deployment.updated', 'deployment.started', + 'deployment.stopped', 'deployment.failed', 'deployment.restarted' + ].map(event => ( +
+ {event} +
+ ))}
+

+ For instructions on how to create and manage webhooks, see the Webhooks API Reference. +

+
+
- {/* Update Environment Variables */} -
-
-

Update Environment Variables

- PATCH -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                              {`curl -X PATCH https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210/env \\
-  -H "Authorization: Bearer sk-proj-abc123def456..." \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "env_vars": {
-      "NODE_ENV": "production",
-      "NEW_FEATURE_ENABLED": "true",
-      "API_TIMEOUT": "30000",
-      "CACHE_TTL": "3600"
-    }
-  }'`}
-                            
-
+ {/* Configuration Section */} +
+
+ +

Advanced Configuration

+
+
+
+

Health Checks

+

+ You can configure custom health check endpoints to ensure your application is running correctly. Container Engine will periodically send requests to this endpoint. If it fails to respond successfully a number of times, the container will be automatically restarted. +

+
+
+

Resource Limits

+

+ To ensure optimal performance and manage costs, you can set specific CPU and memory limits for your deployments. This prevents a single application from consuming excessive resources and guarantees a baseline level of performance. +

+
+

+ These options can be configured when creating or updating a deployment. Check the Create Deployment endpoint in the API Reference for the full schema. +

+
+
+ + {/* --- API REFERENCE --- */} +
+
+
+
+
+ +
+
+

API Reference

+

Detailed endpoints for interacting with the Container Engine API.

+
+
+
-
-
-
Request Body Schema
-
-                                {`{
-  "env_vars": {
-    "KEY_NAME": "string"  // Key-value pairs
-  }
-}`}
-                              
-

- Note: Only specified variables are updated. Existing variables remain unchanged. -

-
+
+ {/* Base URL */} +
+

Base URL

+ + https://decenter.run/v1 + +
-
-
Success Response (200)
-
-                                {`{
-  "message": "Environment variables updated",
-  "env_vars": {
-    "NODE_ENV": "production",
-    "NEW_FEATURE_ENABLED": "true",
-    "API_TIMEOUT": "30000",
-    "CACHE_TTL": "3600"
-  },
-  "updated_at": "2025-10-14T10:45:00Z"
-}`}
-                              
-
-
-
-
+ {/* ================================================================== */} + {/* Authentication Endpoints */} + {/* ================================================================== */} +
+

Authentication

+ + {/* Register */} +
+
+

Register User

+ POST /auth/register +
+

Creates a new user account.

+ {/* ... existing curl block ... */} +
- {/* Deployment Restart */} -
-
-

- 8 - Deployment Restart -

- POST -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X POST https://decenter.run/v1/deployments/dep_9f8e7d6c5b4a3210/restart \\
-  -H "Authorization: Bearer sk-proj-abc123def456..."`}
-                          
-
+ {/* Login */} +
+
+

Login

+ POST /auth/login +
+

Authenticates a user and returns JWT access and refresh tokens.

+ {/* ... existing curl block ... */} +
-
-
-
Success Response (200)
-
-                              {`{
-  "message": "Deployment restart initiated successfully",
-  "deployment_id": "dep_9f8e7d6c5b4a3210",
-  "status": "restarting",
-  "restart_timestamp": "2025-10-14T10:50:00Z"
-}`}
-                            
-
+ {/* Refresh Token */} +
+
+

Refresh Access Token

+ POST /auth/refresh +
+

Uses a refresh token to issue a new access token.

+
+
+ cURL Request + +
+
{`curl -X POST https://decenter.run/v1/auth/refresh \\
+-H "Content-Type: application/json" \\
+-d '{
+  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+}'`}
+
+
-
-
Restart Process
-
    -
  • โ€ข Rolling restart (zero downtime)
  • -
  • โ€ข Pods restarted one by one
  • -
  • โ€ข Environment variables reloaded
  • -
  • โ€ข Health checks performed
  • -
  • โ€ข WebSocket notification sent
  • -
-
-
-
+ {/* Logout */} +
+
+

Logout

+ POST /auth/logout +
+

Invalidates the user's refresh token, effectively logging them out.

+
+
+ cURL Request + +
+
{`curl -X POST https://decenter.run/v1/auth/logout \\
+-H "Content-Type: application/json" \\
+-d '{
+  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+}'`}
+
+
+
- {/* Security Notice */} -
-
- -
-

Security Best Practices

-
    -
  • โ€ข Store API keys securely in environment variables
  • -
  • โ€ข Use different API keys for different environments
  • -
  • โ€ข Rotate API keys regularly for enhanced security
  • -
  • โ€ข Never commit API keys to version control
  • -
-
-
-
+ {/* ================================================================== */} + {/* API Keys Endpoints */} + {/* ================================================================== */} +
+

API Keys

+ + {/* List API Keys */} +
+
+

List API Keys

+ GET /api-keys +
+

Retrieves a list of all API keys for the authenticated user.

+
+
+ cURL Request +
-
-
+
{`curl -X GET https://decenter.run/v1/api-keys \\
+-H "Authorization: Bearer "`}
+
+
- {/* Webhooks Section */} -
-
- -

Webhooks

-
+ {/* Create API Key */} +
+
+

Create API Key

+ POST /api-keys +
+

Generates a new API key for programmatic access.

+ {/* ... existing curl block ... */} +
-
-
-

- Receive real-time notifications about deployment events via webhooks. -

+ {/* Revoke API Key */} +
+
+

Revoke API Key

+ DELETE /api-keys/{'{key_id}'} +
+

Permanently deletes an API key.

+
+
+ cURL Request +
+
{`curl -X DELETE https://decenter.run/v1/api-keys/key_1a2b3c4d5e \\
+-H "Authorization: Bearer "`}
+
+
+
-
- {/* Create Webhook */} -
-
-

- 9 - Create Webhook -

- POST -
- -
-
-
-
-
-
-
-
- curl -
- -
-
-                            {`curl -X POST https://decenter.run/v1/webhooks \\
-  -H "Authorization: Bearer sk-proj-abc123def456..." \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "url": "https://your-app.com/webhook",
-    "events": ["deployment.created", "deployment.updated", "deployment.failed"],
-    "secret": "your-webhook-secret",
-    "active": true
-  }'`}
-                          
-
+ {/* ================================================================== */} + {/* User Profile Endpoints */} + {/* ================================================================== */} +
+

User Profile

+ + {/* Get User Profile */} +
+
+

Get User Profile

+ GET /user/profile +
+

Retrieves the profile information of the authenticated user.

+
+
+ cURL Request + +
+
{`curl -X GET https://decenter.run/v1/user/profile \\
+-H "Authorization: Bearer "`}
+
+
-
-
-
Request Body Schema
-
-                              {`{
-  "url": "string",              // Required: Webhook URL
-  "events": ["string"],         // Required: Event types
-  "secret": "string",           // Optional: Webhook secret
-  "active": boolean             // Optional: Default true
-}`}
-                            
-
+ {/* Update User Profile */} +
+
+

Update User Profile

+ PUT /user/profile +
+

Updates the profile information of the authenticated user.

+
+
+ cURL Request + +
+
{`curl -X PUT https://decenter.run/v1/user/profile \\
+-H "Authorization: Bearer " \\
+-H "Content-Type: application/json" \\
+-d '{
+  "username": "new_username",
+  "email": "new.email@example.com"
+}'`}
+
+
-
-
Success Response (201)
-
-                              {`{
-  "id": "wh_1a2b3c4d5e6f7g8h",
-  "url": "https://your-app.com/webhook",
-  "events": ["deployment.created", "deployment.updated"],
-  "active": true,
-  "created_at": "2025-10-14T10:55:00Z"
-}`}
-                            
-
-
+ {/* Change Password */} +
+
+

Change Password

+ PUT /user/password +
+

Changes the password for the authenticated user.

+
+
+ cURL Request + +
+
{`curl -X PUT https://decenter.run/v1/user/password \\
+-H "Authorization: Bearer " \\
+-H "Content-Type: application/json" \\
+-d '{
+  "current_password": "MySecure123!",
+  "new_password": "ANewVerySecurePassword456!",
+  "confirm_new_password": "ANewVerySecurePassword456!"
+}'`}
+
+
+
-
-
Available Events
-
-
โ€ข deployment.created
-
โ€ข deployment.updated
-
โ€ข deployment.started
-
โ€ข deployment.stopped
-
โ€ข deployment.failed
-
โ€ข deployment.restarted
-
-
-
+ {/* ================================================================== */} + {/* Deployments Endpoints */} + {/* ================================================================== */} +
+

Deployments

+ + {/* List Deployments */} +
+
+

List Deployments

+ GET /deployments +
+

Retrieves a paginated list of all deployments for the authenticated user.

+
+
+ cURL Request +
-
-
+
{`curl -X GET "https://decenter.run/v1/deployments?page=1&limit=10" \\
+-H "Authorization: Bearer "`}
+
+
- {/* Quick Start */} -
-
- -

Quick Start

-
+ {/* Create Deployment */} +
+
+

Create Deployment

+ POST /deployments +
+

Creates and starts a new deployment from a container image.

+ {/* ... existing curl block ... */} +
-
-
-

- Get up and running with Container Engine in minutes with our comprehensive examples. -

+ {/* Get Deployment Details */} +
+
+

Get Deployment Details

+ GET /deployments/{'{deployment_id}'} +
+

Retrieves the full details of a specific deployment.

+ {/* ... existing curl block for Get Deployment ... */} +
+ + {/* Update Deployment */} +
+
+

Update Deployment

+ PUT /deployments/{'{deployment_id}'} +
+

Updates a deployment's configuration, such as its image or replica count. This triggers a redeployment.

+
+
+ cURL Request +
+
{`curl -X PUT https://decenter.run/v1/deployments/dep_9f8e7d6c \\
+-H "Authorization: Bearer " \\
+-H "Content-Type: application/json" \\
+-d '{
+  "image": "nginx:1.22-alpine",
+  "replicas": 3
+}'`}
+
+
-
- {/* Security Notice */} -
-
- -
-

Security Best Practices

-
    -
  • โ€ข Store API keys securely in environment variables
  • -
  • โ€ข Use different API keys for different environments
  • -
  • โ€ข Rotate API keys regularly for enhanced security
  • -
  • โ€ข Never commit API keys to version control
  • -
-
-
-
+ {/* Delete Deployment */} +
+
+

Delete Deployment

+ DELETE /deployments/{'{deployment_id}'} +
+

Stops and permanently deletes a deployment and all associated resources.

+
+
+ cURL Request +
-
-
+
{`curl -X DELETE https://decenter.run/v1/deployments/dep_9f8e7d6c \\
+-H "Authorization: Bearer "`}
+
+
- {/* API Reference */} -
-
- -

API Reference

-
+ {/* Scale Deployment */} +
+
+

Scale Deployment

+ PATCH /deployments/{'{deployment_id}'}/scale +
+

Adjusts the number of running instances (replicas) for a deployment.

+
+
+ cURL Request + +
+
{`curl -X PATCH https://decenter.run/v1/deployments/dep_9f8e7d6c/scale \\
+-H "Authorization: Bearer " \\
+-H "Content-Type: application/json" \\
+-d '{
+  "replicas": 5
+}'`}
+
+
-
-
-

Base URL

- - https://decenter.run/ - + {/* Start Deployment */} +
+
+

Start Deployment

+ POST /deployments/{'{deployment_id}'}/start +
+

Starts a stopped deployment.

+
+
+ cURL Request +
+
{`curl -X POST https://decenter.run/v1/deployments/dep_9f8e7d6c/start \\
+-H "Authorization: Bearer "`}
+
+
-
- {[ - // Authentication - { method: 'POST', endpoint: '/v1/auth/register', description: 'Register a new user', color: 'green' }, - { method: 'POST', endpoint: '/v1/auth/login', description: 'Login and get access token', color: 'blue' }, - { method: 'POST', endpoint: '/v1/auth/refresh', description: 'Refresh access token', color: 'blue' }, - { method: 'POST', endpoint: '/v1/auth/logout', description: 'Logout user', color: 'red' }, - { method: 'POST', endpoint: '/v1/auth/forgot-password', description: 'Request password reset', color: 'yellow' }, - { method: 'POST', endpoint: '/v1/auth/reset-password', description: 'Reset password', color: 'yellow' }, - // API Keys - { method: 'GET', endpoint: '/v1/api-keys', description: 'List API keys', color: 'blue' }, - { method: 'POST', endpoint: '/v1/api-keys', description: 'Create a new API key', color: 'green' }, - { method: 'DELETE', endpoint: '/v1/api-keys/{key_id}', description: 'Revoke an API key', color: 'red' }, - // User Profile - { method: 'GET', endpoint: '/v1/user/profile', description: 'Get user profile', color: 'blue' }, - { method: 'PUT', endpoint: '/v1/user/profile', description: 'Update user profile', color: 'yellow' }, - { method: 'PUT', endpoint: '/v1/user/password', description: 'Change user password', color: 'yellow' }, - // Deployments - { method: 'GET', endpoint: '/v1/deployments', description: 'List all deployments', color: 'blue' }, - { method: 'POST', endpoint: '/v1/deployments', description: 'Create a new deployment', color: 'green' }, - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}', description: 'Get deployment details', color: 'blue' }, - { method: 'PUT', endpoint: '/v1/deployments/{deployment_id}', description: 'Update deployment', color: 'yellow' }, - { method: 'DELETE', endpoint: '/v1/deployments/{deployment_id}', description: 'Delete deployment', color: 'red' }, - { method: 'PATCH', endpoint: '/v1/deployments/{deployment_id}/scale', description: 'Scale deployment', color: 'yellow' }, - { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/start', description: 'Start deployment', color: 'green' }, - { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/stop', description: 'Stop deployment', color: 'red' }, - { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/restart', description: 'Restart deployment', color: 'yellow' }, - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/env', description: 'Get environment variables', color: 'blue' }, - { method: 'PATCH', endpoint: '/v1/deployments/{deployment_id}/env', description: 'Update environment variables', color: 'yellow' }, - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/metrics', description: 'Get deployment metrics', color: 'blue' }, - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/status', description: 'Get deployment status', color: 'blue' }, - // Domains - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/domains', description: 'List domains for deployment', color: 'blue' }, - { method: 'POST', endpoint: '/v1/deployments/{deployment_id}/domains', description: 'Add domain to deployment', color: 'green' }, - { method: 'DELETE', endpoint: '/v1/deployments/{deployment_id}/domains/{domain_id}', description: 'Remove domain from deployment', color: 'red' }, - // Logs - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/logs', description: 'Get deployment logs', color: 'blue' }, - { method: 'GET', endpoint: '/v1/deployments/{deployment_id}/logs/stream', description: 'Stream deployment logs (WebSocket)', color: 'blue' }, - // Notifications - { method: 'GET', endpoint: '/v1/ws/notifications', description: 'WebSocket notifications', color: 'blue' }, - { method: 'GET', endpoint: '/v1/ws/health', description: 'WebSocket health check', color: 'blue' }, - { method: 'GET', endpoint: '/v1/notifications/stats', description: 'Get notification stats', color: 'blue' }, - // Webhooks - { method: 'GET', endpoint: '/v1/webhooks', description: 'List webhooks', color: 'blue' }, - { method: 'POST', endpoint: '/v1/webhooks', description: 'Create webhook', color: 'green' }, - { method: 'GET', endpoint: '/v1/webhooks/{webhook_id}', description: 'Get webhook details', color: 'blue' }, - { method: 'PUT', endpoint: '/v1/webhooks/{webhook_id}', description: 'Update webhook', color: 'yellow' }, - { method: 'DELETE', endpoint: '/v1/webhooks/{webhook_id}', description: 'Delete webhook', color: 'red' }, - // Health - { method: 'GET', endpoint: '/health', description: 'Health check endpoint', color: 'blue' }, - ].map((api, index) => ( -
-
- - {api.method} - - {api.endpoint} -
-

{api.description}

-
- ))} + {/* Stop Deployment */} +
+
+

Stop Deployment

+ POST /deployments/{'{deployment_id}'}/stop +
+

Stops a running deployment by scaling it down to zero replicas.

+
+
+ cURL Request +
-
-
+
{`curl -X POST https://decenter.run/v1/deployments/dep_9f8e7d6c/stop \\
+-H "Authorization: Bearer "`}
+
+ + + {/* Restart Deployment */} +
+
+

Restart Deployment

+ POST /deployments/{'{deployment_id}'}/restart +
+

Initiates a zero-downtime rolling restart for a deployment.

+ {/* ... existing curl block ... */} +
- {/* Deployment Guide */} -
-
- -

Deployment Guide

-
+ -
-

Deploy Your First Container

-
- -
-                        {`curl -X POST https://decenter.run/v1/deployments \\
-  -H "Authorization: Bearer " \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "app_name": "hello-world",
-    "image": "nginx:latest",
-    "port": 80,
-    "env_vars": {
-      "ENVIRONMENT": "production"
-    },
-    "replicas": 1
-  }'`}
-                      
-
+ {/* ================================================================== */} + {/* Environment Variables Endpoints */} + {/* ================================================================== */} +
+

Environment Variables

+ + {/* Get Environment Variables */} +
+
+

Get Environment Variables

+ GET /deployments/{'{deployment_id}'}/env +
+

Retrieves all environment variables for a specific deployment.

+ {/* ... existing curl block ... */} +
-
-

Success Response

-
-                        {`{
-  "id": "dpl-a1b2c3d4e5",
-  "app_name": "hello-world",
-  "status": "pending",
-  "url": "https://hello-world.vinhomes.co.uk",
-  "message": "Deployment is being processed"
-}`}
-                      
+ {/* Update Environment Variables */} +
+
+

Update Environment Variables

+ PATCH /deployments/{'{deployment_id}'}/env +
+

Adds or updates environment variables for a deployment. This triggers a zero-downtime restart to apply the changes.

+ {/* ... existing curl block ... */} +
+
+ + {/* ================================================================== */} + {/* Domains Endpoints */} + {/* ================================================================== */} +
+

Custom Domains

+ + {/* List Domains */} +
+
+

List Custom Domains

+ GET /deployments/{'{deployment_id}'}/domains +
+

Lists all custom domains associated with a deployment.

+
+
+ cURL Request +
-
-
- - {/* Examples */} -
-
- -

Examples

-
+
{`curl -X GET https://decenter.run/v1/deployments/dep_9f8e7d6c/domains \\
+-H "Authorization: Bearer "`}
+ + -
-
-

Python Application

-
-
-                          {`# Deploy a Python Flask app
-{
-  "app_name": "my-python-app",
-  "image": "python:3.9-slim",
-  "port": 5000,
-  "env_vars": {
-    "FLASK_ENV": "production",
-    "DATABASE_URL": "postgresql://..."
-  }
-}`}
-                        
-
+ {/* Add Domain */} +
+
+

Add Custom Domain

+ POST /deployments/{'{deployment_id}'}/domains +
+

Adds a custom domain to a deployment and begins the verification process. Returns the necessary DNS records for configuration.

+
+
+ cURL Request +
+
{`curl -X POST https://decenter.run/v1/deployments/dep_9f8e7d6c/domains \\
+-H "Authorization: Bearer " \\
+-H "Content-Type: application/json" \\
+-d '{
+  "domain": "www.my-awesome-app.com"
+}'`}
+
+
-
-

Node.js Application

-
-
-                          {`# Deploy a Node.js Express app
-{
-  "app_name": "my-node-app",
-  "image": "node:16-alpine",
-  "port": 3000,
-  "env_vars": {
-    "NODE_ENV": "production",
-    "API_KEY": "your-api-key"
-  },
-  "replicas": 3
-}`}
-                        
-
+ {/* Remove Domain */} +
+
+

Remove Custom Domain

+ DELETE /deployments/{'{deployment_id}'}/domains/{'{domain_id}'} +
+

Removes a custom domain from a deployment.

+
+
+ cURL Request + +
+
{`curl -X DELETE https://decenter.run/v1/deployments/dep_9f8e7d6c/domains/dom_12345678 \\
+-H "Authorization: Bearer "`}
+
+
+
+ + {/* ================================================================== */} + {/* Logs Endpoints */} + {/* ================================================================== */} +
+

Logs

+ + {/* Get Logs */} +
+
+

Get Deployment Logs

+ GET /deployments/{'{deployment_id}'}/logs +
+

Retrieves historical logs for all pods in a deployment.

+
+
+ cURL Request +
+
{`curl -X GET "https://decenter.run/v1/deployments/dep_9f8e7d6c/logs?limit=100" \\
+-H "Authorization: Bearer "`}
+
+
-
-

Environment Variables Management

-
-
-

Update environment variables:

-
-
-                              {`# Update specific environment variables
-PATCH /v1/deployments/{deployment_id}/env
-{
-  "env_vars": {
-    "DEBUG_MODE": "true",
-    "CACHE_TTL": "3600"
-  }
-}`}
-                            
-
-
-
-

Restart deployment to apply changes:

-
-
-                              {`# Perform rolling restart
-POST /v1/deployments/{deployment_id}/restart`}
-                            
-
-
-
+ {/* Stream Logs */} +
+
+

Stream Deployment Logs

+ GET /deployments/{'{deployment_id}'}/logs/stream +
+

Establishes a WebSocket connection to stream real-time logs from a deployment.

+
+
+ + {/* ================================================================== */} + {/* Webhooks Endpoints */} + {/* ================================================================== */} +
+

Webhooks

+ + {/* List Webhooks */} +
+
+

List Webhooks

+ GET /webhooks +
+

Retrieves a list of all webhooks configured for the user.

+
+
+ cURL Request +
-
-
+
{`curl -X GET https://decenter.run/v1/webhooks \\
+-H "Authorization: Bearer "`}
+ + - {/* Configuration */} -
-
- -

Configuration

-
+ {/* Create Webhook */} +
+
+

Create Webhook

+ POST /webhooks +
+

Creates a new webhook endpoint to receive notifications for deployment events.

+ {/* ... existing curl block ... */} +
-
-
-

Environment Variables

-

- Configure your application using environment variables for maximum flexibility and security. -

-
-

- Security Note: Environment variables are encrypted at rest and in transit. - Avoid storing sensitive data in plain text. -

-
+ {/* Get Webhook */} +
+
+

Get Webhook Details

+ GET /webhooks/{'{webhook_id}'} +
+

Retrieves the details of a specific webhook.

+
+
+ cURL Request +
+
{`curl -X GET https://decenter.run/v1/webhooks/wh_1a2b3c4d \\
+-H "Authorization: Bearer "`}
+
+
-
-

Health Checks

-

- Configure custom health check endpoints to ensure your application is running correctly. -

-
-
-                          {`{
-  "health_check": {
-    "path": "/health",
-    "initial_delay_seconds": 30,
-    "period_seconds": 10,
-    "timeout_seconds": 5,
-    "failure_threshold": 3
-  }
-}`}
-                        
-
+ {/* Update Webhook */} +
+
+

Update Webhook

+ PUT /webhooks/{'{webhook_id}'} +
+

Updates a webhook's configuration, such as its URL or subscribed events.

+
+
+ cURL Request +
+
{`curl -X PUT https://decenter.run/v1/webhooks/wh_1a2b3c4d \\
+-H "Authorization: Bearer " \\
+-H "Content-Type: application/json" \\
+-d '{
+  "url": "https://new-app.com/webhook",
+  "events": ["deployment.started", "deployment.stopped"],
+  "active": false
+}'`}
+
+
-
-

Resource Limits

-

- Set CPU and memory limits to ensure optimal performance and cost management. -

-
-
-                          {`{
-  "resources": {
-    "cpu": "500m",      // 0.5 CPU cores
-    "memory": "512Mi"   // 512 MB RAM
-  }
-}`}
-                        
-
+ {/* Delete Webhook */} +
+
+

Delete Webhook

+ DELETE /webhooks/{'{webhook_id}'} +
+

Deletes a webhook.

+
+
+ cURL Request +
-
-
- +
{`curl -X DELETE https://decenter.run/v1/webhooks/wh_1a2b3c4d \\
+-H "Authorization: Bearer "`}
+ + + + + {/* ================================================================== */} + {/* Notifications Endpoint */} + {/* ================================================================== */} +
+

Real-time Notifications

+ + {/* WebSocket Notifications */} +
+
+

WebSocket Notifications

+ GET /ws/notifications +
+

Establishes a WebSocket connection to receive real-time notifications about events related to your account and deployments.

+
+
Connection URL
+
wss://decenter.run/v1/ws/notifications
+

+ Note: You must include your JWT access token as a query parameter for authentication, like so: `?token={'{jwt_access_token}'}`. +

+
+
+
+ + + + + {/* Modern Footer */}