Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

782 changes: 782 additions & 0 deletions gears/system/oagw/docs/DECOMPOSITION.md

Large diffs are not rendered by default.

838 changes: 838 additions & 0 deletions gears/system/oagw/docs/features/control-plane-config.md

Large diffs are not rendered by default.

560 changes: 560 additions & 0 deletions gears/system/oagw/docs/features/cors.md

Large diffs are not rendered by default.

1,079 changes: 1,079 additions & 0 deletions gears/system/oagw/docs/features/data-plane-proxy.md

Large diffs are not rendered by default.

493 changes: 493 additions & 0 deletions gears/system/oagw/docs/features/gear-foundation.md

Large diffs are not rendered by default.

655 changes: 655 additions & 0 deletions gears/system/oagw/docs/features/hierarchical-config.md

Large diffs are not rendered by default.

644 changes: 644 additions & 0 deletions gears/system/oagw/docs/features/observability.md

Large diffs are not rendered by default.

852 changes: 852 additions & 0 deletions gears/system/oagw/docs/features/plugin-system.md

Large diffs are not rendered by default.

795 changes: 795 additions & 0 deletions gears/system/oagw/docs/features/rate-limiting.md

Large diffs are not rendered by default.

657 changes: 657 additions & 0 deletions gears/system/oagw/docs/features/streaming.md

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions gears/system/oagw/oagw/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ credstore = { workspace = true }
opentelemetry = { workspace = true }
heck = { workspace = true }
# CP deps
jsonschema = { workspace = true }
dashmap = { workspace = true }
parking_lot = { workspace = true }
psl = { workspace = true }
Expand All @@ -75,10 +76,10 @@ mime = { workspace = true }
form_urlencoded = "1"
pingora-memory-cache = "0.8"
futures-util = { workspace = true, features = ["sink"] }
tokio = { workspace = true, features = ["time"] }
tokio = { workspace = true, features = ["time", "macros"] }
tokio-retry = { workspace = true }
hyper = { workspace = true }
hyper-util = { workspace = true }
hyper-util = { workspace = true, features = ["tokio"] }
# Pingora proxy engine
pingora-proxy = { version = "0.8", features = ["rustls"] }
pingora-core = { version = "0.8", features = ["rustls"] }
Expand Down Expand Up @@ -109,3 +110,4 @@ httpmock = { workspace = true }
tokio-rustls = { workspace = true }
rustls = { workspace = true }
futures-util = { workspace = true }
tracing-test = { workspace = true, features = ["no-env-filter"] }
7 changes: 7 additions & 0 deletions gears/system/oagw/oagw/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Transport layer of the `oagw` gear.
//!
//! The only module in the crate allowed to touch `axum` and `http`. Domain
//! failures arrive here as [`crate::domain::DomainError`] and leave as either
//! an RFC 9457 problem document or a preserved upstream response.

pub mod rest;
161 changes: 161 additions & 0 deletions gears/system/oagw/oagw/src/api/rest/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! Wire representations of the configuration rows — `cpt-cf-oagw-dod-management-routes`.
//!
//! The representation of one row is the resource kind's own schema shape: the
//! domain type serialized as it is declared, with `id` carried as the resource's
//! anonymous GTS instance. A list page carries the platform page envelope plus
//! the projection the caller asked for, and the projection is applied to every
//! item on the wire.

use serde_json::{Map, Value};
use uuid::Uuid;

use crate::control_plane::odata::Page;
use crate::domain::plugin_contract::PluginFamily;
use crate::gts;
use crate::store::{PluginRow, RouteRow, UpstreamRow};

/// The platform page envelope's row set.
const ITEMS: &str = "items";
/// The platform page envelope's paging metadata.
const PAGE_INFO: &str = "page_info";
/// The projection the page was built with, present only when one was asked for.
const PROJECTION: &str = "projection";

/// The anonymous GTS instance identifier of one upstream row.
#[must_use]
pub fn upstream_id(id: Uuid) -> String {
gts::gts_instance(gts::UPSTREAM_TYPE, id)
}

