use actix_web::{
HttpRequest, HttpResponse, post,
web::{self, Data, Json},
};
use serde_json::{Value, json};
use crate::AppState;
use crate::api::auth::authorize_static_admin_key;
use crate::api::provision::register_provisioned_client;
use crate::api::response::{
api_success, bad_request, conflict, internal_error, service_unavailable,
};
use crate::provisioning::runtime_config_from_root;
use crate::provisioning::{
NeonConnectionParams, NeonProjectCreateParams, NeonProjectCreateResult,
NeonProviderProvisionRequest, ProvisioningError, RailwayConnectionParams,
RailwayPluginCreateParams, RailwayProjectCreateParams, RailwayProviderProvisionRequest,
RailwayServiceCreateParams, RailwayServiceCreateResult, RenderConnectionParams,
RenderPostgresCreateParams, RenderProviderProvisionRequest, create_neon_project,
create_railway_plugin, create_railway_project, create_railway_service,
create_render_postgres_service, fetch_neon_connection_uri, fetch_railway_connection_uri,
fetch_railway_project_base_environment_id, fetch_render_connection_uri,
json_object_insert_if_missing, run_provision_sql,
};
fn map_provisioning_error(context: &str, err: ProvisioningError) -> HttpResponse {
match err {
ProvisioningError::InvalidInput(message) => bad_request(context, message),
ProvisioningError::Conflict(message) => conflict(context, message),
ProvisioningError::Unavailable(message) => service_unavailable(context, message),
ProvisioningError::Execution(message) => internal_error(context, message),
}
}
fn required_field(name: &str, value: Option<String>) -> Result<String, HttpResponse> {
let value: String = value.unwrap_or_default();
if value.trim().is_empty() {
return Err(bad_request(
"Missing required field",
format!(
"Provide '{}' or set 'connection_uri' to bypass provider API lookup.",
name
),
));
}
Ok(value)
}
#[post("/admin/provision/providers/neon")]
pub async fn admin_provision_neon(
req: HttpRequest,
state: Data<AppState>,
body: Json<NeonProviderProvisionRequest>,
) -> HttpResponse {
if let Err(resp) = authorize_static_admin_key(&req) {
return resp;
}
let defaults = runtime_config_from_root();
let mut project_id: Option<String> = body.project_id.clone();
let mut branch_id: Option<String> = body.branch_id.clone();
if project_id
.as_ref()
.is_none_or(|value| value.trim().is_empty())
&& body.create_project_if_missing
{
let api_key = match required_field("api_key", body.api_key.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let created: NeonProjectCreateResult = match create_neon_project(NeonProjectCreateParams {
api_key,
project_name: body
.project_name
.clone()
.or_else(|| Some(body.client_name.clone())),
project_payload: body.project_create_payload.clone(),
api_base_url: body
.api_base_url
.clone()
.or_else(|| defaults.neon_api_base_url.clone()),
})
.await
{
Ok(value) => value,
Err(err) => return map_provisioning_error("Failed to create Neon project", err),
};
project_id = Some(created.project_id);
if branch_id.is_none() {
branch_id = created.branch_id;
}
}
let pg_uri: String = if let Some(uri) = body
.connection_uri
.as_ref()
.filter(|value| !value.trim().is_empty())
{
uri.to_string()
} else {
let api_key: String = match required_field("api_key", body.api_key.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let project_id: String = match required_field("project_id", project_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
match fetch_neon_connection_uri(NeonConnectionParams {
api_key,
project_id,
branch_id: branch_id.clone(),
database_name: body.database_name.clone(),
role_name: body.role_name.clone(),
endpoint_id: body.endpoint_id.clone(),
api_base_url: body
.api_base_url
.clone()
.or_else(|| defaults.neon_api_base_url.clone()),
})
.await
{
Ok(uri) => uri,
Err(err) => return map_provisioning_error("Failed to fetch Neon connection URI", err),
}
};
let statements_executed = if body.provision_schema {
Some(match run_provision_sql(&pg_uri).await {
Ok(total) => total,
Err(err) => return map_provisioning_error("Failed to provision Neon database", err),
})
} else {
None
};
let description: Option<String> = body
.description
.clone()
.or_else(|| Some("Neon database managed via Athena".to_string()));
let (runtime_registered, catalog_registered) = match register_provisioned_client(
state.get_ref(),
&body.client_name,
description,
&pg_uri,
None,
json!({
"managed_by": "provision_api",
"provider": "neon",
"project_id": project_id,
"branch_id": branch_id,
"database_name": body.database_name,
"role_name": body.role_name,
"endpoint_id": body.endpoint_id,
"created_project": body.create_project_if_missing,
}),
body.register_runtime,
body.register_catalog,
)
.await
{
Ok(value) => value,
Err(resp) => return resp,
};
api_success(
"Provisioned Neon database",
json!({
"provider": "neon",
"client_name": body.client_name,
"pg_uri": pg_uri,
"pipeline": {
"provision_schema": body.provision_schema,
"statements_executed": statements_executed,
"register_runtime": runtime_registered,
"register_catalog": catalog_registered,
}
}),
)
}
#[post("/admin/provision/providers/railway")]
pub async fn admin_provision_railway(
req: HttpRequest,
state: Data<AppState>,
body: Json<RailwayProviderProvisionRequest>,
) -> HttpResponse {
if let Err(resp) = authorize_static_admin_key(&req) {
return resp;
}
let defaults = runtime_config_from_root();
let api_key_for_create = body
.api_key
.clone()
.filter(|value| !value.trim().is_empty());
let mut project_id = body.project_id.clone();
let mut environment_id = body.environment_id.clone();
let mut service_id = body.service_id.clone();
let mut plugin_id = body.plugin_id.clone();
if project_id
.as_ref()
.is_none_or(|value| value.trim().is_empty())
&& body.create_project_if_missing
{
let api_key = match required_field("api_key", api_key_for_create.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let project_input = match json_object_insert_if_missing(
body.project_create_input.clone(),
"name",
Value::String(
body.project_name
.clone()
.unwrap_or_else(|| format!("athena-{}", body.client_name)),
),
) {
Ok(value) => value,
Err(err) => return map_provisioning_error("Invalid Railway project_create_input", err),
};
let created = match create_railway_project(RailwayProjectCreateParams {
api_key,
project_input,
graphql_url: body
.graphql_url
.clone()
.or_else(|| defaults.railway_graphql_url.clone()),
})
.await
{
Ok(value) => value,
Err(err) => return map_provisioning_error("Failed to create Railway project", err),
};
project_id = Some(created.project_id);
if environment_id.is_none() {
environment_id = created.base_environment_id;
}
}
if environment_id
.as_ref()
.is_none_or(|value| value.trim().is_empty())
&& let (Some(api_key), Some(project)) = (api_key_for_create.clone(), project_id.clone())
{
environment_id = match fetch_railway_project_base_environment_id(
&api_key,
&project,
body.graphql_url
.as_deref()
.or(defaults.railway_graphql_url.as_deref()),
)
.await
{
Ok(value) => value,
Err(err) => {
return map_provisioning_error("Failed to resolve Railway base environment", err);
}
};
}
if service_id
.as_ref()
.is_none_or(|value| value.trim().is_empty())
&& body.create_service_if_missing
{
let api_key: String = match required_field("api_key", api_key_for_create.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let project: String = match required_field("project_id", project_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let service_input: Value = match json_object_insert_if_missing(
body.service_create_input.clone(),
"projectId",
Value::String(project),
) {
Ok(value) => value,
Err(err) => return map_provisioning_error("Invalid Railway service_create_input", err),
};
let service_input = match json_object_insert_if_missing(
Some(service_input),
"name",
Value::String(
body.service_name
.clone()
.unwrap_or_else(|| format!("{}-service", body.client_name)),
),
) {
Ok(value) => value,
Err(err) => return map_provisioning_error("Invalid Railway service_create_input", err),
};
let created: RailwayServiceCreateResult =
match create_railway_service(RailwayServiceCreateParams {
api_key,
service_input,
graphql_url: body
.graphql_url
.clone()
.or_else(|| defaults.railway_graphql_url.clone()),
})
.await
{
Ok(value) => value,
Err(err) => return map_provisioning_error("Failed to create Railway service", err),
};
service_id = Some(created.service_id);
}
if plugin_id
.as_ref()
.is_none_or(|value| value.trim().is_empty())
&& body.create_plugin_if_missing
{
let api_key = match required_field("api_key", api_key_for_create.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let project = match required_field("project_id", project_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let environment = match required_field("environment_id", environment_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let plugin_input = match json_object_insert_if_missing(
body.plugin_create_input.clone(),
"projectId",
Value::String(project),
) {
Ok(value) => value,
Err(err) => return map_provisioning_error("Invalid Railway plugin_create_input", err),
};
let plugin_input = match json_object_insert_if_missing(
Some(plugin_input),
"environmentId",
Value::String(environment),
) {
Ok(value) => value,
Err(err) => return map_provisioning_error("Invalid Railway plugin_create_input", err),
};
let plugin_input = match json_object_insert_if_missing(
Some(plugin_input),
"name",
Value::String(
body.plugin_name
.clone()
.unwrap_or_else(|| "Postgres".to_string()),
),
) {
Ok(value) => value,
Err(err) => return map_provisioning_error("Invalid Railway plugin_create_input", err),
};
let created = match create_railway_plugin(RailwayPluginCreateParams {
api_key,
plugin_input,
graphql_url: body
.graphql_url
.clone()
.or_else(|| defaults.railway_graphql_url.clone()),
})
.await
{
Ok(value) => value,
Err(err) => return map_provisioning_error("Failed to create Railway plugin", err),
};
plugin_id = Some(created.plugin_id);
}
let pg_uri = if let Some(uri) = body
.connection_uri
.as_ref()
.filter(|value| !value.trim().is_empty())
{
uri.to_string()
} else {
let api_key = match required_field("api_key", api_key_for_create.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let project_id = match required_field("project_id", project_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let environment_id = match required_field("environment_id", environment_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
match fetch_railway_connection_uri(RailwayConnectionParams {
api_key,
project_id,
environment_id,
service_id: service_id.clone(),
plugin_id: plugin_id.clone(),
graphql_url: body
.graphql_url
.clone()
.or_else(|| defaults.railway_graphql_url.clone()),
})
.await
{
Ok(uri) => uri,
Err(err) => {
return map_provisioning_error("Failed to fetch Railway connection URI", err);
}
}
};
let statements_executed = if body.provision_schema {
Some(match run_provision_sql(&pg_uri).await {
Ok(total) => total,
Err(err) => return map_provisioning_error("Failed to provision Railway database", err),
})
} else {
None
};
let description = body
.description
.clone()
.or_else(|| Some("Railway database managed via Athena".to_string()));
let (runtime_registered, catalog_registered) = match register_provisioned_client(
state.get_ref(),
&body.client_name,
description,
&pg_uri,
None,
json!({
"managed_by": "provision_api",
"provider": "railway",
"project_id": project_id,
"environment_id": environment_id,
"service_id": service_id,
"plugin_id": plugin_id,
"created_project": body.create_project_if_missing,
"created_service": body.create_service_if_missing,
"created_plugin": body.create_plugin_if_missing,
}),
body.register_runtime,
body.register_catalog,
)
.await
{
Ok(value) => value,
Err(resp) => return resp,
};
api_success(
"Provisioned Railway database",
json!({
"provider": "railway",
"client_name": body.client_name,
"pg_uri": pg_uri,
"pipeline": {
"provision_schema": body.provision_schema,
"statements_executed": statements_executed,
"register_runtime": runtime_registered,
"register_catalog": catalog_registered,
}
}),
)
}
#[post("/admin/provision/providers/render")]
pub async fn admin_provision_render(
req: HttpRequest,
state: Data<AppState>,
body: Json<RenderProviderProvisionRequest>,
) -> HttpResponse {
if let Err(resp) = authorize_static_admin_key(&req) {
return resp;
}
let defaults = runtime_config_from_root();
let api_key_for_create = body
.api_key
.clone()
.filter(|value| !value.trim().is_empty());
let mut service_id = body.service_id.clone();
if service_id
.as_ref()
.is_none_or(|value| value.trim().is_empty())
&& body.create_service_if_missing
{
let api_key = match required_field("api_key", api_key_for_create.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let created = match create_render_postgres_service(RenderPostgresCreateParams {
api_key,
owner_id: body.owner_id.clone(),
service_name: body.service_name.clone(),
service_payload: body.service_create_payload.clone(),
plan: body.plan.clone(),
region: body.region.clone(),
postgres_version: body.postgres_version.clone(),
disk_size_gb: body.disk_size_gb,
api_base_url: body
.api_base_url
.clone()
.or_else(|| defaults.render_api_base_url.clone()),
})
.await
{
Ok(value) => value,
Err(err) => return map_provisioning_error("Failed to create Render service", err),
};
service_id = Some(created.service_id);
}
let pg_uri = if let Some(uri) = body
.connection_uri
.as_ref()
.filter(|value| !value.trim().is_empty())
{
uri.to_string()
} else {
let api_key = match required_field("api_key", api_key_for_create.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
let service_id = match required_field("service_id", service_id.clone()) {
Ok(value) => value,
Err(resp) => return resp,
};
match fetch_render_connection_uri(RenderConnectionParams {
api_key,
service_id,
api_base_url: body
.api_base_url
.clone()
.or_else(|| defaults.render_api_base_url.clone()),
})
.await
{
Ok(uri) => uri,
Err(err) => {
return map_provisioning_error("Failed to fetch Render connection URI", err);
}
}
};
let statements_executed = if body.provision_schema {
Some(match run_provision_sql(&pg_uri).await {
Ok(total) => total,
Err(err) => return map_provisioning_error("Failed to provision Render database", err),
})
} else {
None
};
let description = body
.description
.clone()
.or_else(|| Some("Render Postgres database managed via Athena".to_string()));
let (runtime_registered, catalog_registered) = match register_provisioned_client(
state.get_ref(),
&body.client_name,
description,
&pg_uri,
None,
json!({
"managed_by": "provision_api",
"provider": "render",
"service_id": service_id,
"owner_id": body.owner_id,
"service_name": body.service_name,
"plan": body.plan,
"region": body.region,
"postgres_version": body.postgres_version,
"disk_size_gb": body.disk_size_gb,
"created_service": body.create_service_if_missing,
}),
body.register_runtime,
body.register_catalog,
)
.await
{
Ok(value) => value,
Err(resp) => return resp,
};
api_success(
"Provisioned Render database",
json!({
"provider": "render",
"client_name": body.client_name,
"pg_uri": pg_uri,
"pipeline": {
"provision_schema": body.provision_schema,
"statements_executed": statements_executed,
"register_runtime": runtime_registered,
"register_catalog": catalog_registered,
}
}),
)
}
pub(super) fn register(cfg: &mut web::ServiceConfig) {
cfg.service(admin_provision_neon)
.service(admin_provision_railway)
.service(admin_provision_render);
}