diff --git a/.env.development b/.env.development index 0f88f9d..63df434 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,6 @@ # Development environment configuration for Container Engine # Database Configuration -DATABASE_URL=postgresql://postgres:password@localhost:5432/container_engine +DATABASE_URL=postgresql://postgres:1@localhost:5432/container_engine # Redis Configuration REDIS_URL=redis://localhost:6379 diff --git a/.env.example b/.env.example index 1e926c6..dcde3c8 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Database Configuration -DATABASE_URL=postgresql://postgres:password@localhost:5432/container_engine +DATABASE_URL=postgresql://postgres:1@localhost:5432/container_engine # Redis Configuration REDIS_URL=redis://localhost:6379 diff --git a/.env.integrate_test b/.env.integrate_test index 17f6dd9..f696273 100644 --- a/.env.integrate_test +++ b/.env.integrate_test @@ -1,6 +1,6 @@ # Integration test environment configuration for Container Engine # Database Configuration -DATABASE_URL=postgresql://postgres:password@localhost:5432/container_engine_test +DATABASE_URL=postgresql://postgres:1@localhost:5432/container_engine_test # Redis Configuration REDIS_URL=redis://localhost:6379 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-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-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json b/.sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json similarity index 62% rename from .sqlx/query-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json rename to .sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json index 4993d48..b553eab 100644 --- a/.sqlx/query-3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426.json +++ b/.sqlx/query-227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, app_name, status, replicas FROM deployments WHERE id = $1 AND user_id = $2", + "query": "SELECT id, app_name, env_vars, updated_at FROM deployments WHERE id = $1 AND user_id = $2", "describe": { "columns": [ { @@ -15,13 +15,13 @@ }, { "ordinal": 2, - "name": "status", - "type_info": "Varchar" + "name": "env_vars", + "type_info": "Jsonb" }, { "ordinal": 3, - "name": "replicas", - "type_info": "Int4" + "name": "updated_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -37,5 +37,5 @@ false ] }, - "hash": "3b4271cb4ae120eb8ffbdc1146ceb3a5212e7a7893f7394fc24b3b4052d82426" + "hash": "227c6ddbee82d574c1df8ac08b00572b6d58d0621d3915e0f90e04486f0b14ef" } diff --git a/.sqlx/query-3303a4da1ddb7f16a00c57b6c2b7155359441ba7509b3618c0dca5dba790e06b.json b/.sqlx/query-3303a4da1ddb7f16a00c57b6c2b7155359441ba7509b3618c0dca5dba790e06b.json new file mode 100644 index 0000000..62b716f --- /dev/null +++ b/.sqlx/query-3303a4da1ddb7f16a00c57b6c2b7155359441ba7509b3618c0dca5dba790e06b.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, app_name 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" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "3303a4da1ddb7f16a00c57b6c2b7155359441ba7509b3618c0dca5dba790e06b" +} diff --git a/.sqlx/query-3c97dd5fd2911ec1ba8f34cb86cfc90d12a4fd67b623ad532b4f5f3d2de039f2.json b/.sqlx/query-3c97dd5fd2911ec1ba8f34cb86cfc90d12a4fd67b623ad532b4f5f3d2de039f2.json new file mode 100644 index 0000000..aea1519 --- /dev/null +++ b/.sqlx/query-3c97dd5fd2911ec1ba8f34cb86cfc90d12a4fd67b623ad532b4f5f3d2de039f2.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, domain FROM domains WHERE id = $1 AND deployment_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "domain", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "3c97dd5fd2911ec1ba8f34cb86cfc90d12a4fd67b623ad532b4f5f3d2de039f2" +} diff --git a/.sqlx/query-3e778d680cadfe22f70845aa3b23616b7f8cd26bfac7c7ce2449b5a94a35aeec.json b/.sqlx/query-3e778d680cadfe22f70845aa3b23616b7f8cd26bfac7c7ce2449b5a94a35aeec.json new file mode 100644 index 0000000..485579a --- /dev/null +++ b/.sqlx/query-3e778d680cadfe22f70845aa3b23616b7f8cd26bfac7c7ce2449b5a94a35aeec.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM domains WHERE domain = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "3e778d680cadfe22f70845aa3b23616b7f8cd26bfac7c7ce2449b5a94a35aeec" +} 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-58e8b0bfe9d514bce74ca5037f8816baec7232f9ae17ff80030d256f56c91dc5.json b/.sqlx/query-58e8b0bfe9d514bce74ca5037f8816baec7232f9ae17ff80030d256f56c91dc5.json new file mode 100644 index 0000000..300c5ab --- /dev/null +++ b/.sqlx/query-58e8b0bfe9d514bce74ca5037f8816baec7232f9ae17ff80030d256f56c91dc5.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, domain, status, created_at, verified_at\n FROM domains \n WHERE deployment_id = $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "domain", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "verified_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true + ] + }, + "hash": "58e8b0bfe9d514bce74ca5037f8816baec7232f9ae17ff80030d256f56c91dc5" +} diff --git a/.sqlx/query-5b6879e3a8df61a893bd23d556da137694fc8a0d8fcb88b857c8216d2276081b.json b/.sqlx/query-5b6879e3a8df61a893bd23d556da137694fc8a0d8fcb88b857c8216d2276081b.json new file mode 100644 index 0000000..480cbdb --- /dev/null +++ b/.sqlx/query-5b6879e3a8df61a893bd23d556da137694fc8a0d8fcb88b857c8216d2276081b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE domains SET status = 'failed' WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "5b6879e3a8df61a893bd23d556da137694fc8a0d8fcb88b857c8216d2276081b" +} 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-75ff3e73dc7235391e9b026f68f59dbf3a35ec38a46a30bffe68b2eae6b28795.json b/.sqlx/query-75ff3e73dc7235391e9b026f68f59dbf3a35ec38a46a30bffe68b2eae6b28795.json new file mode 100644 index 0000000..dc548af --- /dev/null +++ b/.sqlx/query-75ff3e73dc7235391e9b026f68f59dbf3a35ec38a46a30bffe68b2eae6b28795.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO dns_records (id, domain_id, record_type, record_name, record_value, ttl)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Varchar", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "75ff3e73dc7235391e9b026f68f59dbf3a35ec38a46a30bffe68b2eae6b28795" +} diff --git a/.sqlx/query-7912232e1b734ea8b8985b5d6372130bbd346edfb234cd4363e607965bdeb39d.json b/.sqlx/query-7912232e1b734ea8b8985b5d6372130bbd346edfb234cd4363e607965bdeb39d.json new file mode 100644 index 0000000..8c15b67 --- /dev/null +++ b/.sqlx/query-7912232e1b734ea8b8985b5d6372130bbd346edfb234cd4363e607965bdeb39d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM domains WHERE id = $1 AND deployment_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "7912232e1b734ea8b8985b5d6372130bbd346edfb234cd4363e607965bdeb39d" +} diff --git a/.sqlx/query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json b/.sqlx/query-92ab85178ef7854f43bbb51527cfc97e725ac664e15a7d20687906d6d2268f9b.json similarity index 57% rename from .sqlx/query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json rename to .sqlx/query-92ab85178ef7854f43bbb51527cfc97e725ac664e15a7d20687906d6d2268f9b.json index 2d15cc8..5a2882b 100644 --- a/.sqlx/query-3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed.json +++ b/.sqlx/query-92ab85178ef7854f43bbb51527cfc97e725ac664e15a7d20687906d6d2268f9b.json @@ -1,15 +1,15 @@ { "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 = $2 WHERE id = $1", "describe": { "columns": [], "parameters": { "Left": [ - "Text", - "Uuid" + "Uuid", + "Text" ] }, "nullable": [] }, - "hash": "3502b7cabac60c2ea61a3711f1c338339c00618bed9e984248f86cd22644a2ed" + "hash": "92ab85178ef7854f43bbb51527cfc97e725ac664e15a7d20687906d6d2268f9b" } 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-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-d84d1fba5c18f5e8d1868f3d52de1e2cd3f96b8cb34644761e114d33c06bf017.json b/.sqlx/query-d84d1fba5c18f5e8d1868f3d52de1e2cd3f96b8cb34644761e114d33c06bf017.json deleted file mode 100644 index c1be521..0000000 --- a/.sqlx/query-d84d1fba5c18f5e8d1868f3d52de1e2cd3f96b8cb34644761e114d33c06bf017.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE deployments SET status = 'stopped', replicas = 0, updated_at = NOW() WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "d84d1fba5c18f5e8d1868f3d52de1e2cd3f96b8cb34644761e114d33c06bf017" -} diff --git a/.sqlx/query-d8dacc4dd5efaa9764ca394f7fa34c1d6cea7ca8b948061503d0918ce63648f8.json b/.sqlx/query-d8dacc4dd5efaa9764ca394f7fa34c1d6cea7ca8b948061503d0918ce63648f8.json new file mode 100644 index 0000000..781fa2f --- /dev/null +++ b/.sqlx/query-d8dacc4dd5efaa9764ca394f7fa34c1d6cea7ca8b948061503d0918ce63648f8.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO domains (id, deployment_id, domain, status, created_at)\n VALUES ($1, $2, $3, 'configured', $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "d8dacc4dd5efaa9764ca394f7fa34c1d6cea7ca8b948061503d0918ce63648f8" +} diff --git a/.sqlx/query-e736ef72a77ba3db3b886be944fe5831a1ef51bb4561a165dfa9b148ef09671c.json b/.sqlx/query-e736ef72a77ba3db3b886be944fe5831a1ef51bb4561a165dfa9b148ef09671c.json new file mode 100644 index 0000000..5daf507 --- /dev/null +++ b/.sqlx/query-e736ef72a77ba3db3b886be944fe5831a1ef51bb4561a165dfa9b148ef09671c.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT error_message FROM deployments WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "error_message", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "e736ef72a77ba3db3b886be944fe5831a1ef51bb4561a165dfa9b148ef09671c" +} 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/.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/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/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/ENVIRONMENT_CONFIG.md b/ENVIRONMENT_CONFIG.md deleted file mode 100644 index d29de3a..0000000 --- a/ENVIRONMENT_CONFIG.md +++ /dev/null @@ -1,53 +0,0 @@ -# Environment Configuration - -This project uses environment-specific configuration files to manage different deployment and testing scenarios. - -## Environment Files - -- `.env.development` - Configuration for local development -- `.env.integrate_test` - Configuration for integration testing -- `.env.example` - Example configuration template - -## Usage - -### Development -```bash -# Uses .env.development by default or when ENVIRONMENT=development -cargo run - -# Explicitly set development environment -ENVIRONMENT=development cargo run -``` - -### Integration Testing -```bash -# Run integration tests (automatically uses .env.integrate_test) -ENVIRONMENT=integrate_test cargo run - -# Run Python integration tests -python -m pytest tests/integrate/ -v -``` - -## Key Differences - -### Development (.env.development) -- Port: 3000 -- Database: `container_engine` -- Namespace: `container-engine-dev` -- Domain: `dev.container-engine.app` -- Log level: debug - -### Integration Test (.env.integrate_test) -- Port: 3001 (avoids conflicts) -- Database: `container_engine_test` -- Namespace: `container-engine-test` -- Domain: `test.container-engine.app` -- Log level: info - -## Port Separation - -Different ports are used to prevent conflicts when running development server and tests simultaneously: -- Development: port 3000 -- Integration tests: port 3001 - -This ensures GitHub Actions and local testing won't encounter "port already in use" errors. \ No newline at end of file diff --git a/PORT_CONFLICT_FIX.md b/PORT_CONFLICT_FIX.md deleted file mode 100644 index 4cd3a03..0000000 --- a/PORT_CONFLICT_FIX.md +++ /dev/null @@ -1,50 +0,0 @@ -# GitHub Actions Port Conflict Fix - -## Problem -GitHub Actions integration tests were failing due to port conflicts. The issue occurred because: - -1. GitHub Actions provides PostgreSQL (port 5432) and Redis (port 6379) as services -2. The test setup code attempted to start its own Docker containers on the same ports -3. This caused port binding conflicts and test failures - -## Solution -Added environment detection to automatically handle both CI and local development environments: - -### Key Changes - -1. **Environment Detection** (`tests/integrate/conftest.py`) - - Added `_detect_github_actions()` method that checks for: - - `GITHUB_ACTIONS=true` - - `CI=true` - - `RUNNER_OS` environment variable - -2. **Conditional Container Management** - - **In GitHub Actions**: Skip Docker container creation, use provided services - - **In Local Development**: Start Docker containers as before - -3. **Updated Cleanup Logic** (`tests/run_tests.sh`) - - Skip container cleanup in CI environments - - Preserve normal cleanup in local development - -### Testing -The fix was comprehensively tested with 6 test cases covering: -- Environment detection in various CI scenarios -- Container management logic for both environments -- Proper cleanup behavior - -## Usage - -### Local Development -```bash -# Works as before - containers are started automatically -make test-integration -``` - -### GitHub Actions -```yaml -# No changes needed - detection is automatic -- name: Run integration tests - run: python -m pytest tests/integrate/ -v -``` - -The fix automatically detects the environment and handles container management appropriately, eliminating port conflicts while maintaining compatibility with existing workflows. \ No newline at end of file diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md deleted file mode 100644 index 930aac3..0000000 --- a/PROJECT_STATUS.md +++ /dev/null @@ -1,223 +0,0 @@ -# 🚀 Container Engine - Development Status - -## Project Overview - -This repository now contains a **complete Rust-based web API project structure** implementing the Container Engine platform - an open-source alternative to Google Cloud Run. The project is built with modern Rust technologies and follows best practices for scalable web applications. - -## ✅ What's Implemented - -### 🏗️ **Core Infrastructure** -- **Rust + Axum** web framework setup with async/await support -- **PostgreSQL** database integration with SQLx for type-safe queries -- **Redis** caching layer for session management and performance -- **JWT-based authentication** with refresh tokens -- **API key management** for programmatic access -- **Comprehensive error handling** with structured error responses -- **Configuration management** with environment variables -- **Docker containerization** with multi-stage builds -- **Docker Compose** development environment - -### 🔐 **Authentication & Security** -- User registration and login system -- Password hashing with bcrypt -- JWT token generation and validation -- API key creation, management, and revocation -- Middleware-based authentication for protected routes -- Secure password change functionality - -### 👤 **User Management** -- User profile management (view, update) -- Account creation with email validation -- User statistics (deployment count, API key count) -- Profile update with conflict detection - -### 📦 **Deployment Management** (API Structure) -- Complete API endpoint structure for container deployments -- Deployment lifecycle management (create, read, update, delete) -- Scaling capabilities (horizontal scaling) -- Environment variable management -- Resource allocation and limits -- Health check configuration -- Custom domain mapping (API structure) -- Logs and metrics endpoints (API structure) - -### 🗄️ **Database Schema** -- **Users table** - User accounts and authentication -- **API keys table** - API key management with expiration -- **Deployments table** - Container deployment metadata -- **Domains table** - Custom domain mappings -- **Proper indexing** for performance -- **Auto-updating timestamps** with triggers -- **Foreign key constraints** for data integrity - -### 🔧 **Development Tools** -- **Makefile** with common development tasks -- **Docker Compose** for local development -- **Database migrations** with SQLx -- **Environment configuration** with validation -- **Development documentation** with setup instructions - -## 🏛️ **Architecture** - -``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Frontend/CLI │ │ Load Balancer │ │ API Gateway │ -│ │────│ (nginx) │────│ (Axum) │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - │ - ┌─────────────────────────────────┼─────────────────┐ - │ │ │ - ┌───────────────┐ ┌─────────────────┐ ┌─────────────┐ - │ PostgreSQL │ │ Redis │ │ Kubernetes │ - │ Database │ │ Cache │ │ Cluster │ - │ │ │ │ │ │ - └───────────────┘ └─────────────────┘ └─────────────┘ -``` - -## 🛠️ **Technology Stack** - -| Component | Technology | Purpose | -|-----------|------------|---------| -| **Backend Framework** | Rust + Axum | High-performance async web server | -| **Database** | PostgreSQL + SQLx | Type-safe database queries | -| **Cache** | Redis | Session management and caching | -| **Authentication** | JWT + bcrypt | Secure user authentication | -| **Containerization** | Docker + Docker Compose | Development and deployment | -| **Orchestration** | Kubernetes (API ready) | Container deployment platform | -| **Configuration** | Environment variables | Flexible configuration management | -| **Logging** | Tracing + tracing-subscriber | Structured logging | -| **Validation** | Validator crate | Request validation | -| **Error Handling** | thiserror + anyhow | Comprehensive error management | - -## 📁 **Project Structure** - -``` -Open-Container-Engine/ -├── src/ -│ ├── main.rs # Application entry point -│ ├── config.rs # Configuration management -│ ├── database.rs # Database setup and connections -│ ├── error.rs # Error types and handling -│ ├── auth/ # Authentication module -│ │ ├── models.rs # User and API key models -│ │ ├── jwt.rs # JWT token management -│ │ └── middleware.rs # Auth middleware -│ ├── user/ # User management -│ │ └── models.rs # User profile models -│ ├── deployment/ # Deployment management -│ │ └── models.rs # Deployment models -│ └── handlers/ # HTTP handlers -│ ├── auth.rs # Auth endpoints -│ ├── user.rs # User endpoints -│ └── deployment.rs # Deployment endpoints -├── migrations/ # Database migrations -├── docker-compose.yml # Development environment -├── Dockerfile # Container definition -├── Makefile # Development commands -├── DEVELOPMENT.md # Setup instructions -├── .env.example # Environment template -└── Cargo.toml # Dependencies and metadata -``` - -## 🚦 **Getting Started** - -### Quick Start (3 commands) -```bash -# 1. Clone and setup -git clone https://github.com/ngocbd/Open-Container-Engine.git -cd Open-Container-Engine -make setup - -# 2. Start databases and run migrations -make db-up && make migrate - -# 3. Start the development server -make dev -``` - -### Test the API -```bash -# Health check -curl http://localhost:3000/health - -# Register a user -curl -X POST http://localhost:3000/v1/auth/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "testuser", - "email": "test@example.com", - "password": "password123", - "confirmPassword": "password123" - }' -``` - -## 📊 **Current Status** - -### ✅ **Completed** -- [x] Complete Rust project structure -- [x] Axum web server with routing -- [x] PostgreSQL database with migrations -- [x] Redis caching integration -- [x] User authentication (JWT + API keys) -- [x] User management endpoints -- [x] Deployment API structure -- [x] Error handling and validation -- [x] Docker containerization -- [x] Development environment setup -- [x] Comprehensive documentation - -### 🔄 **In Progress** -- [ ] SQLx query compilation (requires database connection) -- [ ] Kubernetes integration for actual container deployment -- [ ] Container registry authentication -- [ ] Real-time logging and metrics collection -- [ ] Domain management and SSL certificates - -### 📋 **Next Steps** -1. **Fix SQLx compilation** - Set up database and run `cargo sqlx prepare` -2. **Kubernetes integration** - Implement actual container orchestration -3. **Container registry support** - Add Docker Hub, GCR, ECR integration -4. **Monitoring and logging** - Implement Prometheus metrics -5. **Domain management** - Add DNS and SSL certificate management -6. **Frontend dashboard** - Build user interface -7. **CI/CD pipeline** - Automated testing and deployment - -## 🎯 **API Endpoints Summary** - -### Authentication & Users -- `POST /v1/auth/register` - User registration -- `POST /v1/auth/login` - User login -- `GET /v1/user/profile` - Get user profile -- `POST /v1/api-keys` - Create API key - -### Deployment Management -- `POST /v1/deployments` - Deploy container -- `GET /v1/deployments` - List deployments -- `GET /v1/deployments/{id}` - Get deployment details -- `PUT /v1/deployments/{id}` - Update deployment -- `DELETE /v1/deployments/{id}` - Delete deployment -- `PATCH /v1/deployments/{id}/scale` - Scale deployment - -## 📚 **Documentation** - -- **[DEVELOPMENT.md](./DEVELOPMENT.md)** - Detailed setup and development guide -- **[APIs.md](./APIs.md)** - Complete API documentation -- **[README.md](./README.md)** - Original project overview -- **Inline code documentation** - Comprehensive code comments - -## 🤝 **Contributing** - -The project structure is ready for contributions! See the development guide for: -- Local setup instructions -- Code formatting and linting -- Database migration management -- Testing procedures -- Docker workflow - -## 📄 **License** - -MIT License - See the original README.md for full license details. - ---- - -**This project structure provides a solid foundation for building a production-ready container orchestration platform with Rust, Axum, PostgreSQL, and Redis.** \ No newline at end of file diff --git a/README-DOCKER.md b/README-DOCKER.md deleted file mode 100644 index 5a680e8..0000000 --- a/README-DOCKER.md +++ /dev/null @@ -1,99 +0,0 @@ -# Open Container Engine - Quick Start Guide - -## 🚀 How to Run - -### Step 1: Pull the Docker Image -```bash -docker pull decenter/open-container-engine:v1.0.0 -``` - -### Step 2: Create Kubernetes Config File -Create a file named `k8sConfig.yaml` in your current directory: - -```bash -# Create the config file -touch k8sConfig.yaml -``` - -Then edit `k8sConfig.yaml` with your Kubernetes cluster configuration: - -```yaml -# Example k8sConfig.yaml content -apiVersion: v1 -kind: Config -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTi... # Your cluster CA - server: https://your-k8s-api-server:6443 - name: your-cluster -contexts: -- context: - cluster: your-cluster - user: your-user - name: your-context -current-context: your-context -users: -- name: your-user - user: - token: eyJhbGciOiJSUzI1... # Your service account token -``` - -### Step 3: Run the Container -```bash -docker run -d \ - --name container-engine \ - -p 8080:3000 \ - -v $(pwd)/k8sConfig.yaml:/app/k8sConfig.yaml:ro \ - -e DATABASE_URL="postgresql://user:password@host:5432/database" \ - -e REDIS_URL="redis://host:6379" \ - -e JWT_SECRET="your-super-secret-jwt-key" \ - -e DOMAIN_SUFFIX="yourdomain.com" \ - -e KUBERNETES_NAMESPACE="default" \ - decenter/open-container-engine:v1.0.0 -``` - -### Step 4: Access the Application -- Web Interface: http://localhost:8080 -- Health Check: http://localhost:8080/health - -## 📋 Required Environment Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` | -| `REDIS_URL` | Redis connection string | `redis://host:6379` | -| `JWT_SECRET` | JWT signing secret | `your-super-secret-jwt-key` | -| `DOMAIN_SUFFIX` | Domain for deployments | `yourdomain.com` | -| `KUBERNETES_NAMESPACE` | K8s namespace (optional) | `default` | - -## 🔧 Optional Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `KUBECONFIG_PATH` | Path to kubeconfig | `./k8sConfig.yaml` | -| `ENVIRONMENT` | Environment mode | `development` | - -## 📁 File Structure -``` -your-project/ -├── k8sConfig.yaml # Your Kubernetes config (required) -└── docker-compose.yml # Optional: for development -``` - -## 🛠️ Development with Docker Compose - -For local development with included database and Redis: - -```bash -git clone -cd Open-Container-Engine -docker-compose up -``` - -This will start: -- PostgreSQL database -- Redis cache -- Container Engine application - -Access at: http://localhost:3000 \ No newline at end of file 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/App.tsx b/apps/container-engine-frontend/src/App.tsx index 5fbd01b..a529528 100644 --- a/apps/container-engine-frontend/src/App.tsx +++ b/apps/container-engine-frontend/src/App.tsx @@ -4,6 +4,7 @@ import { AuthProvider } from './context/AuthContext'; import { NotificationProvider } from './context/NotificationContext'; import LandingPage from './pages/LandingPage'; import AuthPage from './pages/AuthPage'; +import ResetPasswordPage from './pages/ResetPasswordPage'; import DashboardPage from './pages/DashboardPage'; import DeploymentsPage from './pages/DeploymentsPage'; import NewDeploymentPage from './pages/NewDeploymentPage'; @@ -30,6 +31,7 @@ function App() { } /> } /> + } /> } /> } /> } /> diff --git a/apps/container-engine-frontend/src/api/api.ts b/apps/container-engine-frontend/src/api/api.ts index 6d642b0..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 @@ -37,7 +40,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/components/DeploymentDetail/LogsPage.tsx b/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx index 68c678b..5b98855 100644 --- a/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx +++ b/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from 'react'; import { ClipboardDocumentListIcon, CubeIcon } from "@heroicons/react/24/outline"; import { useParams } from 'react-router-dom'; import api from '../../api/api'; +import { shouldRetryContainerError, getContainerErrorMessage, analyzeContainerError } from '../../utils/errorHandlers'; export default function LogsPage() { const { deploymentId } = useParams(); @@ -108,24 +109,20 @@ export default function LogsPage() { setError('Authentication failed. Please login again.'); } else if (err?.response?.status === 404) { setError('Deployment not found or no logs available.'); - } else if (err?.response?.status === 400 && err?.response?.data?.message?.includes('ContainerCreating')) { - if (retryCount < 10) { - setError(`Container is starting up... (Retry ${retryCount + 1}/10)`); - setTimeout(() => loadHistoricalLogs(retryCount + 1), 3000); - return; - } else { - setError('Container is taking longer than expected to start. Please refresh manually.'); - } - } else if (err?.response?.status === 400 && err?.response?.data?.message?.includes('waiting to start')) { - if (retryCount < 10) { - setError(`Container is being created... (Retry ${retryCount + 1}/10)`); - setTimeout(() => loadHistoricalLogs(retryCount + 1), 3000); - return; + } else if (shouldRetryContainerError(err, retryCount, 15)) { + const errorMessage = getContainerErrorMessage(err, retryCount, 15); + const analysis = analyzeContainerError(err); + + setError(errorMessage); + setTimeout(() => loadHistoricalLogs(retryCount + 1), analysis.retryDelay || 4000); + return; + } else { + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + setError(getContainerErrorMessage(err, retryCount, 15)); } else { - setError('Container is taking longer than expected to start. Please refresh manually.'); + setError('Failed to load log history'); } - } else { - setError('Failed to load log history'); } } finally { setIsLoadingHistory(false); 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/DeploymentDetailPage.tsx b/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx index d937f4e..bb2a63a 100644 --- a/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx +++ b/apps/container-engine-frontend/src/pages/DeploymentDetailPage.tsx @@ -6,6 +6,7 @@ import DashboardLayout from '../components/Layout/DashboardLayout'; import LogsPage from '../components/DeploymentDetail/LogsPage'; import { useNotifications } from '../context/NotificationContext'; import type { WebSocketMessage } from '../services/websocket'; +import { analyzeContainerError } from '../utils/errorHandlers'; import { RocketLaunchIcon, CubeIcon, @@ -56,12 +57,29 @@ 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; +} + +interface AddDomainResponse { + domain: string; + node_ip: string; + instructions: string; + message: string; +} + const DeploymentDetailPage: React.FC = () => { const { deploymentId } = useParams<{ deploymentId: string }>(); - + const domainComingSoon = false; // Enable Domains tab functionality const navigate = useNavigate(); const [deployment, setDeployment] = useState(null); - const [ logs,setLogs] = useState([]); + const [logs, setLogs] = useState([]); console.log(logs); const [loading, setLoading] = useState(true); @@ -76,8 +94,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(() => { @@ -99,16 +127,18 @@ 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.'); + console.log(err); } finally { setLoading(false); } @@ -122,25 +152,27 @@ 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) || - (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); @@ -161,6 +193,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) { @@ -185,7 +221,12 @@ const DeploymentDetailPage: React.FC = () => { window.location.reload(); }, 1000); } catch (err: any) { - showToast(err.response?.data?.error?.message || 'Failed to scale deployment.', 'error'); + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + showToast('Deployment is starting up. Scaling will be applied once the container is ready.', 'error'); + } else { + showToast(err.response?.data?.error?.message || 'Failed to scale deployment.', 'error'); + } } finally { setIsScaling(false); } @@ -203,10 +244,142 @@ const DeploymentDetailPage: React.FC = () => { navigate('/deployments'); }, 1500); } catch (err: any) { - showToast(err.response?.data?.error?.message || 'Failed to delete deployment.', 'error'); + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + showToast('Cannot delete deployment while it is starting up. Please wait for the deployment to be ready.', 'error'); + } else { + showToast(err.response?.data?.error?.message || 'Failed to delete deployment.', 'error'); + } + } + }; + + 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) { + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + showToast('Deployment is starting up. Restart operation will be queued.', 'error'); + } else { + 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) { + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + showToast('Deployment is starting up. Environment variables will be updated once the container is ready.', 'error'); + } else { + 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) { + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + showToast('Deployment is starting up. Environment variable will be updated once the container is ready.', 'error'); + } else { + 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) { + const analysis = analyzeContainerError(err); + if (analysis.isContainerError) { + showToast('Deployment is starting up. Environment variable will be deleted once the container is ready.', 'error'); + } else { + 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'; @@ -258,10 +431,10 @@ const DeploymentDetailPage: React.FC = () => { if (error) { return ( -
+
-

