From 02a17fe889c9b10466d75c156b77513d332d65de Mon Sep 17 00:00:00 2001 From: Erin Power Date: Mon, 31 Jan 2022 19:22:48 +0100 Subject: [PATCH] Add auto-generation of JSON schema. This commit adds support for Quilkin to generate JSON schema for any of the included filters, and adds some glue to make it so that these schemas are automatically included in the mdBook documentation. --- Cargo.toml | 3 +- docs/book.toml | 6 + docs/preprocessor.sh | 6 + docs/src/filters/capture_bytes.md | 26 +---- docs/src/filters/compress.md | 38 ++----- docs/src/filters/concatenate_bytes.md | 18 +-- docs/src/filters/debug.md | 6 +- docs/src/filters/firewall.md | 36 +----- docs/src/filters/load_balancer.md | 11 +- docs/src/filters/local_rate_limit.md | 16 +-- docs/src/filters/token_router.md | 7 +- docs/src/filters/writing_custom_filters.md | 10 ++ examples/quilkin-filter-example/Cargo.toml | 1 + examples/quilkin-filter-example/src/main.rs | 9 +- src/config.rs | 22 ++-- src/filters/capture_bytes.rs | 4 + src/filters/capture_bytes/config.rs | 5 +- src/filters/compress.rs | 4 + src/filters/compress/config.rs | 7 +- src/filters/concatenate_bytes.rs | 4 + src/filters/concatenate_bytes/config.rs | 10 +- src/filters/debug.rs | 6 +- src/filters/factory.rs | 3 + src/filters/firewall.rs | 4 + src/filters/firewall/config.rs | 10 +- src/filters/load_balancer.rs | 4 + src/filters/load_balancer/config.rs | 5 +- src/filters/local_rate_limit.rs | 6 +- src/filters/set.rs | 26 +++++ src/filters/token_router.rs | 6 +- src/lib.rs | 2 +- src/main.rs | 115 +++++++++++++++----- src/runner.rs | 12 +- src/test_utils.rs | 4 + src/xds/listener.rs | 6 +- 35 files changed, 242 insertions(+), 216 deletions(-) create mode 100755 docs/preprocessor.sh 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