Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cat-gateway): config API #981

Merged
merged 47 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
30655de
feat: add config endpoint
bkioshn Oct 9, 2024
241481b
feat: add jsonschema lib
bkioshn Oct 9, 2024
9e15dff
fix: config table
bkioshn Oct 9, 2024
c5aa5ab
fix: sql format
bkioshn Oct 9, 2024
0ec7541
Merge branch 'main' into feat/config-api
bkioshn Oct 10, 2024
e3fd2ce
fix: comment to sql file
bkioshn Oct 10, 2024
3c78d55
feat: add upsert sql
bkioshn Oct 10, 2024
42b35b4
fix: update endpoint
bkioshn Oct 10, 2024
d9748e7
fix: frontend key implementation
bkioshn Oct 10, 2024
8f57b94
fix: config query
bkioshn Oct 10, 2024
bf7f8e0
fix: sql lint
bkioshn Oct 10, 2024
e1ae169
Merge branch 'main' into feat/config-api
bkioshn Oct 10, 2024
798b73f
fix: refactor
bkioshn Oct 11, 2024
0613100
Merge branch 'main' into feat/config-api
bkioshn Oct 11, 2024
939df7e
Merge branch 'main' into feat/config-api
stevenj Oct 15, 2024
c522ce4
fix: config endpoint
bkioshn Oct 15, 2024
eef973f
fix: lazy lock validator and rename function
bkioshn Oct 15, 2024
816447e
fix: frontend default and json schema
bkioshn Oct 15, 2024
087cd96
chore:add license MIT
bkioshn Oct 15, 2024
96e2cb5
fix: remove migration v2 to v9
bkioshn Oct 15, 2024
f0d7766
Merge branch 'main' into feat/config-api
bkioshn Oct 15, 2024
54258ef
fix: format
bkioshn Oct 15, 2024
7114d39
chore: change license to MIT-0
bkioshn Oct 15, 2024
27e5310
chore: remove license
bkioshn Oct 15, 2024
cfa7e55
fix: add mit-0 license to deny.toml and test it
bkioshn Oct 16, 2024
97d69ad
fix: update cat-gateway code gen
bkioshn Oct 16, 2024
5cc62ef
fix: update cat-gateway rust-ci version
bkioshn Oct 16, 2024
fad3cfc
fix: revert change
bkioshn Oct 16, 2024
5fde921
fix: add new endpoint and fix validate json
bkioshn Oct 16, 2024
4e1a2b3
fix: cat-gateway api code gen
bkioshn Oct 16, 2024
e4afeb1
Update catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json
stevenj Oct 16, 2024
65c587a
Update catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json
stevenj Oct 16, 2024
c6db422
Merge branch 'main' into feat/config-api
bkioshn Oct 17, 2024
ef78702
fix: frontend default and json schema
bkioshn Oct 17, 2024
80db594
fix: error handling
bkioshn Oct 17, 2024
577aee6
fix: cat-gateway api code gen
bkioshn Oct 17, 2024
a0062c2
fix: openapi lint
bkioshn Oct 17, 2024
ffa2dec
fix: frontend json schema
bkioshn Oct 17, 2024
1d6fa85
fix: error handling
bkioshn Oct 17, 2024
1a6ac40
fix: cat-gateway api code gen
bkioshn Oct 17, 2024
ff1399a
Merge branch 'main' into feat/config-api
bkioshn Oct 17, 2024
e0813d4
fix: remove id
bkioshn Oct 17, 2024
55965fb
fix: error log
bkioshn Oct 18, 2024
20290d9
Merge branch 'main' into feat/config-api
bkioshn Oct 18, 2024
5ea12b9
fix: bump ci to v3.2.18
bkioshn Oct 18, 2024
4dbde99
Merge branch 'main' into feat/config-api
stevenj Oct 18, 2024
a3ba7be
Merge branch 'main' into feat/config-api
stevenj Oct 18, 2024
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
1 change: 1 addition & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Joaquín
jorm
jormungandr
Jörmungandr
jsonschema
junitreport
junitxml
Keyhash
Expand Down
1 change: 1 addition & 0 deletions catalyst-gateway/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ base64 = "0.22.1"
dashmap = "6.1.0"
x509-cert = "0.2.5"
der-parser = "9.0.0"
jsonschema = "0.22.3"

