aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2022-05-05 10:29:45 +0200
committerAlex Auvolat <alex@adnab.me>2022-05-10 13:25:06 +0200
commit633958c7b1ce9c83df5159051fd299b484d0d797 (patch)
tree906246beab76ee6981af03fb31ce04421cd6b6ab /src/api
parent5768bf362262f78376af14517c4921941986192e (diff)
downloadgarage-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.toml3
-rw-r--r--src/api/admin/api_server.rs128
-rw-r--r--src/api/admin/mod.rs2
-rw-r--r--src/api/admin/router.rs59
-rw-r--r--src/api/lib.rs1
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;