diff --git a/Cargo.toml b/Cargo.toml index fa1a51f0bd..725dbd9c80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ quilkin-macros = { version = "0.3.0-dev", path = "./macros" } base64 = "0.13.0" base64-serde = "0.6.1" bytes = "1.1.0" -clap = { version = "3", features = ["cargo"] } +clap = { version = "3", features = ["cargo", "derive", "env"] } dashmap = "4.0.2" either = "1.6.1" hyper = "0.14.15" @@ -70,6 +70,7 @@ eyre = "0.6.5" stable-eyre = "0.2.2" ipnetwork = "0.18.0" futures = "0.3.17" +schemars = "0.8.8" [target.'cfg(target_os = "linux")'.dependencies] sys-info = "0.9.0" diff --git a/docs/book.toml b/docs/book.toml index 420102ca06..f308768dcc 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -4,4 +4,10 @@ multilingual = false src = "src" title = "Quilkin Book" +[preprocessor.links] +after = ["quilkin"] + +[preprocessor.quilkin] +command = "./preprocessor.sh" + [output.html] diff --git a/docs/preprocessor.sh b/docs/preprocessor.sh new file mode 100755 index 0000000000..83323813ca --- /dev/null +++ b/docs/preprocessor.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cargo run -q --manifest-path ../Cargo.toml -- -q generate-config-schema -o ../target + +echo $(jq -M -c .[1] <&0) diff --git a/docs/src/filters/capture_bytes.md b/docs/src/filters/capture_bytes.md index e36fd333bd..d7f3a41522 100644 --- a/docs/src/filters/capture_bytes.md +++ b/docs/src/filters/capture_bytes.md @@ -36,31 +36,7 @@ static: ### Configuration Options ([Rust Doc](../../api/quilkin/filters/capture_bytes/struct.Config.html)) ```yaml -properties: - strategy: - type: string - description: | - The selected strategy for capturing the series of bytes from the incoming packet. - - SUFFIX: Retrieve bytes from the end of the packet. - - PREFIX: Retrieve bytes from the beginnning of the packet. - default: "SUFFIX" - enum: ['PREFIX', 'SUFFIX'] - metadataKey: - type: string - default: quilkin.dev/captured_bytes - description: | - The key under which the captured bytes are stored in the Filter invocation values. - size: - type: integer - description: | - The number of bytes in the packet to capture using the applied strategy. - remove: - type: boolean - default: false - description: | - Whether or not to remove the captured bytes from the packet before passing it along to the next filter in the - chain. - required: ['size'] +{{#include ../../../target/quilkin.extensions.filters.capture_bytes.v1alpha1.yaml}} ``` ### Metrics diff --git a/docs/src/filters/compress.md b/docs/src/filters/compress.md index 3fa6de92c7..eee5c5396b 100644 --- a/docs/src/filters/compress.md +++ b/docs/src/filters/compress.md @@ -1,6 +1,6 @@ # Compress -The `Compress` filter's job is to provide a variety of compression implementations for compression +The `Compress` filter's job is to provide a variety of compression implementations for compression and subsequent decompression of UDP data when sent between systems, such as a game client and game server. #### Filter name @@ -27,9 +27,9 @@ static: # quilkin::Builder::from(std::sync::Arc::new(config)).validate().unwrap(); ``` -The above example shows a proxy that could be used with a typical game client, where the original client data is -sent to the local listening port and then compressed when heading up to a dedicated game server, and then -decompressed when traffic is returned from the dedicated game server before being handed back to game client. +The above example shows a proxy that could be used with a typical game client, where the original client data is +sent to the local listening port and then compressed when heading up to a dedicated game server, and then +decompressed when traffic is returned from the dedicated game server before being handed back to game client. > It is worth noting that since the Compress filter modifies the *entire packet*, it is worth paying special attention to the order it is placed in your [Filter configuration](../filters.md). Most of the time it will likely be @@ -38,38 +38,14 @@ decompressed when traffic is returned from the dedicated game server before bein ### Configuration Options ([Rust Doc](../../api/quilkin/filters/compress/struct.Config.html)) ```yaml -properties: - on_read: - '$ref': '#/definitions/action' - description: | - Whether to compress, decompress or do nothing when reading packets from the local listening port - on_write: - '$ref': '#/definitions/action' - description: | - Whether to compress, decompress or do nothing when writing packets to the local listening port - mode: - type: string - description: | - The compression implementation to use on the incoming and outgoing packets. See "Compression Modes" for details. - enum: - - SNAPPY - default: SNAPPY - -definitions: - action: - type: string - enum: - - DO_NOTHING - - COMPRESS - - DECOMPRESS - default: DO_NOTHING +{{#include ../../../target/quilkin.extensions.filters.compress.v1alpha1.yaml}} ``` #### Compression Modes ##### Snappy -> Snappy is a compression/decompression library. It does not aim for maximum compression, or compatibility with any +> Snappy is a compression/decompression library. It does not aim for maximum compression, or compatibility with any > other compression library; instead, it aims for very high speeds and reasonable compression. Currently, this filter only provides the [Snappy](https://github.com/google/snappy/) compression format via the @@ -78,7 +54,7 @@ provided in the future. ### Metrics * `quilkin_filter_Compress_packets_dropped_total` - Total number of packets dropped as they could not be processed. + Total number of packets dropped as they could not be processed. * Labels: * `action`: The action that could not be completed successfully, thereby causing the packet to be dropped. * `Compress`: Compressing the packet with the configured `mode` was attempted. diff --git a/docs/src/filters/concatenate_bytes.md b/docs/src/filters/concatenate_bytes.md index de3e0874cf..165e309a13 100644 --- a/docs/src/filters/concatenate_bytes.md +++ b/docs/src/filters/concatenate_bytes.md @@ -30,23 +30,7 @@ static: ### Configuration Options ([Rust Doc](../../api/quilkin/filters/concatenate_bytes/struct.Config.html)) ```yaml -properties: - on_read: - type: string - description: | - Either append or prepend the `bytes` data to each packet filtered on read of the listening port. - default: DO_NOTHING - enum: ['DO_NOTHING', 'APPEND', 'PREPEND'] - on_write: - type: string - description: | - Either append or prepend the `bytes` data to each packet filtered on write of the listening port. - default: DO_NOTHING - enum: ['DO_NOTHING', 'APPEND', 'PREPEND'] - bytes: - type: string - description: | - Base64 encoded string of the byte array to add to each packet as it is filtered. +{{#include ../../../target/quilkin.extensions.filters.concatenate_bytes.v1alpha1.yaml}} ``` ### Metrics diff --git a/docs/src/filters/debug.md b/docs/src/filters/debug.md index 4d312571db..385655623b 100644 --- a/docs/src/filters/debug.md +++ b/docs/src/filters/debug.md @@ -29,11 +29,7 @@ static: ### Configuration Options ([Rust Doc](../../api/quilkin/filters/debug/struct.Config.html)) ```yaml -properties: - id: - type: string - description: | - An identifier that will be included with each log message. +{{#include ../../../target/quilkin.extensions.filters.debug.v1alpha1.yaml}} ``` diff --git a/docs/src/filters/firewall.md b/docs/src/filters/firewall.md index 691f504a52..166fef2d46 100644 --- a/docs/src/filters/firewall.md +++ b/docs/src/filters/firewall.md @@ -38,41 +38,7 @@ static: ### Configuration Options ([Rust Doc](../../api/quilkin/filters/firewall/struct.Config.html)) ```yaml -properties: - on_read: - '$ref': '#/definitions/rules' - description: Rules to match against when reading packets to the local listening port. - on_write: - type: array - '$ref': '#/definitions/rules' - description: Rules to match against when writing packets to the local listening port. - -definitions: - rules: - type: array - description: Rules to match against when writing packets to the local listening port. - items: - type: object - properties: - action: - type: string - description: | - Whether or not a matching Rule should Allow or Deny access - - DENY: If the rule matches, block the traffic. - - ALLOW: If the rule matches, allow the traffic through. - enum: ['ALLOW', 'DENY'] - source: - type: string - description: A CIDR network range, either in a v4 or v6 format. - ports: - type: array - description: Array of singular ports or port ranges to match against. - items: - type: string - description: | - Either in the format of "10" for a singular port or "10-100" for a port range where - min is inclusive, and max is exclusive. - required: ['action', 'source', 'ports'] +{{#include ../../../target/quilkin.extensions.filters.firewall.v1alpha1.yaml}} ``` #### Rule Evaluation diff --git a/docs/src/filters/load_balancer.md b/docs/src/filters/load_balancer.md index efa2ea7662..2cedc146ea 100644 --- a/docs/src/filters/load_balancer.md +++ b/docs/src/filters/load_balancer.md @@ -33,16 +33,7 @@ In the example above, packets will be distributed by selecting endpoints in turn ### Configuration Options ([Rust Doc](../../api/quilkin/filters/load_balancer/struct.Config.html)) ```yaml -properties: - policy: - type: string - description: | - The load balancing policy with which to distribute packets among endpoints. - enum: - - ROUND_ROBIN # Send packets by selecting endpoints in turn. - - RANDOM # Send packets by randomly selecting endpoints. - - HASH # Send packets by hashing the source IP and port. - default: ROUND_ROBIN +{{#include ../../../target/quilkin.extensions.filters.load_balancer.v1alpha1.yaml}} ``` ### Metrics diff --git a/docs/src/filters/local_rate_limit.md b/docs/src/filters/local_rate_limit.md index 7e92d38afa..85ced02f74 100644 --- a/docs/src/filters/local_rate_limit.md +++ b/docs/src/filters/local_rate_limit.md @@ -42,21 +42,7 @@ To configure a rate limiter, we specify the maximum rate at which the proxy is a ### Configuration Options ([Rust Doc](../../api/quilkin/filters/local_rate_limit/struct.Config.html)) ```yaml -properties: - max_packets: - type: integer - description: | - The maximum number of packets allowed to be forwarded over the given duration. - minimum: 0 - - period: - type: string - description: | - The duration in seconds overwhich `max_packets` applies. - default: 1 # 1 second - minimum: 1 - -required: [ 'max_packets' ] +{{#include ../../../target/quilkin.extensions.filters.local_rate_limit.v1alpha1.yaml}} ``` diff --git a/docs/src/filters/token_router.md b/docs/src/filters/token_router.md index 6ab77151d6..38d15bd600 100644 --- a/docs/src/filters/token_router.md +++ b/docs/src/filters/token_router.md @@ -43,12 +43,7 @@ View the [CaptureBytes](./capture_bytes.md) filter documentation for more detail ### Configuration Options ([Rust Doc](../../api/quilkin/filters/token_router/struct.Config.html)) ```yaml -properties: - metadataKey: - type: string - default: quilkin.dev/captured_bytes - description: | - The key under which the token is stored in the Filter dynamic metadata. +{{#include ../../../target/quilkin.extensions.filters.token_router.v1alpha1.yaml}} ``` ### Metrics diff --git a/docs/src/filters/writing_custom_filters.md b/docs/src/filters/writing_custom_filters.md index 8a7cfe24be..7724fae686 100644 --- a/docs/src/filters/writing_custom_filters.md +++ b/docs/src/filters/writing_custom_filters.md @@ -111,6 +111,11 @@ impl FilterFactory for GreetFilterFactory { fn name(&self) -> &'static str { NAME } + + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(serde_json::Value) + } + fn create_filter(&self, _: CreateFilterArgs) -> Result { let filter: Box = Box::new(Greet); Ok(FilterInstance::new(serde_json::Value::Null, filter)) @@ -235,6 +240,11 @@ impl FilterFactory for GreetFilterFactory { fn name(&self) -> &'static str { NAME } + + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(serde_json::Value) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let config = match args.config.unwrap() { ConfigType::Static(config) => { diff --git a/examples/quilkin-filter-example/Cargo.toml b/examples/quilkin-filter-example/Cargo.toml index e2db318aa9..c00639925b 100644 --- a/examples/quilkin-filter-example/Cargo.toml +++ b/examples/quilkin-filter-example/Cargo.toml @@ -34,6 +34,7 @@ prost-types = "0.9.0" serde = "1.0" serde_yaml = "0.8" bytes = "1.1.0" +schemars = "0.8.8" [build-dependencies] prost-build = "0.9.0" diff --git a/examples/quilkin-filter-example/src/main.rs b/examples/quilkin-filter-example/src/main.rs index ec8d731d01..7bba59d9b1 100644 --- a/examples/quilkin-filter-example/src/main.rs +++ b/examples/quilkin-filter-example/src/main.rs @@ -24,7 +24,7 @@ use serde::{Deserialize, Serialize}; use std::convert::TryFrom; // ANCHOR: serde_config -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, schemars::JsonSchema)] struct Config { greeting: String, } @@ -71,6 +71,11 @@ impl FilterFactory for GreetFilterFactory { fn name(&self) -> &'static str { NAME } + + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? @@ -84,7 +89,7 @@ impl FilterFactory for GreetFilterFactory { // ANCHOR: run #[tokio::main] async fn main() { - quilkin::run(vec![self::factory()].into_iter()) + quilkin::run(quilkin::Config::builder().build(), vec![self::factory()].into_iter()) .await .unwrap(); } diff --git a/src/config.rs b/src/config.rs index 37419868e6..8b4ba208a3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -56,26 +56,18 @@ pub struct Config { } impl Config { + /// Returns a new empty [`Builder`] for [`Config`]. + pub fn builder() -> Builder { + Builder::empty() + } + /// Attempts to locate and parse a `Config` located at either `path`, the /// `$QUILKIN_CONFIG` environment variable if set, the current directory, /// or the `/etc/quilkin` directory (on unix platforms only). Returns an /// error if the found configuration is invalid, or if no configuration /// could be found at any location. - pub fn find(path: Option<&str>) -> crate::Result { - const ENV_CONFIG_PATH: &str = "QUILKIN_CONFIG"; - const CONFIG_FILE: &str = "quilkin.yaml"; - - let config_env = std::env::var(ENV_CONFIG_PATH).ok(); - - let config_path = std::path::Path::new( - path.or_else(|| config_env.as_deref()) - .unwrap_or(CONFIG_FILE), - ) - .canonicalize()?; - - tracing::info!(path = %config_path.display(), "Found configuration file"); - - std::fs::File::open(&config_path) + pub fn find>(path: A) -> crate::Result { + std::fs::File::open(path) .or_else(|error| { if cfg!(unix) { std::fs::File::open("/etc/quilkin/quilkin.yaml") diff --git a/src/filters/capture_bytes.rs b/src/filters/capture_bytes.rs index b96ae89fc2..9a5fe089ce 100644 --- a/src/filters/capture_bytes.rs +++ b/src/filters/capture_bytes.rs @@ -94,6 +94,10 @@ impl FilterFactory for CaptureBytesFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? diff --git a/src/filters/capture_bytes/config.rs b/src/filters/capture_bytes/config.rs index 4c904b7a57..f5ce6d9d03 100644 --- a/src/filters/capture_bytes/config.rs +++ b/src/filters/capture_bytes/config.rs @@ -16,6 +16,7 @@ use std::convert::TryFrom; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::proto::quilkin::extensions::filters::capture_bytes::v1alpha1::{ @@ -26,7 +27,7 @@ use crate::map_proto_enum; use super::capture::{Capture, Prefix, Suffix}; -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, JsonSchema)] /// Strategy to apply for acquiring a set of bytes in the UDP packet pub enum Strategy { #[serde(rename = "PREFIX")] @@ -46,7 +47,7 @@ impl Strategy { } } -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, JsonSchema)] pub struct Config { #[serde(default)] pub strategy: Strategy, diff --git a/src/filters/compress.rs b/src/filters/compress.rs index 27a172659c..95c6e0f22f 100644 --- a/src/filters/compress.rs +++ b/src/filters/compress.rs @@ -158,6 +158,10 @@ impl FilterFactory for CompressFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? diff --git a/src/filters/compress/config.rs b/src/filters/compress/config.rs index 77d8530d7a..e361695c9b 100644 --- a/src/filters/compress/config.rs +++ b/src/filters/compress/config.rs @@ -16,6 +16,7 @@ use std::convert::TryFrom; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::compressor::{Compressor, Snappy}; @@ -25,7 +26,7 @@ use super::quilkin::extensions::filters::compress::v1alpha1::{ use crate::{filters::ConvertProtoConfigError, map_proto_enum}; /// The library to use when compressing. -#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize)] +#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize, JsonSchema)] #[non_exhaustive] pub enum Mode { // we only support one mode for now, but adding in the config option to @@ -49,7 +50,7 @@ impl Default for Mode { } /// Whether to do nothing, compress or decompress the packet. -#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize)] +#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize, JsonSchema)] pub enum Action { #[serde(rename = "DO_NOTHING")] DoNothing, @@ -65,7 +66,7 @@ impl Default for Action { } } -#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize)] +#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize, JsonSchema)] #[non_exhaustive] pub struct Config { #[serde(default)] diff --git a/src/filters/concatenate_bytes.rs b/src/filters/concatenate_bytes.rs index 1bb4c0f433..f61768fcc6 100644 --- a/src/filters/concatenate_bytes.rs +++ b/src/filters/concatenate_bytes.rs @@ -86,6 +86,10 @@ impl FilterFactory for ConcatBytesFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? diff --git a/src/filters/concatenate_bytes/config.rs b/src/filters/concatenate_bytes/config.rs index 329ac71f99..f7e574aeef 100644 --- a/src/filters/concatenate_bytes/config.rs +++ b/src/filters/concatenate_bytes/config.rs @@ -19,6 +19,7 @@ crate::include_proto!("quilkin.extensions.filters.concatenate_bytes.v1alpha1"); use std::convert::TryFrom; use base64_serde::base64_serde_type; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{filters::prelude::*, map_proto_enum}; @@ -29,7 +30,7 @@ pub use self::quilkin::extensions::filters::concatenate_bytes::v1alpha1::Concate base64_serde_type!(Base64Standard, base64::STANDARD); -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, JsonSchema)] pub enum Strategy { #[serde(rename = "APPEND")] Append, @@ -46,7 +47,7 @@ impl Default for Strategy { } /// Config represents a `ConcatenateBytes` filter configuration. -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, JsonSchema)] #[non_exhaustive] pub struct Config { /// Whether or not to `append` or `prepend` or `do nothing` on Filter `Read` @@ -56,7 +57,10 @@ pub struct Config { #[serde(default)] pub on_write: Strategy, - #[serde(with = "Base64Standard")] + #[serde( + deserialize_with = "Base64Standard::deserialize", + serialize_with = "Base64Standard::serialize" + )] pub bytes: Vec, } diff --git a/src/filters/debug.rs b/src/filters/debug.rs index 2ad86a40c9..63c1818df3 100644 --- a/src/filters/debug.rs +++ b/src/filters/debug.rs @@ -80,6 +80,10 @@ impl FilterFactory for DebugFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let config: Option<(_, Config)> = args .config @@ -99,7 +103,7 @@ impl FilterFactory for DebugFactory { } /// A Debug filter's configuration. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, schemars::JsonSchema)] pub struct Config { /// Identifier that will be optionally included with each log message. pub id: Option, diff --git a/src/filters/factory.rs b/src/filters/factory.rs index d36ecd6aa2..429193caae 100644 --- a/src/filters/factory.rs +++ b/src/filters/factory.rs @@ -58,6 +58,9 @@ pub trait FilterFactory: Sync + Send { /// `quilkin.extensions.filters.debug_filter.v1alpha1.Debug` fn name(&self) -> &'static str; + /// Returns the schema for the configuration of the [`Filter`]. + fn config_schema(&self) -> schemars::schema::RootSchema; + /// Returns a filter based on the provided arguments. fn create_filter(&self, args: CreateFilterArgs) -> Result; diff --git a/src/filters/firewall.rs b/src/filters/firewall.rs index bce31df397..c7eaf89b95 100644 --- a/src/filters/firewall.rs +++ b/src/filters/firewall.rs @@ -49,6 +49,10 @@ impl FilterFactory for FirewallFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? diff --git a/src/filters/firewall/config.rs b/src/filters/firewall/config.rs index 8e5776588d..3c5d38ca7c 100644 --- a/src/filters/firewall/config.rs +++ b/src/filters/firewall/config.rs @@ -17,6 +17,7 @@ use std::{convert::TryFrom, fmt, fmt::Formatter, net::SocketAddr, ops::Range}; use ipnetwork::IpNetwork; +use schemars::JsonSchema; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -29,7 +30,7 @@ use super::quilkin::extensions::filters::firewall::v1alpha1::{ /// Represents how a Firewall filter is configured for read and write /// operations. -#[derive(Clone, Deserialize, Debug, PartialEq, Serialize)] +#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, JsonSchema)] #[non_exhaustive] pub struct Config { pub on_read: Vec, @@ -37,7 +38,7 @@ pub struct Config { } /// Whether or not a matching [Rule] should Allow or Deny access -#[derive(Clone, Deserialize, Debug, PartialEq, Serialize)] +#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, JsonSchema)] pub enum Action { /// Matching rules will allow packets through. #[serde(rename = "ALLOW")] @@ -48,10 +49,11 @@ pub enum Action { } /// Combination of CIDR range, port range and action to take. -#[derive(Clone, Deserialize, Debug, PartialEq, Serialize)] +#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, JsonSchema)] pub struct Rule { pub action: Action, /// ipv4 or ipv6 CIDR address. + #[schemars(with = "String")] pub source: IpNetwork, pub ports: Vec, } @@ -98,7 +100,7 @@ pub enum PortRangeError { } /// Range of matching ports that are configured against a [Rule]. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, JsonSchema)] pub struct PortRange(Range); impl PortRange { diff --git a/src/filters/load_balancer.rs b/src/filters/load_balancer.rs index dc981c9911..82f2ef3fe6 100644 --- a/src/filters/load_balancer.rs +++ b/src/filters/load_balancer.rs @@ -50,6 +50,10 @@ impl FilterFactory for LoadBalancerFilterFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? diff --git a/src/filters/load_balancer/config.rs b/src/filters/load_balancer/config.rs index 453ed65a6a..19d123e630 100644 --- a/src/filters/load_balancer/config.rs +++ b/src/filters/load_balancer/config.rs @@ -18,6 +18,7 @@ crate::include_proto!("quilkin.extensions.filters.load_balancer.v1alpha1"); use std::convert::TryFrom; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use self::quilkin::extensions::filters::load_balancer::v1alpha1::load_balancer::Policy as ProtoPolicy; @@ -29,7 +30,7 @@ use crate::{filters::ConvertProtoConfigError, map_proto_enum}; pub use self::quilkin::extensions::filters::load_balancer::v1alpha1::LoadBalancer as ProtoConfig; /// The configuration for [`load_balancer`][super]. -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, JsonSchema)] #[non_exhaustive] pub struct Config { #[serde(default)] @@ -59,7 +60,7 @@ impl TryFrom for Config { /// Policy represents how a [`load_balancer`][super] distributes /// packets across endpoints. -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, JsonSchema)] pub enum Policy { /// Send packets to endpoints in turns. #[serde(rename = "ROUND_ROBIN")] diff --git a/src/filters/local_rate_limit.rs b/src/filters/local_rate_limit.rs index cbcc3c6a0f..9f046ecdf9 100644 --- a/src/filters/local_rate_limit.rs +++ b/src/filters/local_rate_limit.rs @@ -178,6 +178,10 @@ impl FilterFactory for LocalRateLimitFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = self .require_config(args.config)? @@ -199,7 +203,7 @@ impl FilterFactory for LocalRateLimitFactory { } /// Config represents a [self]'s configuration. -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, schemars::JsonSchema)] pub struct Config { /// The maximum number of packets allowed to be forwarded by the rate /// limiter in a given duration. diff --git a/src/filters/set.rs b/src/filters/set.rs index 45d33699a6..115b97d683 100644 --- a/src/filters/set.rs +++ b/src/filters/set.rs @@ -70,6 +70,19 @@ impl FilterSet { pub fn with(filters: impl IntoIterator) -> Self { Self::from_iter(filters) } + + /// Returns a [`DynFilterFactory`] if one matches `id`, otherwise returns + /// `None`. + pub fn get(&self, id: &str) -> Option<&DynFilterFactory> { + self.0.get(id) + } + + /// Returns a by reference iterator over the set of filters. + pub fn iter(&self) -> Iter { + Iter { + inner: self.0.iter(), + } + } } impl> From for FilterSet { @@ -113,3 +126,16 @@ impl Iterator for IntoIter { self.inner.next().map(|(_, v)| v) } } + +/// Iterator over a set of [`DynFilterFactory`]s. +pub struct Iter<'r> { + inner: std::collections::hash_map::Iter<'r, &'static str, DynFilterFactory>, +} + +impl<'r> Iterator for Iter<'r> { + type Item = &'r DynFilterFactory; + + fn next(&mut self) -> Option { + self.inner.next().map(|(_, v)| v) + } +} diff --git a/src/filters/token_router.rs b/src/filters/token_router.rs index ad52972bc7..6861341108 100644 --- a/src/filters/token_router.rs +++ b/src/filters/token_router.rs @@ -72,6 +72,10 @@ impl FilterFactory for TokenRouterFactory { NAME } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Config) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, config) = args .config @@ -142,7 +146,7 @@ impl Filter for TokenRouter { } } -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, schemars::JsonSchema)] #[serde(default)] pub struct Config { /// the key to use when retrieving the token from the Filter's dynamic metadata diff --git a/src/lib.rs b/src/lib.rs index 50662c32d6..8e979e6cca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ pub type Result = std::result::Result; pub use self::{ config::Config, proxy::{Builder, PendingValidation, Server, Validated}, - runner::{run, run_with_config}, + runner::run, }; pub use quilkin_macros::include_proto; diff --git a/src/main.rs b/src/main.rs index 101888410d..5da06ea7ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,15 +14,54 @@ * limitations under the License. */ -use clap::{App, AppSettings, Arg}; -use std::sync::Arc; +use std::path::PathBuf; use tracing::info; const VERSION: &str = env!("CARGO_PKG_VERSION"); +#[derive(clap::Parser)] +struct Cli { + #[clap( + short, + long, + env = "QUILKIN_CONFIG", + default_value = "quilkin.yaml", + help = "The YAML configuration file." + )] + config: PathBuf, + #[clap( + short, + long, + env, + help = "Whether Quilkin will report any results to stdout/stderr." + )] + quiet: bool, + #[clap(subcommand)] + command: Commands, +} + +#[derive(clap::Subcommand)] +enum Commands { + Run, + GenerateConfigSchema { + #[clap( + short, + long, + default_value = ".", + help = "The directory to write configuration files." + )] + output_directory: PathBuf, + #[clap( + min_values = 1, + default_value = "all", + help = "A list of one or more filter IDs to generate or 'all' to generate all available filter schemas." + )] + filter_ids: Vec, + }, +} + #[tokio::main] async fn main() -> quilkin::Result<()> { - tracing_subscriber::fmt().json().with_target(false).init(); stable_eyre::install()?; let version: std::borrow::Cow<'static, str> = if cfg!(debug_assertions) { format!("{VERSION}+debug").into() @@ -30,33 +69,57 @@ async fn main() -> quilkin::Result<()> { VERSION.into() }; - let config_arg = Arg::new("config") - .short('c') - .long("config") - .value_name("CONFIG") - .help("The YAML configuration file") - .takes_value(true); - - let cli = App::new(clap::crate_name!()) - .version(&*version) - .about(clap::crate_description!()) - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - App::new("run") - .about("Start Quilkin process.") - .arg(config_arg.clone()), - ) - .get_matches(); + let cli = ::parse(); + + if !cli.quiet { + tracing_subscriber::fmt().json().with_target(false).init(); + } info!(version = &*version, "Starting Quilkin"); - match cli.subcommand() { - Some(("run", matches)) => { - let config = quilkin::config::Config::find(matches.value_of("config")).map(Arc::new)?; + match cli.command { + Commands::Run => { + let config = quilkin::config::Config::find(cli.config)?; - quilkin::run_with_config(config, vec![]).await + quilkin::run(config, vec![]).await } - Some((cmd, _)) => panic!("Unimplemented subcommand: {}", cmd), - None => unreachable!(), + Commands::GenerateConfigSchema { + output_directory, + filter_ids, + } => { + let set = quilkin::filters::FilterSet::default(); + type SchemaIterator<'r> = + Box + 'r>; + + let schemas = (filter_ids.len() == 1 && filter_ids[0].to_lowercase() == "all") + .then(|| { + Box::new( + set.iter() + .map(|factory| (factory.name(), factory.config_schema())), + ) as SchemaIterator + }) + .unwrap_or_else(|| { + Box::new(filter_ids.iter().filter_map(|id| { + let item = set.get(id); + + if item.is_none() { + tracing::error!("{id} not found in filter set."); + } + + item.map(|item| (item.name(), item.config_schema())) + })) as SchemaIterator + }); + + for (id, schema) in schemas { + let mut path = output_directory.join(id); + path.set_extension("yaml"); + + tracing::info!("Writing {id} schema to {}", path.display()); + + std::fs::write(path, serde_yaml::to_string(&schema)?)?; + } + + Ok(()) + } } } diff --git a/src/runner.rs b/src/runner.rs index 61428c7129..521dff7513 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -29,22 +29,16 @@ use crate::{ #[cfg(doc)] use crate::filters::FilterFactory; -/// Calls [`run`] with the [`Config`] found by [`Config::find`] and the -/// default [`FilterSet`]. -pub async fn run(filter_factories: impl IntoIterator) -> Result<()> { - run_with_config(Config::find(None).map(Arc::new)?, filter_factories).await -} - /// Start and run a proxy. Any passed in [`FilterFactory`]s are included /// alongside the default filter factories. -pub async fn run_with_config( - config: Arc, +pub async fn run( + config: Config, filter_factories: impl IntoIterator, ) -> Result<()> { let span = span!(Level::INFO, "source::run"); let _enter = span.enter(); - let server = Builder::from(config) + let server = Builder::from(Arc::new(config)) .with_filter_registry(FilterRegistry::new(FilterSet::default_with( filter_factories.into_iter(), ))) diff --git a/src/test_utils.rs b/src/test_utils.rs index a85333908a..aae62b108a 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -34,6 +34,10 @@ impl FilterFactory for TestFilterFactory { "TestFilter" } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for_value!(serde_json::Value::Null) + } + fn create_filter(&self, _: CreateFilterArgs) -> Result { Ok(Self::create_empty_filter()) } diff --git a/src/xds/listener.rs b/src/xds/listener.rs index 8819ae694c..306c034030 100644 --- a/src/xds/listener.rs +++ b/src/xds/listener.rs @@ -203,7 +203,7 @@ mod tests { // A simple filter that will be used in the following tests. // It appends a string to each payload. const APPEND_TYPE_URL: &str = "filter.append"; - #[derive(Clone, PartialEq, Serialize, Deserialize)] + #[derive(Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)] pub struct Append { pub value: Option, } @@ -244,6 +244,10 @@ mod tests { APPEND_TYPE_URL } + fn config_schema(&self) -> schemars::schema::RootSchema { + schemars::schema_for!(Append) + } + fn create_filter(&self, args: CreateFilterArgs) -> Result { let (config_json, filter) = args .config