[dev-dependencies]
proptest = "1.5.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object"
}
106 changes: 106 additions & 0 deletions catalyst-gateway/bin/src/db/event/config/key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Configuration Key

use std::{net::IpAddr, sync::LazyLock};

use serde_json::Value;
use tracing::error;

/// Configuration key
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ConfigKey {
/// Frontend general configuration.
Frontend,
/// Frontend configuration for a specific IP address.
FrontendForIp(IpAddr),
}

/// Frontend schema configuration.
static FRONTEND_SCHEMA: LazyLock<Option<Value>> =
LazyLock::new(|| load_json_lazy(include_str!("jsonschema/frontend.json")));

/// Frontend default configuration.
static FRONTEND_DEFAULT: LazyLock<Option<Value>> =
LazyLock::new(|| load_json_lazy(include_str!("default/frontend.json")));

/// Helper function to convert a JSON string to a JSON value.
fn load_json_lazy(data: &str) -> Option<Value> {
let json = serde_json::from_str(data);

match json {
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
Ok(value) => Some(value),
Err(err) => {
error!("Error parsing JSON : {:?}", err);
None
},
}
}

impl ConfigKey {
/// Convert a `ConfigKey` to its corresponding IDs.
pub(super) fn to_id(&self) -> (String, String, String) {
match self {
ConfigKey::Frontend => ("frontend".to_string(), String::new(), String::new()),
ConfigKey::FrontendForIp(ip) => {
("frontend".to_string(), "ip".to_string(), ip.to_string())
},
}
}

/// Validate the provided value against the JSON schema.
pub(super) fn validate(&self, value: &Value) -> anyhow::Result<()> {
// Retrieve the schema based on ConfigKey
#[allow(clippy::match_same_arms)]
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
let schema = match self {
ConfigKey::Frontend => &*FRONTEND_SCHEMA,
ConfigKey::FrontendForIp(_) => &*FRONTEND_SCHEMA,
};

let validator = match schema {
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
Some(s) => {
jsonschema::validator_for(s)
.map_err(|e| anyhow::anyhow!("Failed to create JSON validator: {:?}", e))?
},
None => return Err(anyhow::anyhow!("Failed to retrieve JSON schema")),
};

stevenj marked this conversation as resolved.
Show resolved Hide resolved
// Validate the value against the schema
if validator.is_valid(value) {
stevenj marked this conversation as resolved.
Show resolved Hide resolved
return Ok(());
}
Err(anyhow::anyhow!("Invalid configuration"))
}

/// Retrieve the default configuration value.
pub(super) fn default(&self) -> Option<Value> {
// Retrieve the default value based on the ConfigKey
#[allow(clippy::match_same_arms)]
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
let default = match self {
ConfigKey::Frontend => &*FRONTEND_DEFAULT,
ConfigKey::FrontendForIp(_) => &*FRONTEND_DEFAULT,
};

default.clone()
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;

#[test]
fn test_validate() {
let value = json!({
"test": "test"
});
let result = ConfigKey::Frontend.validate(&value);
assert!(result.is_ok());
}

#[test]
fn test_default() {
let result = ConfigKey::Frontend.default();
assert!(result.is_some());
}
}
51 changes: 51 additions & 0 deletions catalyst-gateway/bin/src/db/event/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Configuration query

use key::ConfigKey;
use serde_json::Value;

use crate::db::event::EventDB;

pub(crate) mod key;

/// Configuration struct
pub(crate) struct Config {}

/// SQL get configuration.
const GET_CONFIG: &str = include_str!("sql/get.sql");
/// SQL update if exist or else insert configuration.
const UPSERT_CONFIG: &str = include_str!("sql/upsert.sql");

impl Config {
/// Retrieve configuration based on the given `ConfigKey`.
///
/// # Returns
///
/// - A JSON value of the configuration, if not found, returns the default value.
pub(crate) async fn get(id: ConfigKey) -> anyhow::Result<Value> {
let (id1, id2, id3) = id.to_id();
let rows = EventDB::query(GET_CONFIG, &[&id1, &id2, &id3]).await?;

if let Some(row) = rows.first() {
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
let value: Value = row.get(0);
id.validate(&value).map_err(|e| anyhow::anyhow!(e))?;
return Ok(value);
}

// If data not found return default config value
match id.default() {
Some(default) => Ok(default),
None => Err(anyhow::anyhow!("Default value not found for {:?}", id)),
}
}

/// Update or insert (upsert) configuration for the given `ConfigKey`.
pub(crate) async fn upsert(id: ConfigKey, value: Value) -> anyhow::Result<()> {
// Validate the value
id.validate(&value)?;

let (id1, id2, id3) = id.to_id();
EventDB::query(UPSERT_CONFIG, &[&id1, &id2, &id3, &value]).await?;

Ok(())
}
}
7 changes: 7 additions & 0 deletions catalyst-gateway/bin/src/db/event/config/sql/get.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Select the 'value' column from the 'config' table
SELECT value
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
FROM config
WHERE
id1 = $1 -- Match rows where 'id1' equals the first parameter
AND id2 = $2 -- Match rows where 'id2' equals the second parameter
AND id3 = $3; -- Match rows where 'id3' equals the third parameter
3 changes: 3 additions & 0 deletions catalyst-gateway/bin/src/db/event/config/sql/insert.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Insert a new row into the 'config' table
INSERT INTO config (id1, id2, id3, value)
VALUES ($1, $2, $3, $4); -- The values to insert for each column
9 changes: 9 additions & 0 deletions catalyst-gateway/bin/src/db/event/config/sql/upsert.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Insert a new configuration entry into the 'config' table
INSERT INTO config (id1, id2, id3, value)
VALUES ($1, $2, $3, $4) -- Values to insert for each column

-- Handle conflicts when attempting to insert a row that would violate the unique constraint
ON CONFLICT (id1, id2, id3) -- Specify the unique constraint columns that identify conflicts

-- If a conflict occurs, update the existing row 'value' column with the new value provided
DO UPDATE SET value = excluded.value; -- 'EXCLUDED' refers to the values that were proposed for insertion
1 change: 1 addition & 0 deletions catalyst-gateway/bin/src/db/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use tracing::{debug, debug_span, error, Instrument};

use crate::settings::Settings;

pub(crate) mod config;
pub(crate) mod error;
pub(crate) mod legacy;
pub(crate) mod schema_check;
Expand Down
133 changes: 133 additions & 0 deletions catalyst-gateway/bin/src/service/api/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! Configuration Endpoints

use std::{net::IpAddr, str::FromStr};

use poem_openapi::{
param::{Header, Query},
payload::Json,
ApiResponse, OpenApi,
};
use serde_json::Value;

use crate::{
db::event::config::{key::ConfigKey, Config},
service::common::tags::ApiTags,
};

/// Configuration API struct
pub(crate) struct ConfigApi;

/// Endpoint responses
#[derive(ApiResponse)]
enum Responses {
/// Configuration result
#[oai(status = 200)]
Ok(Json<String>),
/// Configuration not found
#[oai(status = 404)]
NotFound(Json<String>),
/// Bad request
#[oai(status = 400)]
BadRequest(Json<String>),
}

#[OpenApi(tag = "ApiTags::Config")]
impl ConfigApi {
/// Get the configuration for the frontend.
/// Retrieving IP from X-Forwarded-For header if provided.
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
#[oai(
path = "/draft/config/frontend",
method = "get",
operation_id = "get_config_frontend"
)]
async fn get_frontend(
&self, #[oai(name = "X-Forwarded-For")] header: Header<Option<String>>,
) -> Responses {
// Retrieve the IP address from the header
// According to the X-Forwarded-For header spec, the first value is the IP address
let ip_address = header
.0
.as_ref()
.and_then(|h| h.split(',').next())
.map(String::from);

// Fetch the general configuration
let general_config = Config::get(ConfigKey::Frontend).await;

// Attempt to fetch the IP configuration
let ip_config = if let Some(ip) = ip_address {
match IpAddr::from_str(&ip) {
Ok(parsed_ip) => Config::get(ConfigKey::FrontendForIp(parsed_ip)).await.ok(),
Err(_) => {
return Responses::BadRequest(Json(format!("Invalid IP address: {ip}")));
},
}
} else {
None
};

// Handle the response
match general_config {
Ok(general) => {
// If there is IP specific config, replace any key in the general config with
// the keys from the IP specific config
let response_config = if let Some(ip_specific) = ip_config {
merge_configs(&general, &ip_specific)
} else {
general
};

Responses::Ok(Json(response_config.to_string()))
},
Err(err) => Responses::NotFound(Json(err.to_string())),
}
}

