-
Notifications
You must be signed in to change notification settings - Fork 625
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: endpoints for config management via goose-server (#1207)
Co-authored-by: Lily Delalande <ldelalande@squareup.com>
- Loading branch information
1 parent
e461e68
commit 9287eae
Showing
14 changed files
with
569 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// We'll generate the schema at runtime since we need access to the complete application context | ||
fn main() { | ||
println!("cargo:rerun-if-changed=src/"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
use goose_server::openapi; | ||
use std::env; | ||
use std::fs; | ||
|
||
fn main() { | ||
let schema = openapi::generate_schema(); | ||
|
||
// Get the current working directory | ||
let current_dir = env::current_dir().unwrap(); | ||
let output_path = current_dir.join("ui").join("desktop").join("openapi.json"); | ||
|
||
// Ensure parent directory exists | ||
if let Some(parent) = output_path.parent() { | ||
fs::create_dir_all(parent).unwrap(); | ||
} | ||
|
||
fs::write(&output_path, schema).unwrap(); | ||
println!( | ||
"Successfully generated OpenAPI schema at {}", | ||
output_path.display() | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
pub mod openapi; | ||
pub mod routes; | ||
pub mod state; | ||
|
||
// Re-export commonly used items | ||
pub use openapi::*; | ||
pub use state::*; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ mod commands; | |
mod configuration; | ||
mod error; | ||
mod logging; | ||
mod openapi; | ||
mod routes; | ||
mod state; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
use utoipa::OpenApi; | ||
|
||
#[allow(dead_code)] // Used by utoipa for OpenAPI generation | ||
#[derive(OpenApi)] | ||
#[openapi( | ||
paths( | ||
super::routes::config_management::upsert_config, | ||
super::routes::config_management::remove_config, | ||
super::routes::config_management::read_config, | ||
super::routes::config_management::add_extension, | ||
super::routes::config_management::remove_extension, | ||
super::routes::config_management::read_all_config | ||
), | ||
components(schemas( | ||
super::routes::config_management::UpsertConfigQuery, | ||
super::routes::config_management::ConfigKeyQuery, | ||
super::routes::config_management::ExtensionQuery, | ||
super::routes::config_management::ConfigResponse | ||
)) | ||
)] | ||
pub struct ApiDoc; | ||
|
||
#[allow(dead_code)] // Used by generate_schema binary | ||
pub fn generate_schema() -> String { | ||
let api_doc = ApiDoc::openapi(); | ||
serde_json::to_string_pretty(&api_doc).unwrap() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
use axum::{ | ||
extract::State, | ||
routing::{delete, get, post}, | ||
Json, Router, | ||
}; | ||
use goose::config::Config; | ||
use http::StatusCode; | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json::Value; | ||
use std::{collections::HashMap, sync::Arc}; | ||
use tokio::sync::Mutex; | ||
use utoipa::ToSchema; | ||
|
||
use crate::state::AppState; | ||
|
||
#[derive(Deserialize, ToSchema)] | ||
pub struct UpsertConfigQuery { | ||
pub key: String, | ||
pub value: Value, | ||
pub is_secret: Option<bool>, | ||
} | ||
|
||
#[derive(Deserialize, ToSchema)] | ||
pub struct ConfigKeyQuery { | ||
pub key: String, | ||
} | ||
|
||
#[derive(Deserialize, ToSchema)] | ||
pub struct ExtensionQuery { | ||
pub name: String, | ||
pub config: Value, | ||
} | ||
|
||
#[derive(Serialize, ToSchema)] | ||
pub struct ConfigResponse { | ||
pub config: HashMap<String, Value>, | ||
} | ||
|
||
#[utoipa::path( | ||
post, | ||
path = "/config/upsert", | ||
request_body = UpsertConfigQuery, | ||
responses( | ||
(status = 200, description = "Configuration value upserted successfully", body = String), | ||
(status = 500, description = "Internal server error") | ||
) | ||
)] | ||
pub async fn upsert_config( | ||
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>, | ||
Json(query): Json<UpsertConfigQuery>, | ||
) -> Result<Json<Value>, StatusCode> { | ||
let config = Config::global(); | ||
|
||
let result = if query.is_secret.unwrap_or(false) { | ||
config.set_secret(&query.key, query.value) | ||
} else { | ||
config.set(&query.key, query.value) | ||
}; | ||
|
||
match result { | ||
Ok(_) => Ok(Json(Value::String(format!("Upserted key {}", query.key)))), | ||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), | ||
} | ||
} | ||
|
||
#[utoipa::path( | ||
post, | ||
path = "/config/remove", | ||
request_body = ConfigKeyQuery, | ||
responses( | ||
(status = 200, description = "Configuration value removed successfully", body = String), | ||
(status = 404, description = "Configuration key not found"), | ||
(status = 500, description = "Internal server error") | ||
) | ||
)] | ||
pub async fn remove_config( | ||
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>, | ||
Json(query): Json<ConfigKeyQuery>, | ||
) -> Result<Json<String>, StatusCode> { | ||
let config = Config::global(); | ||
|
||
match config.delete(&query.key) { | ||
Ok(_) => Ok(Json(format!("Removed key {}", query.key))), | ||
Err(_) => Err(StatusCode::NOT_FOUND), | ||
} | ||
} | ||
|
||
#[utoipa::path( | ||
get, | ||
path = "/config/read", | ||
request_body = ConfigKeyQuery, | ||
responses( | ||
(status = 200, description = "Configuration value retrieved successfully", body = Value), | ||
(status = 404, description = "Configuration key not found") | ||
) | ||
)] | ||
pub async fn read_config( | ||
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>, | ||
Json(query): Json<ConfigKeyQuery>, | ||
) -> Result<Json<Value>, StatusCode> { | ||
let config = Config::global(); | ||
|
||
match config.get::<Value>(&query.key) { | ||
Ok(value) => Ok(Json(value)), | ||
Err(_) => Err(StatusCode::NOT_FOUND), | ||
} | ||
} | ||
|
||
#[utoipa::path( | ||
post, | ||
path = "/config/extension", | ||
request_body = ExtensionQuery, | ||
responses( | ||
(status = 200, description = "Extension added successfully", body = String), | ||
(status = 400, description = "Invalid request"), | ||
(status = 500, description = "Internal server error") | ||
) | ||
)] | ||
pub async fn add_extension( | ||
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>, | ||
Json(extension): Json<ExtensionQuery>, | ||
) -> Result<Json<String>, StatusCode> { | ||
let config = Config::global(); | ||
|
||
// Get current extensions or initialize empty map | ||
let mut extensions: HashMap<String, Value> = | ||
config.get("extensions").unwrap_or_else(|_| HashMap::new()); | ||
|
||
// Add new extension | ||
extensions.insert(extension.name.clone(), extension.config); | ||
|
||
// Save updated extensions | ||
match config.set( | ||
"extensions", | ||
Value::Object(extensions.into_iter().collect()), | ||
) { | ||
Ok(_) => Ok(Json(format!("Added extension {}", extension.name))), | ||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), | ||
} | ||
} | ||
|
||
#[utoipa::path( | ||
delete, | ||
path = "/config/extension", | ||
request_body = ConfigKeyQuery, | ||
responses( | ||
(status = 200, description = "Extension removed successfully", body = String), | ||
(status = 404, description = "Extension not found"), | ||
(status = 500, description = "Internal server error") | ||
) | ||
)] | ||
pub async fn remove_extension( | ||
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>, | ||
Json(query): Json<ConfigKeyQuery>, | ||
) -> Result<Json<String>, StatusCode> { | ||
let config = Config::global(); | ||
|
||
// Get current extensions | ||
let mut extensions: HashMap<String, Value> = match config.get("extensions") { | ||
Ok(exts) => exts, | ||
Err(_) => return Err(StatusCode::NOT_FOUND), | ||
}; | ||
|
||
// Remove extension if it exists | ||
if extensions.remove(&query.key).is_some() { | ||
// Save updated extensions | ||
match config.set( | ||
"extensions", | ||
Value::Object(extensions.into_iter().collect()), | ||
) { | ||
Ok(_) => Ok(Json(format!("Removed extension {}", query.key))), | ||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), | ||
} | ||
} else { | ||
Err(StatusCode::NOT_FOUND) | ||
} | ||
} | ||
|
||
#[utoipa::path( | ||
get, | ||
path = "/config", | ||
responses( | ||
(status = 200, description = "All configuration values retrieved successfully", body = ConfigResponse) | ||
) | ||
)] | ||
pub async fn read_all_config( | ||
State(_state): State<Arc<Mutex<HashMap<String, Value>>>>, | ||
) -> Result<Json<ConfigResponse>, StatusCode> { | ||
let config = Config::global(); | ||
|
||
// Load values from config file | ||
let values = config.load_values().unwrap_or_default(); | ||
|
||
Ok(Json(ConfigResponse { config: values })) | ||
} | ||
|
||
pub fn routes(state: AppState) -> Router { | ||
Router::new() | ||
.route("/config", get(read_all_config)) | ||
.route("/config/upsert", post(upsert_config)) | ||
.route("/config/remove", post(remove_config)) | ||
.route("/config/read", post(read_config)) | ||
.route("/config/extension", post(add_extension)) | ||
.route("/config/extension", delete(remove_extension)) | ||
.with_state(state.config) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.