/// The anonymous GTS instance identifier of one route row.
#[must_use]
pub fn route_id(id: Uuid) -> String {
gts::gts_instance(gts::ROUTE_TYPE, id)
}

/// The representation of one upstream row.
#[must_use]
pub fn upstream(row: &UpstreamRow) -> Value {
let representation = serde_json::to_value(&row.upstream).unwrap_or_default();
identified(representation, upstream_id(row.upstream.id))
}

/// The representation of one route row.
#[must_use]
pub fn route(row: &RouteRow) -> Value {
let representation = serde_json::to_value(&row.route).unwrap_or_default();
identified(representation, route_id(row.route.id))
}

/// The wire page of an upstream list.
#[must_use]
pub fn upstream_page(page: &Page<UpstreamRow>) -> Value {
let items: Vec<Value> = page
.items
.iter()
.map(|row| projected(&upstream(row), &page.projection))
.collect();
envelope(items, &page.projection, page.top)
}

/// The wire page of a route list.
#[must_use]
pub fn route_page(page: &Page<RouteRow>) -> Value {
let items: Vec<Value> = page
.items
.iter()
.map(|row| projected(&route(row), &page.projection))
.collect();
envelope(items, &page.projection, page.top)
}

/// The anonymous GTS instance identifier of one plugin row, derived from the
/// family its `plugin_type` names.
#[must_use]
pub fn plugin_id(family: PluginFamily, id: Uuid) -> String {
gts::gts_instance(family.base_type(), id)
}

/// The representation of one plugin row.
///
/// The configuration schema and the source are carried exactly as stored: no
/// member is re-rendered, defaulted, or dropped.
#[must_use]
pub fn plugin(row: &PluginRow) -> Value {
let representation = serde_json::to_value(&row.plugin).unwrap_or_default();
identified(representation, plugin_row_id(row))
}

/// The stored Starlark source of one plugin, as the source path answers it.
#[must_use]
pub fn plugin_source(source: &str) -> Value {
Value::from(source)
}

/// The wire page of a plugin list.
#[must_use]
pub fn plugin_page(page: &Page<PluginRow>) -> Value {
let items: Vec<Value> = page
.items
.iter()
.map(|row| projected(&plugin(row), &page.projection))
.collect();
envelope(items, &page.projection, page.top)
}

/// The anonymous GTS instance identifier one stored plugin row answers to,
/// derived from the family literal its `plugin_type` carries.
fn plugin_row_id(row: &PluginRow) -> String {
match PluginFamily::from_type_literal(&row.plugin.plugin_type) {
Some(family) => plugin_id(family, row.plugin.id),
// The store's invariant check holds every stored literal to one of the
// three families, so a row that named no family is never stored; it
// would answer to its bare identifier.
None => row.plugin.id.to_string(),
}
}

/// Serializes one domain row and states its identifier as the anonymous GTS
/// instance the resource kind is addressed by.
fn identified(mut representation: Value, id: String) -> Value {
if let Some(object) = representation.as_object_mut() {
object.insert(String::from("id"), Value::from(id));
}
representation
}

/// Narrows one representation to the properties the caller projected.
///
/// An empty projection leaves the representation whole.
fn projected(representation: &Value, projection: &[String]) -> Value {
if projection.is_empty() {
return representation.clone();
}
let Some(fields) = representation.as_object() else {
return representation.clone();
};
let mut narrowed = Map::new();
for name in projection {
if let Some(value) = fields.get(name.as_str()) {
narrowed.insert(name.clone(), value.clone());
}
}
Value::Object(narrowed)
}