/// Insert or update the frontend configuration.
#[oai(
path = "/draft/config/frontend",
method = "put",
operation_id = "put_config_frontend"
)]
async fn put_frontend(&self, ip_query: Query<Option<String>>, body: Json<Value>) -> Responses {
let body_value = body.0;

match ip_query.0 {
Some(ip) => {
match IpAddr::from_str(&ip) {
Ok(parsed_ip) => upsert(ConfigKey::FrontendForIp(parsed_ip), body_value).await,
Err(err) => Responses::BadRequest(Json(format!("Invalid IP address: {err}"))),
}
},
None => upsert(ConfigKey::Frontend, body_value).await,
}
}
}

/// Helper function to merge two JSON values.
fn merge_configs(general: &Value, ip_specific: &Value) -> Value {
let mut merged = general.clone();

if let Some(ip_specific_obj) = ip_specific.as_object() {
if let Some(merged_obj) = merged.as_object_mut() {
for (key, value) in ip_specific_obj {
if let Some(existing_value) = merged_obj.get_mut(key) {
*existing_value = value.clone();
} else {
merged_obj.insert(key.clone(), value.clone());
}
}
}
}

merged
}

/// Helper function to handle upsert.
async fn upsert(key: ConfigKey, value: Value) -> Responses {
match Config::upsert(key, value).await {
Ok(()) => Responses::Ok(Json("Configuration upsert successfully.".to_string())),
Err(err) => Responses::BadRequest(Json(format!("Failed to upsert configuration: {err}"))),
}
}
5 changes: 4 additions & 1 deletion catalyst-gateway/bin/src/service/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! It however does NOT contain any processing for them, that is defined elsewhere.
use std::net::IpAddr;

use config::ConfigApi;
use gethostname::gethostname;
use health::HealthApi;
use legacy::LegacyApi;
Expand All @@ -15,6 +16,7 @@ use crate::settings::Settings;
/// Auth
mod auth;
pub(crate) mod cardano;
mod config;
mod health;
mod legacy;

Expand Down Expand Up @@ -59,11 +61,12 @@ const TERMS_OF_SERVICE: &str =
"https://github.com/input-output-hk/catalyst-voices/blob/main/CODE_OF_CONDUCT.md";

/// Create the `OpenAPI` definition
pub(crate) fn mk_api() -> OpenApiService<(HealthApi, CardanoApi, LegacyApi), ()> {
pub(crate) fn mk_api() -> OpenApiService<(HealthApi, CardanoApi, ConfigApi, LegacyApi), ()> {
let mut service = OpenApiService::new(
(
HealthApi,
CardanoApi,
ConfigApi,
(legacy::RegistrationApi, legacy::V0Api, legacy::V1Api),
),
API_TITLE,
Expand Down
2 changes: 2 additions & 0 deletions catalyst-gateway/bin/src/service/common/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ pub(crate) enum ApiTags {
V0,
/// API Version 1 Endpoints
V1,
/// Config
Config,
}
Loading
Loading