Error Loading Deployment

+

Error Loading Deployment

{error}

); + 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 [lastAddedDomain, setLastAddedDomain] = useState(null); + const [nodeIp, setNodeIp] = useState(''); + + 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]); + + const fetchNodeIp = useCallback(async () => { + if (!deploymentId) return; + try { + const response = await api.get(`/v1/deployments/${deploymentId}/node-ip`); + setNodeIp(response.data.node_ip || ''); + } catch (err: any) { + console.error('Failed to fetch node IP:', err); + setNodeIp(''); + } + }, [deploymentId]); + + useEffect(() => { + fetchDomains(); + fetchNodeIp(); + + // Poll for domain status updates every 10 seconds + const pollInterval = setInterval(() => { + fetchDomains(); + }, 10000); + + return () => clearInterval(pollInterval); + }, [fetchDomains, fetchNodeIp]); + + const handleAddDomain = async () => { + if (!newDomain.trim() || !deploymentId) return; + + try { + setAddingDomain(true); + const response = await api.post(`/v1/deployments/${deploymentId}/domains`, { + domain: newDomain.trim() + }); + + // Store the response data for showing DNS instructions + const domainData: AddDomainResponse = response.data; + setLastAddedDomain(domainData); + + showToast(`Domain added successfully! Node IP: ${domainData.node_ip}`, '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 domains with automated SSL

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

Add Custom Domain

+
+ setNewDomain(e.target.value)} + className="flex-1 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 mb-2 sm:mb-0" + 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 && ( + + 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 && !lastAddedDomain && ( +
+

DNS Configuration Instructions

+

+ To connect your custom domain, add these DNS records at your domain provider: +

+
+
+
A Record (Root Domain):
+
+
Type: A
+
Name: @ (or leave blank)
+
Value: {nodeIp || 'Loading...'} + {nodeIp && ( + + )} +
+
TTL: 300
+
+
+
+
CNAME Record (WWW Subdomain):
+
+
Type: CNAME
+
Name: www
+
Value: your-domain.com (without www)
+
TTL: 300
+
+
+
+
+

+ ✅ Node IP Available: {nodeIp || 'Loading...'} - Use this IP address for your A record. +

+
+
+ )} + + {/* DNS Instructions - Show after domain is added */} + {lastAddedDomain && ( +
+

🎉 Domain Added Successfully!

+

+ {lastAddedDomain.domain} has been configured. Now add these DNS records to your domain provider: +

+
+
+
A Record (Root Domain):
+
+
Type: A
+
Name: @ (or leave blank)
+
Value: {lastAddedDomain.node_ip} + +
+
TTL: 300
+
+
+
+
CNAME Record (WWW Subdomain):
+
+
Type: CNAME
+
Name: www
+
Value: {lastAddedDomain.domain}
+
TTL: 300
+
+
+
+
+

+ {lastAddedDomain.instructions} +

+
+ +
+ )} +
+
+ ); + }; + return (
-
+
{/* Toast Notification */} {toast.show && (
{ {/* Header Section */}
-
-
+
+
-
-
-

{deployment.app_name}

+
+
+

{deployment.app_name}

{getStatusIcon(deployment.status)} {deployment.status}
- + {deployment.url} @@ -364,15 +897,20 @@ const DeploymentDetailPage: React.FC = () => {
- {/* Quick Stats */} -
+
-
+
@@ -397,7 +935,7 @@ const DeploymentDetailPage: React.FC = () => {
-
+
@@ -409,7 +947,7 @@ const DeploymentDetailPage: React.FC = () => {
-
+
@@ -421,7 +959,7 @@ const DeploymentDetailPage: React.FC = () => {
-
+
@@ -432,8 +970,30 @@ const DeploymentDetailPage: React.FC = () => {
+ {/* Container Status Warning */} + {(deployment.status === 'pending' || deployment.status === 'updating') && ( +
+
+
+ +
+
+

Container Starting Up

+

+ Your deployment is currently initializing. Some operations may not be available until the container is fully ready. This typically takes 30-90 seconds. +

+
+
+
+

+ 💡 Tip: You can still configure settings, but changes will be applied once the container is ready. +

+
+
+ )} + {/* Navigation Tabs */} -
+
{ {activeTab === 'overview' && (
{/* Deployment Information */} -
+
-
+

Deployment Information

-

Core deployment details and metadata

+

Core deployment details

-
- Deployment ID: +
+ Deployment ID:
- - {deployment.id.substring(0, 8)}... + + {deployment.id}
-
- Container Image: +
+ Container Image:
- + {deployment.image}
{/* Scaling Controls */} -
+
-
+

Scaling Control

-

Adjust the number of running instances

+

Adjust running instances

@@ -544,7 +1104,7 @@ const DeploymentDetailPage: React.FC = () => { -
+
{ max="10" value={scaleReplicas} onChange={(e) => setScaleReplicas(Number(e.target.value))} - className="w-24 px-4 py-3 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-center font-mono text-lg" + className="w-full sm:w-24 px-4 py-3 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-center font-mono text-lg" /> + + +
+
+ + {/* Environment Variables */} +
+
+
+
+ +
+
+

Environment Variables

+

Application configuration

+
+
+ +
+ + {/* 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" + /> +

Use uppercase with underscores (e.g., API_KEY)

+
+
+ +