/// The platform page envelope: the row set, the paging metadata, and — only
/// when the caller projected — the projection the items were narrowed to.
fn envelope(items: Vec<Value>, projection: &[String], top: u64) -> Value {
let mut page_info = Map::new();
page_info.insert(String::from("limit"), Value::from(top));
page_info.insert(String::from("next_cursor"), Value::Null);
page_info.insert(String::from("prev_cursor"), Value::Null);

let mut body = Map::new();
body.insert(String::from(ITEMS), Value::from(items));
body.insert(String::from(PAGE_INFO), Value::Object(page_info));
if !projection.is_empty() {
body.insert(String::from(PROJECTION), Value::from(projection.to_vec()));
}
Value::Object(body)
}
112 changes: 112 additions & 0 deletions gears/system/oagw/oagw/src/api/rest/handlers/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! The metrics surface — the one path `cpt-cf-oagw-feature-observability`
//! registers.
//!
//! `GET /oagw/v1/metrics` is gear-relative on the mount point the foundation
//! created, is registered for that method alone, and is enforced with the
//! `gts.cf.core.oagw.metrics.v1~:read` permission before any collector is
//! read. The handler reads nothing but the seam's exposition and answers no
//! audit record of its own: the scrape observes nothing and is observed by
//! nothing (§1.5).

use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Response;
use axum::Extension;
use toolkit_security::SecurityContext;

use super::{SharedState, READ, SUPPORTED_PROPERTIES};
use crate::api::rest::problem;

/// The content type the Prometheus text exposition format names.
const EXPOSITION_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8";

/// The enforcer's descriptor of the metrics resource.
#[must_use]
fn metrics_resource_type() -> authz_resolver_sdk::pep::ResourceType {
authz_resolver_sdk::pep::ResourceType::from_static(
crate::gts::METRICS_TYPE,
SUPPORTED_PROPERTIES,
)
}

/// The 403 the metrics permission is answered with.
fn forbidden(instance: &str) -> Response {
problem::forbidden_response(crate::gts::METRICS_TYPE, instance)
}

/// Answers `GET /oagw/v1/metrics` with the text exposition of the twelve
/// families DESIGN §4.2 declares.
pub async fn scrape(
State(state): State<SharedState>,
context: Option<Extension<SecurityContext>>,
) -> Response {
let instance = String::from("/oagw/v1/metrics");

// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-issue
// The actor issues the scrape with a bearer token; the handler answers it
// with the exposition or with a refusal, and decides nothing else.
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-issue
// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-api
// The API is the one path this feature registers, on the gear-relative
// mount point the foundation created, and the platform middleware that
// authenticates the bearer token has run before this handler did.
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-api

// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-authz
// The permission is enforced before any collector is read: a token without
// it is answered 403 and renders nothing, and a request with no
// authenticated subject is answered 401, because the platform middleware
// that would have resolved one did not run for it.
let Some(context) = context.as_ref().map(|extension| &extension.0) else {
return problem::problem_response(
&crate::domain::error::DomainError::gateway(
crate::domain::error::ErrorKind::AuthenticationFailed,
"the request carries no authenticated subject",
),
&instance,
);
};
let Some(enforcer) = state.enforcer() else {
tracing::warn!(instance, "no AuthZ client resolved; the metrics surface fails closed");
return forbidden(&instance);
};
if let Err(error) = enforcer
.access_scope(context, &metrics_resource_type(), READ, None)
.await
{
tracing::warn!(instance, error = %error, "the metrics permission was refused");
// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-permitted-else
// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-forbidden
// RETURN 403 with no exposition rendered, through the foundation's
// error mapping: an `application/problem+json` body tagged
// `X-OAGW-Error-Source: gateway` that carries the `trace_id` of the
// correlation context this scrape request was assigned.
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-forbidden
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-permitted-else
return forbidden(&instance);
}
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-authz

// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-permitted-if
// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-render
// `cpt-cf-oagw-algo-metrics-render` reads the twelve collectors at the
// moment the scrape is served and renders the text exposition format, with
// a `# HELP` and a `# TYPE` line per family and the histogram as its
// `_bucket` series plus its `_sum` and `_count` series.
let exposition = state.observability().render();
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-render
// @cpt-begin:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-return
// RETURN 200 with the exposition and the content type the format names;
// no audit record is written and no series is observed for the scrape
// itself.
Response::builder()
.status(StatusCode::OK)
.header(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static(EXPOSITION_TYPE),
)
.body(axum::body::Body::from(exposition))
.unwrap_or_else(|_| Response::new(axum::body::Body::empty()))
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-return
// @cpt-end:cpt-cf-oagw-flow-metrics-scrape:p1:inst-ms-permitted-if
}
Loading