Skip to content

Commit

Permalink
feat(cat-gateway): config API (#981)
Browse files Browse the repository at this point in the history
* feat: add config endpoint

Signed-off-by: bkioshn <[email protected]>

* feat: add jsonschema lib

Signed-off-by: bkioshn <[email protected]>

* fix: config table

Signed-off-by: bkioshn <[email protected]>

* fix: sql format

Signed-off-by: bkioshn <[email protected]>

* fix: comment to sql file

Signed-off-by: bkioshn <[email protected]>

* feat: add upsert sql

Signed-off-by: bkioshn <[email protected]>

* fix: update endpoint

Signed-off-by: bkioshn <[email protected]>

* fix: frontend key implementation

Signed-off-by: bkioshn <[email protected]>

* fix: config query

Signed-off-by: bkioshn <[email protected]>

* fix: sql lint

Signed-off-by: bkioshn <[email protected]>

* fix: refactor

Signed-off-by: bkioshn <[email protected]>

* fix: config endpoint

Signed-off-by: bkioshn <[email protected]>

* fix: lazy lock validator and rename function

Signed-off-by: bkioshn <[email protected]>

* fix: frontend default and json schema

Signed-off-by: bkioshn <[email protected]>

* chore:add license MIT

Signed-off-by: bkioshn <[email protected]>

* fix: remove migration v2 to v9

Signed-off-by: bkioshn <[email protected]>

* fix: format

Signed-off-by: bkioshn <[email protected]>

* chore: change license to MIT-0

Signed-off-by: bkioshn <[email protected]>

* chore: remove license

Signed-off-by: bkioshn <[email protected]>

* fix: add mit-0 license to deny.toml and test it

Signed-off-by: bkioshn <[email protected]>

* fix: update cat-gateway code gen

Signed-off-by: bkioshn <[email protected]>

* fix: update cat-gateway rust-ci version

Signed-off-by: bkioshn <[email protected]>

* fix: revert change

Signed-off-by: bkioshn <[email protected]>

* fix: add new endpoint and fix validate json

Signed-off-by: bkioshn <[email protected]>

* fix: cat-gateway api code gen

Signed-off-by: bkioshn <[email protected]>

* Update catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json

* Update catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json

* fix: frontend default and json schema

Signed-off-by: bkioshn <[email protected]>

* fix: error handling

Signed-off-by: bkioshn <[email protected]>

* fix: cat-gateway api code gen

Signed-off-by: bkioshn <[email protected]>

* fix: openapi lint

Signed-off-by: bkioshn <[email protected]>

* fix: frontend json schema

Signed-off-by: bkioshn <[email protected]>

* fix: error handling

Signed-off-by: bkioshn <[email protected]>

* fix: cat-gateway api code gen

Signed-off-by: bkioshn <[email protected]>

* fix: remove id

Signed-off-by: bkioshn <[email protected]>

* fix: error log

Signed-off-by: bkioshn <[email protected]>

* fix: bump ci to v3.2.18

Signed-off-by: bkioshn <[email protected]>

---------

Signed-off-by: bkioshn <[email protected]>
Co-authored-by: Steven Johnson <[email protected]>
  • Loading branch information
bkioshn and stevenj authored Oct 18, 2024
1 parent 54ec09d commit 63fee26
Show file tree
Hide file tree
Showing 36 changed files with 616 additions and 1,187 deletions.
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
6 changes: 3 additions & 3 deletions Earthfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
VERSION 0.8

IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.16 AS mdlint-ci
IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.16 AS cspell-ci
IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.16 AS postgresql-ci
IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.18 AS mdlint-ci
IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.18 AS cspell-ci
IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.18 AS postgresql-ci

FROM debian:stable-slim

Expand Down
2 changes: 1 addition & 1 deletion catalyst-gateway/Earthfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
VERSION 0.8

IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.16 AS rust-ci
IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.18 AS rust-ci

#cspell: words rustfmt toolsets USERARCH stdcfgs

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,5 @@
{
"sentry": {
"environment": "dev"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
30 changes: 30 additions & 0 deletions catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Frontend JSON schema",
"type": "object",
"sentry": {
"type": "object",
"description": "Configuration for Sentry.",
"properties": {
"dsn": {
"$ref": "#/definitions/httpsUrl",
"description": "The Data Source Name (DSN) for Sentry."
},
"release": {
"type": "string",
"description": "A version of the code deployed to an environment"
},
"environment": {
"type": "string",
"description": "The environment in which the application is running, e.g., 'dev', 'qa'."
}
}
},
"definitions": {
"httpsUrl": {
"type": "string",
"format": "uri",
"pattern": "^https?://"
}
}
}
140 changes: 140 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,140 @@
//! Configuration Key

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

use jsonschema::{BasicOutput, Validator};
use serde_json::{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),
}

impl Display for ConfigKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigKey::Frontend => write!(f, "config_key_frontend"),
ConfigKey::FrontendForIp(_) => write!(f, "config_key_frontend_ip"),
}
}
}

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

