diff --git a/Cargo.lock b/Cargo.lock index 84babfd..64e9a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,20 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -128,6 +142,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "arc-bytes" version = "0.3.5" @@ -231,6 +251,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "2.6.0" @@ -492,6 +527,7 @@ dependencies = [ "chrono", "clap", "directories", + "jsonschema", "rayon", "regex", "reqwest", @@ -625,6 +661,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-where" version = "1.2.7" @@ -760,6 +805,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -831,6 +887,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "fs2" version = "0.4.3" @@ -1012,7 +1078,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] @@ -1221,6 +1287,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.11.0" @@ -1245,6 +1320,41 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2eef4e82b548e08ac880d307c8e8838b45f497a08d3202f3b26c9debaed8058" +dependencies = [ + "ahash 0.8.11", + "anyhow", + "base64", + "bytecount", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid-simd", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.159" @@ -1327,6 +1437,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1392,6 +1508,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -1401,6 +1527,82 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1491,6 +1693,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9cc9f18ab4bad1e01726bda1259feb8f11e5e76308708a966b4c0136e9db34c" +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + [[package]] name = "p256" version = "0.13.2" @@ -1606,6 +1814,12 @@ dependencies = [ "serde", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2264,6 +2478,36 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2485,6 +2729,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2497,6 +2758,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 0315b78..d28045c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ bonsaidb = { version = "~0.5", features = ["local"] } chrono = "0.4.23" clap = { version = "4.1.8", features = ["derive"] } directories = "5.0.1" +jsonschema = "0.20.0" rayon = "1.7.0" regex = "1.7.1" reqwest = { version = "0.12", features = ["blocking", "json"] } diff --git a/README.md b/README.md index 61b553c..69c6d87 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,13 @@ corrator For additional options, see `corrator --help`. +### Config via URL + +Alternatively, if you want to consume JSON from a URL (e.g., you generate the config on the fly) you +can use the `-u` flag to provide a URL. + +You can find the reference schema [here](src/config.schema.json). + ## Configuring Corrator The heart of corrator is a configuration directory featuring two files: diff --git a/examples/config.json b/examples/config.json new file mode 100644 index 0000000..f5a05e9 --- /dev/null +++ b/examples/config.json @@ -0,0 +1,44 @@ +{ + "applications": { + "bash": { + "version_regex": "GNU bash, version (?P[0-9.]+)", + "version_command": "bash --version" + }, + "busybox": { + "version_regex": "BusyBox v(?P[0-9.]+)", + "version_command": "/bin/busybox --help" + }, + "grep": { + "version_regex": "grep \\(GNU grep\\) (?P[0-9.]+)", + "version_command": "grep --version" + }, + "ubuntu": { + "version_regex": "PRETTY_NAME=\"Ubuntu (?P[0-9.]{5}).*\"", + "version_command": "cat /etc/os-release", + "eol": { + "product_name": "ubuntu", + "version_regex": "^[0-9]{2}\\.[0-9]{2}" + } + } + }, + "containers": { + "ubuntu": { + "path": "ubuntu", + "apps": [ + "bash", + "grep", + "ubuntu" + ], + "tags": [ + "testing", + "tags" + ] + }, + "alpine": { + "path": "alpine", + "apps": [ + "busybox" + ] + } + } +} diff --git a/examples/containers.toml b/examples/containers.toml index dcb4b62..837a392 100644 --- a/examples/containers.toml +++ b/examples/containers.toml @@ -5,4 +5,4 @@ tags = [ "testing", "tags" ] [alpine] path = "alpine" -apps = [ "busybox" ] \ No newline at end of file +apps = [ "busybox" ] diff --git a/src/config.schema.json b/src/config.schema.json new file mode 100644 index 0000000..2ca5f5a --- /dev/null +++ b/src/config.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://corrator.rs/config.schema.json", + "title": "Corrator Config", + "description": "A reference config for applications and containers", + "type": "object", + "properties": { + "containers": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "apps": { + "type": "array", + "items": { "type": "string" } + }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false, + "required": [ "path", "apps" ] + } + }, + "applications": { + "additionalProperties": { + "type": "object", + "properties": { + "version_regex": { "type": "string" }, + "version_command": { "type": "string" }, + "eol": { + "type": "object", + "properties": { + "product_name": { "type": "string" }, + "version_regex": { "type": "string" } + }, + "additionalProperties": false, + "required": ["product_name", "version_regex" ] + } + }, + "additionalProperties": false, + "required": [ "version_regex", "version_command" ] + } + } + }, + "additionalProperties": false, + "required": [ "containers", "applications" ] +} + diff --git a/src/main.rs b/src/main.rs index 0c3259d..ab82e98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,46 @@ use clap::Parser; -use corrator::{Config, Options}; +use core::panic; +use corrator::{ApplicationMap, Config, ContainerMap, Options}; use directories::ProjectDirs; -use serde::de::DeserializeOwned; -use std::{fmt::Write, fs, path::Path}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{fmt::Write, fs, path::Path, process::exit}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(short, long, default_value_t = default_config_path())] + /// Specify a directory to load toml files from + #[arg(short = 'd', long, default_value_t = default_config_path(), conflicts_with = "config_url", help_heading="Config Settings")] config_directory: String, - #[arg(short, long, default_value = "text", value_parser = ["text", "json"])] + /// URL to fetch a JSON formatted config + /// + /// See corrator github repo for a JSON schema + #[arg( + short = 'u', + long, + conflicts_with = "config_directory", + help_heading = "Config Settings" + )] + config_url: Option, + + /// Validate config URL only and then exit + /// + /// Program will exit with a failing status if validation is not successful + #[arg( + short = 'v', + long, + requires = "config_url", + help_heading = "Config Settings" + )] + validate_config_url: bool, + + #[arg(short, long, default_value = "text", value_parser = ["text", "json"], help_heading = "Output")] format: String, /// Writes output to a file at this given path if provided /// /// Will write to stdout if this option is not used - #[arg(short, long)] + #[arg(short, long, help_heading = "Output")] output: Option, /// Enable flag to remove images after version queries @@ -24,15 +48,15 @@ struct Args { clean: bool, /// Filter containers by tag; can be used multiple times - #[arg(short, long)] + #[arg(short, long, help_heading = "Filtering")] tag: Option>, /// Filter function for tagging - #[arg(long, value_enum, default_value_t = corrator::FilterFunction::Any)] + #[arg(long, value_enum, default_value_t = corrator::FilterFunction::Any, help_heading = "Filtering")] filter: corrator::FilterFunction, /// Filter containers by name; can be used multiple times - #[arg(short, long)] + #[arg(short, long, help_heading = "Filtering")] name: Option>, /// Do not clear the EOL cache before querying apps @@ -51,6 +75,12 @@ impl From<&Args> for Options { } } +#[derive(Serialize, Deserialize)] +struct JsonConfig { + containers: ContainerMap, + applications: ApplicationMap, +} + fn default_config_path() -> String { ProjectDirs::from("rs", "", "corrator") .expect("could not get project directory") @@ -64,12 +94,39 @@ fn main() { let args = Args::parse(); let options = Options::from(&args); - let config = Path::new(&args.config_directory); - let config = Config::new( - parse_config_file(config, "containers.toml"), - parse_config_file(config, "applications.toml"), - options, - ); + let config = match &args.config_url { + Some(x) => { + let schema = include_str!("config.schema.json"); + let schema = serde_json::from_str(schema).expect("Could not read json schema!"); + + let validator = + jsonschema::validator_for(&schema).expect("Could not initialize json validator!"); + let config = get_config_from_url(x); + let is_valid = validator.is_valid(&config); + + if args.validate_config_url { + let exit_code = if is_valid { 0 } else { 1 }; + println!("is valid: {}", is_valid); + exit(exit_code); + } + + if !is_valid { + panic!("Unable to validate the fetched config json!") + } + + let json_config: JsonConfig = parse_config_url(x); + Config::new(json_config.containers, json_config.applications, options) + } + + None => { + let directory = Path::new(&args.config_directory); + Config::new( + parse_config_file(directory, "containers.toml"), + parse_config_file(directory, "applications.toml"), + options, + ) + } + }; if !args.keep_eol_cache { corrator::end_of_life::cache::clear().expect("Unable to clear EOL cache"); @@ -90,6 +147,30 @@ fn main() { } } +fn get_config_from_url(url: &String) -> serde_json::Value { + let response = reqwest::blocking::get(url).expect("Unable to reach config"); + + if response.status() != 200 { + eprintln!("Bad response from {}: {}", url, response.status()); + panic!(); + } + + response.json().expect("Unable to parse config from URL") +} + +fn parse_config_url(url: &String) -> T { + let response = reqwest::blocking::get(url).expect("Unable to reach config"); + + if response.status() != 200 { + eprintln!("Bad response from {}: {}", url, response.status()); + panic!(); + } + + response + .json::() + .expect("Unable to parse URL into config") +} + fn parse_config_file(config_directory: &Path, file_name: &str) -> T { let config_directory = config_directory.join(file_name);