diff options
author | Alex Auvolat <alex@adnab.me> | 2022-05-05 10:29:45 +0200 |
---|---|---|
committer | Alex Auvolat <alex@adnab.me> | 2022-05-10 13:25:06 +0200 |
commit | 633958c7b1ce9c83df5159051fd299b484d0d797 (patch) | |
tree | 906246beab76ee6981af03fb31ce04421cd6b6ab /src/api | |
parent | 5768bf362262f78376af14517c4921941986192e (diff) | |
download | garage-633958c7b1ce9c83df5159051fd299b484d0d797.tar.gz garage-633958c7b1ce9c83df5159051fd299b484d0d797.zip |
Refactor admin API to be in api/admin and use common code
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/Cargo.toml | 3 | ||||
-rw-r--r-- | src/api/admin/api_server.rs | 128 | ||||
-rw-r--r-- | src/api/admin/mod.rs | 2 | ||||
-rw-r--r-- | src/api/admin/router.rs | 59 | ||||
-rw-r--r-- | src/api/lib.rs | 1 |
5 files changed, 193 insertions, 0 deletions
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 29b26e5e..db77cf38 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -54,6 +54,9 @@ quick-xml = { version = "0.21", features = [ "serialize" ] } url = "2.1" opentelemetry = "0.17" +opentelemetry-prometheus = "0.10" +opentelemetry-otlp = "0.10" +prometheus = "0.13" [features] k2v = [ "garage_util/k2v", "garage_model/k2v" ] diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs new file mode 100644 index 00000000..836b5158 --- /dev/null +++ b/src/api/admin/api_server.rs @@ -0,0 +1,128 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use futures::future::Future; +use http::header::CONTENT_TYPE; +use hyper::{Body, Request, Response}; + +use opentelemetry::trace::{SpanRef, Tracer}; +use opentelemetry_prometheus::PrometheusExporter; +use prometheus::{Encoder, TextEncoder}; + +use garage_model::garage::Garage; +use garage_util::error::Error as GarageError; + +use crate::error::*; +use crate::generic_server::*; + +use crate::admin::router::{Authorization, Endpoint}; + +pub struct AdminApiServer { + garage: Arc<Garage>, + exporter: PrometheusExporter, + metrics_token: Option<String>, + admin_token: Option<String>, +} + +impl AdminApiServer { + pub fn new(garage: Arc<Garage>) -> Self { + let exporter = opentelemetry_prometheus::exporter().init(); + let cfg = &garage.config.admin; + let metrics_token = cfg + .metrics_token + .as_ref() + .map(|tok| format!("Bearer {}", tok)); + let admin_token = cfg + .admin_token + .as_ref() + .map(|tok| format!("Bearer {}", tok)); + Self { + garage, + exporter, + metrics_token, + admin_token, + } + } + + pub async fn run(self, shutdown_signal: impl Future<Output = ()>) -> Result<(), GarageError> { + if let Some(bind_addr) = self.garage.config.admin.api_bind_addr { + let region = self.garage.config.s3_api.s3_region.clone(); + ApiServer::new(region, self) + .run_server(bind_addr, shutdown_signal) + .await + } else { + Ok(()) + } + } + + fn handle_metrics(&self) -> Result<Response<Body>, Error> { + let mut buffer = vec![]; + let encoder = TextEncoder::new(); + + let tracer = opentelemetry::global::tracer("garage"); + let metric_families = tracer.in_span("admin/gather_metrics", |_| { + self.exporter.registry().gather() + }); + + encoder + .encode(&metric_families, &mut buffer) + .ok_or_internal_error("Could not serialize metrics")?; + + Ok(Response::builder() + .status(200) + .header(CONTENT_TYPE, encoder.format_type()) + .body(Body::from(buffer))?) + } +} + +#[async_trait] +impl ApiHandler for AdminApiServer { + const API_NAME: &'static str = "admin"; + const API_NAME_DISPLAY: &'static str = "Admin"; + + type Endpoint = Endpoint; + + fn parse_endpoint(&self, req: &Request<Body>) -> Result<Endpoint, Error> { + Endpoint::from_request(req) + } + + async fn handle( + &self, + req: Request<Body>, + endpoint: Endpoint, + ) -> Result<Response<Body>, Error> { + let expected_auth_header = match endpoint.authorization_type() { + Authorization::MetricsToken => self.metrics_token.as_ref(), + Authorization::AdminToken => self.admin_token.as_ref(), + }; + + if let Some(h) = expected_auth_header { + match req.headers().get("Authorization") { + None => Err(Error::Forbidden( + "Authorization token must be provided".into(), + )), + Some(v) if v.to_str().map(|hv| hv == h).unwrap_or(false) => Ok(()), + _ => Err(Error::Forbidden( + "Invalid authorization token provided".into(), + )), + }?; + } + + match endpoint { + Endpoint::Metrics => self.handle_metrics(), + _ => Err(Error::NotImplemented(format!( + "Admin endpoint {} not implemented yet", + endpoint.name() + ))), + } + } +} + +impl ApiEndpoint for Endpoint { + fn name(&self) -> &'static str { + Endpoint::name(self) + } + + fn add_span_attributes(&self, _span: SpanRef<'_>) {} +} diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs new file mode 100644 index 00000000..ff2cf4b1 --- /dev/null +++ b/src/api/admin/mod.rs @@ -0,0 +1,2 @@ +pub mod api_server; +mod router; diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs new file mode 100644 index 00000000..d0b30fc1 --- /dev/null +++ b/src/api/admin/router.rs @@ -0,0 +1,59 @@ +use crate::error::*; + +use hyper::{Method, Request}; + +use crate::router_macros::router_match; + +pub enum Authorization { + MetricsToken, + AdminToken, +} + +router_match! {@func + +/// List of all Admin API endpoints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Endpoint { + Metrics, + Options, + GetClusterStatus, + GetClusterLayout, + UpdateClusterLayout, + ApplyClusterLayout, + RevertClusterLayout, +}} + +impl Endpoint { + /// Determine which S3 endpoint a request is for using the request, and a bucket which was + /// possibly extracted from the Host header. + /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets + pub fn from_request<T>(req: &Request<T>) -> Result<Self, Error> { + let path = req.uri().path(); + + use Endpoint::*; + let res = match (req.method(), path) { + (&Method::OPTIONS, _) => Options, + (&Method::GET, "/metrics") => Metrics, + (&Method::GET, "/status") => GetClusterStatus, + (&Method::GET, "/layout") => GetClusterLayout, + (&Method::POST, "/layout") => UpdateClusterLayout, + (&Method::POST, "/layout/apply") => ApplyClusterLayout, + (&Method::POST, "/layout/revert") => RevertClusterLayout, + (m, p) => { + return Err(Error::BadRequest(format!( + "Unknown API endpoint: {} {}", + m, p + ))) + } + }; + + Ok(res) + } + /// Get the kind of authorization which is required to perform the operation. + pub fn authorization_type(&self) -> Authorization { + match self { + Self::Metrics => Authorization::MetricsToken, + _ => Authorization::AdminToken, + } + } +} diff --git a/src/api/lib.rs b/src/api/lib.rs index 0078f7b5..5c522799 100644 --- a/src/api/lib.rs +++ b/src/api/lib.rs @@ -12,6 +12,7 @@ mod router_macros; /// This mode is public only to help testing. Don't expect stability here pub mod signature; +pub mod admin; #[cfg(feature = "k2v")] pub mod k2v; pub mod s3; |