/// Frontend schema validator.
static FRONTEND_SCHEMA_VALIDATOR: LazyLock<Validator> =
LazyLock::new(|| schema_validator(&FRONTEND_SCHEMA));

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

/// Frontend specific configuration.
static FRONTEND_IP_DEFAULT: LazyLock<Value> =
LazyLock::new(|| load_json_lazy(include_str!("default/frontend_ip.json")));

/// Helper function to create a JSON validator from a JSON schema.
/// If the schema is invalid, a default JSON validator is created.
fn schema_validator(schema: &Value) -> Validator {
jsonschema::validator_for(schema).unwrap_or_else(|err| {
error!(
id = "schema_validator",
error=?err,
"Error creating JSON validator"
);

// Create a default JSON validator as a fallback
// This should not fail since it is hard coded
#[allow(clippy::expect_used)]
Validator::new(&json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object"
}))
.expect("Failed to create default JSON validator")
})
}

/// Helper function to convert a JSON string to a JSON value.
fn load_json_lazy(data: &str) -> Value {
serde_json::from_str(data).unwrap_or_else(|err| {
error!(id = "load_json_lazy", error=?err, "Error parsing JSON");
json!({})
})
}

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) -> BasicOutput<'static> {
// Retrieve the validator based on ConfigKey
let validator = match self {
ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => &*FRONTEND_SCHEMA_VALIDATOR,
};

// Validate the value against the schema
validator.apply(value).basic()
}

/// Retrieve the default configuration value.
pub(super) fn default(&self) -> Value {
// Retrieve the default value based on the ConfigKey
match self {
ConfigKey::Frontend => FRONTEND_DEFAULT.clone(),
ConfigKey::FrontendForIp(_) => FRONTEND_IP_DEFAULT.clone(),
}
}

/// Retrieve the JSON schema.
pub(crate) fn schema(&self) -> &Value {
match self {
ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => &FRONTEND_SCHEMA,
}
}
}

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

use super::*;

#[test]
fn test_valid_validate() {
let value = json!({
"test": "test"
});
let result = ConfigKey::Frontend.validate(&value);
assert!(result.is_valid());
println!("{:?}", serde_json::to_value(result).unwrap());
}

#[test]
fn test_invalid_validate() {
let value = json!([]);
let result = ConfigKey::Frontend.validate(&value);
assert!(!result.is_valid());
println!("{:?}", serde_json::to_value(result).unwrap());
}

#[test]
fn test_default() {
let result = ConfigKey::Frontend.default();
assert!(result.is_object());
}
}
64 changes: 64 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,64 @@
//! Configuration query

use jsonschema::BasicOutput;
use key::ConfigKey;
use serde_json::Value;
use tracing::error;

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 or error, returns the default
/// value.
/// - Error if the query fails.
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() {
let value: Value = row.get(0);
match id.validate(&value) {
BasicOutput::Valid(_) => return Ok(value),
BasicOutput::Invalid(errors) => {
// This should not happen, expecting the schema to be valid
error!(id=%id, error=?errors, "Get Config, schema validation failed, defaulting.");
},
}
}
// Return the default config value as a fallback
Ok(id.default())
}

/// Set the configuration for the given `ConfigKey`.
///
/// # Returns
///
/// - A `BasicOutput` of the validation result, which can be valid or invalid.
/// - Error if the query fails.
pub(crate) async fn set(id: ConfigKey, value: Value) -> anyhow::Result<BasicOutput<'static>> {
let validate = id.validate(&value);
// Validate schema failed, return immediately with JSON schema error
if !validate.is_valid() {
return Ok(validate);
}

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

Ok(validate)
}
}
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
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
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
Loading

0 comments on commit 63fee26

Please sign in to comment.