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 4 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"
}
90 changes: 90 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,90 @@
//! Configuration Key

use std::net::IpAddr;

use serde_json::Value;

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

/// Frontend JSON schema
const FRONTEND_JSON_SCHEMA: &str = include_str!("jsonschema/frontend.json");
/// Default frontend configuration
const FRONTEND_DEFAULT: &str = include_str!("default/frontend.json");

impl ConfigKey {
/// Create a `ConfigKey` from ids.
pub(crate) fn from_id(id1: &str, id2: &str, id3: &str) -> Option<Self> {
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
match (id1, id2, id3) {
("frontend", "", "") => Some(ConfigKey::Frontend),
("frontend", "ip", ip) => ip.parse().ok().map(ConfigKey::FrontendForIp),
_ => None,
}
}

/// Convert a `ConfigKey` to its 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<()> {
let schema: Value = serde_json::from_str(FRONTEND_JSON_SCHEMA)
stevenj marked this conversation as resolved.
Show resolved Hide resolved
.map_err(|e| anyhow::anyhow!("Failed to parse JSON schema: {:?}", e))?;

let validator = jsonschema::validator_for(&schema)
.map_err(|e| anyhow::anyhow!("Failed to create JSON validator: {:?}", e))?;

stevenj marked this conversation as resolved.
Show resolved Hide resolved
if validator.is_valid(value) {
stevenj marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
} else {
Err(anyhow::anyhow!(
"Provided JSON value does not match the schema for {:?}.",
self
))
}
}

/// Retrieve the default configuration value.
pub(super) fn default(&self) -> anyhow::Result<Value> {
let default_value: Value = serde_json::from_str(FRONTEND_DEFAULT)
stevenj marked this conversation as resolved.
Show resolved Hide resolved
.map_err(|e| anyhow::anyhow!("Failed to parse default JSON: {:?}", e))?;

match self {
ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => Ok(default_value),
}
}
}

#[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_ok());
}
}
48 changes: 48 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,48 @@
//! 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 insert configuration
const INSERT_CONFIG: &str = include_str!("sql/insert.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
id.default()
}

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

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

Ok(())
}
}
3 changes: 3 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,3 @@
SELECT value
bkioshn marked this conversation as resolved.
Show resolved Hide resolved
FROM config
WHERE id1 = $1 AND id2 = $2 AND id3 = $3;
2 changes: 2 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,2 @@
INSERT INTO config (id1, id2, id3, value)
VALUES ($1, $2, $3, $4)
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
100 changes: 100 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,100 @@
//! Configuration Endpoints

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

use poem_openapi::{param::Path, payload::Json, ApiResponse, Multipart, OpenApi};

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>),
}

/// Configuration insert request
#[derive(Multipart)]
struct ConfigInsertRequest {
/// ID1 of config table
id1: String,
/// ID2 of config table
id2: Option<String>,
/// ID3 of config table
id3: Option<String>,
/// Value of config table
value: String,
}

#[OpenApi(prefix_path = "/config", tag = "ApiTags::Config")]
impl ConfigApi {
/// Get the general configuration
#[oai(path = "/", method = "get", operation_id = "get_general_config")]
async fn get_general(&self) -> Responses {
match Config::get(ConfigKey::Frontend).await {
Ok(config) => Responses::Ok(Json(config.to_string())),
Err(err) => Responses::NotFound(Json(err.to_string())),
}
}

/// Get configuration for a specific IP
#[oai(path = "/:ip", method = "get", operation_id = "get_ip_config")]
async fn get_with_ip(&self, ip: Path<String>) -> Responses {
match IpAddr::from_str(&ip.0) {
Ok(parsed_ip) => {
match Config::get(ConfigKey::FrontendForIp(parsed_ip)).await {
Ok(config) => Responses::Ok(Json(config.to_string())),
Err(err) => Responses::NotFound(Json(err.to_string())),
}
},
Err(err) => Responses::BadRequest(Json(format!("Invalid IP address: {err}"))),
}
}

/// Insert configuration
#[oai(path = "/insert", method = "post", operation_id = "insert_config")]
async fn insert(&self, payload: ConfigInsertRequest) -> Responses {
stevenj marked this conversation as resolved.
Show resolved Hide resolved
let config_key = ConfigKey::from_id(
&payload.id1,
&payload.id2.unwrap_or_default(),
&payload.id3.unwrap_or_default(),
);

if let Some(config_key) = config_key {
match serde_json::from_str(&payload.value) {
Ok(parsed_value) => {
match Config::insert(config_key, parsed_value).await {
Ok(()) => {
Responses::Ok(Json("Configuration inserted successfully.".to_string()))
},
Err(err) => {
Responses::BadRequest(Json(format!(
"Failed to insert configuration: {err}"
)))
},
}
},
Err(err) => {
Responses::BadRequest(Json(format!("Failed to parse JSON value: {err}")))
},
}
} else {
Responses::BadRequest(Json(
"Invalid configuration key derives from ids.".to_string(),
))
}
}
}
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