environment_variables.rs•8.07 kB
use application::EnvVarChange;
use axum::{
extract::{
FromRef,
State,
},
response::IntoResponse,
};
use common::http::{
extract::Json,
HttpResponseError,
};
use http::StatusCode;
use model::environment_variables::{
types::{
EnvVarName,
EnvVarValue,
EnvironmentVariable,
},
EnvironmentVariablesModel,
};
use serde::{
Deserialize,
Serialize,
};
use utoipa::ToSchema;
use utoipa_axum::router::OpenApiRouter;
use crate::{
admin::{
must_be_admin,
must_be_admin_with_write_access,
},
authentication::ExtractIdentity,
LocalAppState,
};
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateEnvVarRequest {
name: String,
value: Option<String>, // None → delete existing
}
impl UpdateEnvVarRequest {
pub async fn into_env_var_changes(self) -> anyhow::Result<Vec<EnvVarChange>> {
match self {
UpdateEnvVarRequest {
name,
value: Some(value),
} => {
let env_var = validate_env_var(&name, &value)?;
Ok(vec![EnvVarChange::Set(env_var)])
},
UpdateEnvVarRequest { name, value: None } => {
let name = name.parse()?;
Ok(vec![EnvVarChange::Unset(name)])
},
}
}
}
#[derive(Deserialize, ToSchema)]
pub struct UpdateEnvVarsRequest {
changes: Vec<UpdateEnvVarRequest>,
}
/// Update environment variables
///
/// Update one or many environment variables in a deployment.
/// This will invalidate all subscriptions, since environment variables
/// are accessible in queries but are not part of the cache key of a query
/// result.
#[utoipa::path(
post,
path = "/update_environment_variables",
request_body = UpdateEnvVarsRequest,
responses((status = 200)),
)]
pub async fn update_environment_variables(
State(st): State<LocalAppState>,
ExtractIdentity(identity): ExtractIdentity,
Json(UpdateEnvVarsRequest { changes }): Json<UpdateEnvVarsRequest>,
) -> Result<impl IntoResponse, HttpResponseError> {
must_be_admin_with_write_access(&identity)?;
let mut env_var_changes = vec![];
for change in changes {
env_var_changes.extend(change.into_env_var_changes().await?);
}
env_var_changes.sort();
let mut tx = st.application.begin(identity).await?;
let audit_events = st
.application
.update_environment_variables(&mut tx, env_var_changes)
.await?;
st.application
.commit_with_audit_log_events(tx, audit_events, "update_env_vars")
.await?;
Ok(StatusCode::OK)
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ListEnvVarsResponse {
environment_variables: std::collections::BTreeMap<String, String>,
}
/// List environment variables
///
/// Get all environment variables in a deployment.
/// In the future this might not include "secret" environment
/// variables.
#[utoipa::path(
get,
path = "/list_environment_variables",
responses(
(status = 200, body = ListEnvVarsResponse)
),
)]
pub async fn list_environment_variables(
State(st): State<LocalAppState>,
ExtractIdentity(identity): ExtractIdentity,
) -> Result<impl IntoResponse, HttpResponseError> {
must_be_admin(&identity)?;
let mut tx = st.application.begin(identity).await?;
let env_vars = EnvironmentVariablesModel::new(&mut tx).get_all().await?;
let environment_variables = env_vars
.into_iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect();
Ok(Json(ListEnvVarsResponse {
environment_variables,
}))
}
fn validate_env_var(name: &String, value: &String) -> anyhow::Result<EnvironmentVariable> {
let name: EnvVarName = name.parse()?;
let value: EnvVarValue = value.parse()?;
Ok(EnvironmentVariable::new(name, value))
}
pub fn platform_router<S>() -> OpenApiRouter<S>
where
LocalAppState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
OpenApiRouter::new().routes(utoipa_axum::routes!(
update_environment_variables,
list_environment_variables
))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use axum_extra::headers::authorization::Credentials;
use common::types::{
EnvVarName,
EnvVarValue,
};
use http::Request;
use keybroker::Identity;
use maplit::btreemap;
use model::environment_variables::EnvironmentVariablesModel;
use runtime::prod::ProdRuntime;
use serde_json::json;
use crate::test_helpers::{
setup_backend_for_test,
TestLocalBackend,
};
async fn update_environment_variables(
backend: &TestLocalBackend,
changes: serde_json::Value,
) -> anyhow::Result<()> {
let json_body = json!({"changes": changes});
let body = axum::body::Body::from(serde_json::to_vec(&json_body)?);
let req = Request::builder()
.uri("/api/update_environment_variables")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", backend.admin_auth_header.0.encode())
.body(body)?;
let () = backend.expect_success(req).await?;
Ok(())
}
async fn list_environment_variables(
backend: &TestLocalBackend,
) -> anyhow::Result<BTreeMap<EnvVarName, EnvVarValue>> {
let mut tx = backend.st.application.begin(Identity::system()).await?;
let envs = EnvironmentVariablesModel::new(&mut tx).get_all().await?;
Ok(envs)
}
#[convex_macro::prod_rt_test]
async fn test_create_env_vars(rt: ProdRuntime) -> anyhow::Result<()> {
let backend = setup_backend_for_test(rt).await?;
update_environment_variables(
&backend,
json!([
{"name": "name1", "value": "value1"},
{"name": "name2", "value": "value2"},
]),
)
.await?;
assert_eq!(
list_environment_variables(&backend).await?,
btreemap! {
"name1".parse()? => "value1".parse()?,
"name2".parse()? => "value2".parse()?,
}
);
Ok(())
}
#[convex_macro::prod_rt_test]
async fn test_update_env_vars(rt: ProdRuntime) -> anyhow::Result<()> {
let backend = setup_backend_for_test(rt).await?;
update_environment_variables(
&backend,
json!([
{"name": "name1", "value": "value1"},
{"name": "name2", "value": "value2"},
]),
)
.await?;
update_environment_variables(
&backend,
json!([
{"name": "name2", "value": "value2b"},
{"name": "name3", "value": "value3"},
]),
)
.await?;
assert_eq!(
list_environment_variables(&backend).await?,
btreemap! {
"name1".parse()? => "value1".parse()?,
"name2".parse()? => "value2b".parse()?,
"name3".parse()? => "value3".parse()?,
}
);
Ok(())
}
#[convex_macro::prod_rt_test]
async fn test_delete_env_vars(rt: ProdRuntime) -> anyhow::Result<()> {
let backend = setup_backend_for_test(rt).await?;
update_environment_variables(
&backend,
json!([
{"name": "name1", "value": "value1"},
{"name": "name2", "value": "value2"},
]),
)
.await?;
update_environment_variables(
&backend,
json!([
{"name": "name2"},
{"name": "name3"},
]),
)
.await?;
assert_eq!(
list_environment_variables(&backend).await?,
btreemap! {
"name1".parse()? => "value1".parse()?,
}
);
Ok(())
}
}