From 4c78a4e9b39656e9bac5ce71c72170e61683fd5e Mon Sep 17 00:00:00 2001 From: meskill <8974488+meskill@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:06:20 +0000 Subject: [PATCH] feat: expose config through admin api --- generated/.tailcallrc.graphql | 11 ++++ generated/.tailcallrc.schema.json | 25 +++++++++ src/cli/server/admin_server/graphql.rs | 11 ++++ src/cli/server/admin_server/mod.rs | 4 ++ src/cli/server/admin_server/server.rs | 75 ++++++++++++++++++++++++++ src/cli/server/http_server.rs | 9 +++- src/cli/server/mod.rs | 1 + src/core/config/server.rs | 12 +++++ src/core/primitive.rs | 3 +- 9 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/cli/server/admin_server/graphql.rs create mode 100644 src/cli/server/admin_server/mod.rs create mode 100644 src/cli/server/admin_server/server.rs diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index 9efc3d86a1..a3453dfc04 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -243,6 +243,10 @@ set of server configurations. It dictates how the server behaves and helps tune for various use-cases. """ directive @server( + """ + Control private admin API settings + """ + admin: Admin """ `apolloTracing` exposes GraphQL query performance data, including execution time of queries and individual resolvers. @@ -576,6 +580,13 @@ input Schema { Enum: [String!] } +input Admin { + """ + TCP port on which the admin api will be available + """ + port: Int! +} + """ Type to configure Cross-Origin Resource Sharing (CORS) for a server. """ diff --git a/generated/.tailcallrc.schema.json b/generated/.tailcallrc.schema.json index 6f20d154bf..e0ebdb4cd9 100644 --- a/generated/.tailcallrc.schema.json +++ b/generated/.tailcallrc.schema.json @@ -93,6 +93,20 @@ }, "additionalProperties": false }, + "Admin": { + "type": "object", + "required": [ + "port" + ], + "properties": { + "port": { + "description": "TCP port on which the admin api will be available", + "type": "integer", + "format": "uint16", + "minimum": 1.0 + } + } + }, "Alias": { "description": "The @alias directive indicates that aliases of one enum value.", "type": "object", @@ -1004,6 +1018,17 @@ "description": "The `@server` directive, when applied at the schema level, offers a comprehensive set of server configurations. It dictates how the server behaves and helps tune tailcall for various use-cases.", "type": "object", "properties": { + "admin": { + "description": "Control private admin API settings", + "anyOf": [ + { + "$ref": "#/definitions/Admin" + }, + { + "type": "null" + } + ] + }, "apolloTracing": { "description": "`apolloTracing` exposes GraphQL query performance data, including execution time of queries and individual resolvers.", "type": [ diff --git a/src/cli/server/admin_server/graphql.rs b/src/cli/server/admin_server/graphql.rs new file mode 100644 index 0000000000..19bfe7317b --- /dev/null +++ b/src/cli/server/admin_server/graphql.rs @@ -0,0 +1,11 @@ +use async_graphql::SimpleObject; + +#[derive(SimpleObject)] +pub struct Config { + pub sdl: String, +} + +#[derive(SimpleObject)] +pub struct Query { + pub config: Config, +} diff --git a/src/cli/server/admin_server/mod.rs b/src/cli/server/admin_server/mod.rs new file mode 100644 index 0000000000..0af655fc7e --- /dev/null +++ b/src/cli/server/admin_server/mod.rs @@ -0,0 +1,4 @@ +mod graphql; +mod server; + +pub use server::AdminServer; diff --git a/src/cli/server/admin_server/server.rs b/src/cli/server/admin_server/server.rs new file mode 100644 index 0000000000..4729e55ec0 --- /dev/null +++ b/src/cli/server/admin_server/server.rs @@ -0,0 +1,75 @@ +use std::net::SocketAddr; + +use anyhow::Result; +use async_graphql::{EmptyMutation, EmptySubscription, Request, Schema}; +use http::{Method, Response, StatusCode}; +use hyper::service::{make_service_fn, service_fn}; +use hyper::Server; + +use super::graphql::{Config, Query}; +use crate::core::async_graphql_hyper::GraphQLResponse; +use crate::core::blueprint::Blueprint; +use crate::core::config::ConfigModule; +use crate::core::Errata; + +#[derive(Debug)] +pub struct AdminServer { + addr: SocketAddr, + sdl: String, +} + +impl AdminServer { + pub fn new(config_module: &ConfigModule) -> Result> { + if let Some(admin) = config_module.server.admin.as_ref() { + let blueprint = Blueprint::try_from(config_module).map_err(Errata::from)?; + let sdl = crate::core::document::print(config_module.config().into()); + let addr = (blueprint.server.hostname, admin.port.get()).into(); + + Ok(Some(Self { addr, sdl })) + } else { + Ok(None) + } + } + + pub async fn start(self) -> Result<()> { + let server = Server::try_bind(&self.addr)?; + let config = Config { sdl: self.sdl }; + let query = Query { config }; + let schema = Schema::new(query, EmptyMutation, EmptySubscription); + + server + .serve(make_service_fn(|_| { + let schema = schema.clone(); + async move { + Result::<_>::Ok(service_fn(move |req| { + let (parts, body) = req.into_parts(); + let schema = schema.clone(); + + async move { + match parts.method { + Method::POST if parts.uri.path() == "/graphql" => { + let body = hyper::body::to_bytes(body).await?; + let request: Request = serde_json::from_slice(&body)?; + + let res = schema.execute(request).await; + let res = GraphQLResponse::from(res); + + Result::<_>::Ok(res.into_response()?) + } + _ => { + let mut response = Response::default(); + + *response.status_mut() = StatusCode::NOT_FOUND; + + Result::<_>::Ok(response) + } + } + } + })) + } + })) + .await?; + + Ok(()) + } +} diff --git a/src/cli/server/http_server.rs b/src/cli/server/http_server.rs index 3661c9f5f7..394af04390 100644 --- a/src/cli/server/http_server.rs +++ b/src/cli/server/http_server.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use anyhow::Result; use tokio::sync::oneshot::{self}; +use super::admin_server::AdminServer; use super::http_1::start_http_1; use super::http_2::start_http_2; use super::server_config::ServerConfig; @@ -53,7 +54,13 @@ impl Server { .enable_all() .build()?; - let result = runtime.spawn(async { self.start().await }).await?; + let admin_api = AdminServer::new(&self.config_module)?; + + if let Some(admin_api) = admin_api { + runtime.spawn(admin_api.start()); + } + + let result = runtime.spawn(self.start()).await?; runtime.shutdown_background(); result diff --git a/src/cli/server/mod.rs b/src/cli/server/mod.rs index 3cd410e27b..1fca4b7435 100644 --- a/src/cli/server/mod.rs +++ b/src/cli/server/mod.rs @@ -1,3 +1,4 @@ +mod admin_server; pub mod http_1; pub mod http_2; pub mod http_server; diff --git a/src/core/config/server.rs b/src/core/config/server.rs index 25c306393a..4f8a6cce86 100644 --- a/src/core/config/server.rs +++ b/src/core/config/server.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::num::NonZeroU16; use derive_getters::Getters; use schemars::JsonSchema; @@ -130,6 +131,10 @@ pub struct Server { /// - graphQL: "/graphql" If not specified, these default values will be /// used. pub routes: Option, + + /// Control private admin API settings + #[serde(default, skip_serializing_if = "is_default")] + pub admin: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, MergeRight, JsonSchema, Getters)] @@ -184,6 +189,13 @@ pub enum HttpVersion { HTTP2, } +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, schemars::JsonSchema, MergeRight)] +#[serde(rename_all = "camelCase")] +pub struct Admin { + /// TCP port on which the admin api will be available + pub port: NonZeroU16, +} + impl Server { pub fn enable_apollo_tracing(&self) -> bool { self.apollo_tracing.unwrap_or(false) diff --git a/src/core/primitive.rs b/src/core/primitive.rs index f606689674..dfc8626496 100644 --- a/src/core/primitive.rs +++ b/src/core/primitive.rs @@ -1,5 +1,5 @@ use std::marker::PhantomData; -use std::num::NonZeroU64; +use std::num::{NonZeroU16, NonZeroU64}; use crate::core::merge_right::MergeRight; @@ -16,6 +16,7 @@ impl Primitive for i8 {} impl Primitive for NonZeroU64 {} impl Primitive for String {} impl Primitive for u16 {} +impl Primitive for NonZeroU16 {} impl Primitive for u32 {} impl Primitive for u64 {} impl Primitive for u8 {}