diff --git a/Cargo.lock b/Cargo.lock index ac30583a..b8f3fcbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,19 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -17,6 +30,17 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -85,6 +109,40 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-trait" version = "0.1.91" @@ -102,6 +160,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "axum" version = "0.8.9" @@ -175,6 +239,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "base64" version = "0.22.1" @@ -368,6 +443,40 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + [[package]] name = "dashmap" version = "6.2.1" @@ -413,6 +522,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -430,6 +560,24 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "either" version = "1.17.0" @@ -445,6 +593,35 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "env_filter" version = "2.0.0" @@ -482,6 +659,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fact" version = "0.4.0-dev" @@ -491,6 +688,7 @@ dependencies = [ "clap", "env_logger", "fact-api", + "fact-core", "fact-ebpf", "glob", "globset", @@ -537,6 +735,19 @@ dependencies = [ "tonic-prost-build", ] +[[package]] +name = "fact-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "log", + "schemars", + "serde", + "tokio", + "yaml-rust2", +] + [[package]] name = "fact-ebpf" version = "0.1.0" @@ -548,6 +759,24 @@ dependencies = [ "serde", ] +[[package]] +name = "fact-operator" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "fact-core", + "futures", + "k8s-openapi", + "kube", + "log", + "schemars", + "serde", + "serde_json", + "tokio", + "yaml-rust2", +] + [[package]] name = "fastrand" version = "2.5.0" @@ -608,6 +837,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.33" @@ -676,6 +919,7 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -731,6 +975,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "governor" version = "0.10.4" @@ -754,6 +1010,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "granit-parser" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" +dependencies = [ + "arraydeque", + "smallvec", +] + [[package]] name = "h2" version = "0.4.15" @@ -824,6 +1090,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "http" version = "1.4.2" @@ -891,6 +1168,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-openssl" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527d4d619ca2c2aafa31ec139a3d1d60bf557bf7578a1f20f743637eccd9ca19" +dependencies = [ + "http", + "hyper", + "hyper-util", + "linked_hash_set", + "once_cell", + "openssl", + "openssl-sys", + "parking_lot", + "pin-project", + "tower-layer", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1025,6 +1321,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1139,6 +1441,162 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonpath-rust" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62a190c17bf3c6e3a2a676b522edf09d3ba94046e3cdb85fb9d6f09234fdc82" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "k8s-openapi" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c6922f6afe80418dd6019818af5d0d34584c371780ff09b9752370c25b4abb" +dependencies = [ + "base64", + "jiff", + "schemars", + "serde", + "serde_json", +] + +[[package]] +name = "kube" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208d7fe1380066abb194812a8a8e303cb896a33bb3d7b3177b71bd03ff39bf18" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e940a73033a7c5c7918b5ece7851d84571b58be25e937198da37fa13f116d3" +dependencies = [ + "base64", + "bytes", + "either", + "futures", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-openssl", + "hyper-timeout", + "hyper-util", + "jiff", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "openssl", + "pem", + "secrecy", + "serde", + "serde-saphyr", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d2353c118cf3462c352ee0b5bd5b0cf17990af456dfd8662d136ff9812eeb4" +dependencies = [ + "derive_more", + "form_urlencoded", + "http", + "jiff", + "json-patch", + "k8s-openapi", + "schemars", + "serde", + "serde-value", + "serde_json", + "thiserror", +] + +[[package]] +name = "kube-derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92141c19e1fa83bf91633c1234c66b03375c29aa8ca5486331ddb47e3a3da9c0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "kube-runtime" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb064ef71c7bea55e934a443960da9e9ec31fbb2ba63e47e4aeca32603d5cc7" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "backon", + "educe", + "futures", + "hashbrown 0.16.1", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "libc" version = "0.2.189" @@ -1155,6 +1613,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1240,6 +1713,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -1256,6 +1735,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.39.1" @@ -1393,6 +1881,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1416,12 +1919,64 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" +dependencies = [ + "pest", +] + [[package]] name = "petgraph" version = "0.8.3" @@ -1713,6 +2268,26 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "regex" version = "1.13.1" @@ -1779,6 +2354,15 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1807,12 +2391,46 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.3", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -1836,6 +2454,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -1846,6 +2470,35 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" +dependencies = [ + "ahash", + "annotate-snippets", + "base64", + "encoding_rs_io", + "getrandom 0.3.4", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -1866,6 +2519,17 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "serde_json" version = "1.0.151" @@ -2089,6 +2753,7 @@ dependencies = [ "futures-sink", "libc", "pin-project-lite", + "slab", "tokio", ] @@ -2185,15 +2850,18 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ + "base64", "bitflags 2.13.1", "bytes", "futures-util", "http", "http-body", + "mime", "pin-project-lite", "tower", "tower-layer", "tower-service", + "tracing", "url", ] @@ -2215,6 +2883,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2246,6 +2915,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicase" version = "2.9.0" @@ -2258,6 +2933,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "url" version = "2.5.8" @@ -2299,6 +2980,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -2522,6 +3209,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index c4f525a2..6686bf89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,9 @@ resolver = "2" members = [ "fact", "fact-api", + "fact-core", "fact-ebpf", + "fact-operator", ] default-members = ["fact"] @@ -31,6 +33,7 @@ prometheus-client = { version = "0.25.0", default-features = false } prost = "0.14.0" prost-types = "0.14.0" rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] } +schemars = { version = "1" } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" shlex = "2.0.1" diff --git a/Containerfile b/Containerfile index e296dc64..7ab96155 100644 --- a/Containerfile +++ b/Containerfile @@ -43,10 +43,11 @@ ARG FACT_VERSION ARG CARGO_ARGS="" RUN --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/app/target \ - cargo build --release $CARGO_ARGS && \ - cp target/release/fact fact + cargo build --release $CARGO_ARGS --all && \ + cp target/release/fact fact && \ + cp target/release/fact-operator fact-operator -FROM ubi-micro-base +FROM ubi-micro-base AS fact ARG FACT_VERSION LABEL name="fact" \ @@ -65,3 +66,23 @@ COPY LICENSE-APACHE LICENSE-MIT LICENSE-GPL2 /licenses/ RUN update-crypto-policies --set DEFAULT:PQ ENTRYPOINT ["fact"] + +FROM ubi-micro-base AS fact-operator + +ARG FACT_VERSION +LABEL name="fact-operator" \ + vendor="StackRox" \ + maintainer="support@stackrox.com" \ + summary="File activity operator" \ + description="This image supports an operator for deploying the file activity daemonset" \ + io.stackrox.fact-operator.version="${FACT_VERSION}" + +COPY --from=package_installer /out/ / + +COPY --from=build /app/fact-operator /usr/local/bin + +COPY LICENSE-APACHE LICENSE-MIT LICENSE-GPL2 /licenses/ + +RUN update-crypto-policies --set DEFAULT:PQ + +ENTRYPOINT ["fact-operator"] diff --git a/Makefile b/Makefile index ef94eb3d..5859c680 100644 --- a/Makefile +++ b/Makefile @@ -9,21 +9,32 @@ version: image-name: @echo "$(FACT_IMAGE_NAME)" +operator-name: + @echo "$(FACT_OPERATOR_NAME)" + mock-server: make -C mock-server +BUILD_TARGET ?= fact +IMAGE_NAME ?= $(FACT_IMAGE_NAME) + image: $(DOCKER) build \ -f Containerfile \ --build-arg FACT_VERSION=$(FACT_VERSION) \ --build-arg RUST_VERSION=$(RUST_VERSION) \ --build-arg CARGO_ARGS="$(CARGO_ARGS)" \ - -t $(FACT_IMAGE_NAME) \ + --target $(BUILD_TARGET) \ + -t $(IMAGE_NAME) \ $(CURDIR) image-otel: CARGO_ARGS = --features otel image-otel: image +operator: BUILD_TARGET = fact-operator +operator: IMAGE_NAME = $(FACT_OPERATOR_NAME) +operator: image + licenses:THIRD_PARTY_LICENSES.html THIRD_PARTY_LICENSES.html:Cargo.lock @@ -58,4 +69,5 @@ format: make -C fact-ebpf format ruff format tests/ -.PHONY: tag mock-server integration-tests image image-otel image-name licenses coverage lint clean +.PHONY: tag mock-server integration-tests image image-otel image-name +.PHONY: operator operator-name licenses coverage lint clean diff --git a/constants.mk b/constants.mk index c624c597..6e2f9427 100644 --- a/constants.mk +++ b/constants.mk @@ -4,6 +4,8 @@ FACT_TAG ?= $(shell git describe --always --tags --abbrev=10 --dirty) FACT_VERSION ?= $(FACT_TAG) FACT_REGISTRY ?= quay.io/stackrox-io/fact FACT_IMAGE_NAME ?= $(FACT_REGISTRY):$(FACT_TAG) +FACT_OPERATOR_REGISTRY ?= quay.io/stackrox-io/fact-operator +FACT_OPERATOR_NAME ?= $(FACT_OPERATOR_REGISTRY):$(FACT_TAG) CLANG_FMT ?= $(shell which clang-format) diff --git a/fact-core/Cargo.toml b/fact-core/Cargo.toml new file mode 100644 index 00000000..3cbdc801 --- /dev/null +++ b/fact-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fact-core" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +log = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } +yaml-rust2 = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } diff --git a/fact/build.rs b/fact-core/build.rs similarity index 100% rename from fact/build.rs rename to fact-core/build.rs diff --git a/fact/src/config/mod.rs b/fact-core/src/config/mod.rs similarity index 79% rename from fact/src/config/mod.rs rename to fact-core/src/config/mod.rs index ab8e9d6a..d8637444 100644 --- a/fact/src/config/mod.rs +++ b/fact-core/src/config/mod.rs @@ -11,6 +11,8 @@ use std::{ use anyhow::{Context, bail}; use clap::Parser; use log::info; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use yaml_rust2::{Yaml, YamlLoader, yaml}; pub mod reloader; @@ -31,12 +33,25 @@ fn yaml_to_duration_secs(v: &Yaml) -> Option { .map(Duration::from_secs_f64) } -#[derive(Debug, Default, PartialEq, Clone)] +fn duration_to_yaml(v: Duration) -> Yaml { + let v = v.as_secs_f64(); + if v.fract() != 0.0 { + Yaml::Real(v.to_string()) + } else { + Yaml::Integer(v as i64) + } +} + +#[derive(Debug, Default, PartialEq, Clone, JsonSchema, Serialize, Deserialize)] pub struct FactConfig { paths: Option>, + #[serde(default)] pub grpc: GrpcConfig, + #[serde(default)] pub otel: OTelConfig, + #[serde(default)] pub endpoint: EndpointConfig, + #[serde(default)] pub bpf: BpfConfig, skip_pre_flight: Option, json: Option, @@ -144,10 +159,75 @@ impl FactConfig { self.replay.as_deref() } - #[cfg(test)] pub fn set_paths(&mut self, paths: Vec) { self.paths = Some(paths); } + + pub fn to_yaml(&self) -> Yaml { + let mut config = yaml::Hash::new(); + + if let Some(paths) = &self.paths { + let paths = paths + .iter() + .filter_map(|p| p.to_str().map(|p| Yaml::String(p.to_string()))) + .collect::>(); + config.insert(Yaml::String("paths".into()), Yaml::Array(paths)); + } + + if let Some(grpc) = self.grpc.to_yaml() { + config.insert(Yaml::String("grpc".into()), grpc); + } + + if let Some(otel) = self.otel.to_yaml() { + config.insert(Yaml::String("otel".into()), otel); + } + + if let Some(endpoint) = self.endpoint.to_yaml() { + config.insert(Yaml::String("endpoint".into()), endpoint); + } + + if let Some(bpf) = self.bpf.to_yaml() { + config.insert(Yaml::String("bpf".into()), bpf); + } + + if let Some(skip_pre_flight) = self.skip_pre_flight { + config.insert( + Yaml::String("skip_pre_flight".into()), + Yaml::Boolean(skip_pre_flight), + ); + } + + if let Some(json) = self.json { + config.insert(Yaml::String("json".into()), Yaml::Boolean(json)); + } + + if let Some(hotreload) = self.hotreload { + config.insert(Yaml::String("hotreload".into()), Yaml::Boolean(hotreload)); + } + + if let Some(scan_interval) = self.scan_interval { + config.insert( + Yaml::String("scan_interval".into()), + duration_to_yaml(scan_interval), + ); + } + + if let Some(rate_limit) = self.rate_limit { + config.insert( + Yaml::String("rate_limit".into()), + Yaml::Integer(rate_limit as i64), + ); + } + + if let Some(replay) = &self.replay { + config.insert( + Yaml::String("replay".into()), + Yaml::String(replay.to_string_lossy().to_string()), + ); + } + + Yaml::Hash(config) + } } impl TryFrom<&str> for FactConfig { @@ -228,10 +308,8 @@ impl TryFrom> for FactConfig { }; config.json = Some(json); } - "bpf" => { - let Some(bpf) = v.as_hash() else { - bail!("bpf section has incorrect type: {v:#?}"); - }; + "bpf" if v.is_hash() => { + let bpf = v.as_hash().unwrap(); config.bpf = BpfConfig::try_from(bpf)?; } "hotreload" => { @@ -271,7 +349,7 @@ impl TryFrom> for FactConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct EndpointConfig { address: Option, expose_metrics: Option, @@ -305,6 +383,37 @@ impl EndpointConfig { pub fn health_check(&self) -> bool { self.health_check.unwrap_or(false) } + + fn to_yaml(&self) -> Option { + let mut endpoint = yaml::Hash::new(); + + if let Some(address) = self.address { + endpoint.insert( + Yaml::String("address".into()), + Yaml::String(address.to_string()), + ); + } + + if let Some(expose_metrics) = self.expose_metrics { + endpoint.insert( + Yaml::String("expose_metrics".into()), + Yaml::Boolean(expose_metrics), + ); + } + + if let Some(health_check) = self.health_check { + endpoint.insert( + Yaml::String("health_check".into()), + Yaml::Boolean(health_check), + ); + } + + if !endpoint.is_empty() { + Some(Yaml::Hash(endpoint)) + } else { + None + } + } } impl TryFrom<&yaml::Hash> for EndpointConfig { @@ -348,7 +457,7 @@ impl TryFrom<&yaml::Hash> for EndpointConfig { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, JsonSchema, Serialize, Deserialize)] pub struct BackoffConfig { initial: Option, max: Option, @@ -395,6 +504,42 @@ impl BackoffConfig { pub fn retries(&self) -> u64 { self.retries_max.unwrap_or(10) } + + fn to_yaml(&self) -> Option { + let mut backoff = yaml::Hash::new(); + + if let Some(initial) = self.initial { + backoff.insert(Yaml::String("initial".into()), duration_to_yaml(initial)); + } + + if let Some(max) = self.max { + backoff.insert(Yaml::String("max".into()), duration_to_yaml(max)); + } + + if let Some(jitter) = self.jitter { + backoff.insert(Yaml::String("jitter".into()), Yaml::Boolean(jitter)); + } + + if let Some(multiplier) = self.multiplier { + backoff.insert( + Yaml::String("multiplier".into()), + Yaml::Real(multiplier.to_string()), + ); + } + + if let Some(retries_max) = self.retries_max { + backoff.insert( + Yaml::String("retries".into()), + Yaml::Integer(retries_max as i64), + ); + } + + if !backoff.is_empty() { + Some(Yaml::Hash(backoff)) + } else { + None + } + } } impl TryFrom<&yaml::Hash> for BackoffConfig { @@ -448,7 +593,7 @@ impl TryFrom<&yaml::Hash> for BackoffConfig { } } -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone, JsonSchema, Serialize, Deserialize)] pub struct GrpcConfig { url: Option, certs: Option, @@ -475,6 +620,30 @@ impl GrpcConfig { pub fn certs(&self) -> Option<&Path> { self.certs.as_deref() } + + fn to_yaml(&self) -> Option { + let mut grpc = yaml::Hash::new(); + + if let Some(url) = &self.url { + grpc.insert(Yaml::String("url".into()), Yaml::String(url.clone())); + } + + if let Some(certs) = &self.certs + && let Some(certs) = certs.to_str() + { + grpc.insert(Yaml::String("certs".into()), Yaml::String(certs.into())); + } + + if let Some(backoff) = self.backoff.to_yaml() { + grpc.insert(Yaml::String("backoff".into()), backoff); + } + + if !grpc.is_empty() { + Some(Yaml::Hash(grpc)) + } else { + None + } + } } impl TryFrom<&yaml::Hash> for GrpcConfig { @@ -514,7 +683,7 @@ impl TryFrom<&yaml::Hash> for GrpcConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct OTelConfig { endpoint: Option, } @@ -529,6 +698,23 @@ impl OTelConfig { pub fn endpoint(&self) -> Option<&str> { self.endpoint.as_deref() } + + fn to_yaml(&self) -> Option { + let mut otel = yaml::Hash::new(); + + if let Some(endpoint) = &self.endpoint { + otel.insert( + Yaml::String("endpoint".into()), + Yaml::String(endpoint.clone()), + ); + } + + if !otel.is_empty() { + Some(Yaml::Hash(otel)) + } else { + None + } + } } impl TryFrom<&yaml::Hash> for OTelConfig { @@ -556,7 +742,7 @@ impl TryFrom<&yaml::Hash> for OTelConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct BpfConfig { ringbuf_size: Option, inodes_max: Option, @@ -589,6 +775,40 @@ impl BpfConfig { pub fn program_is_enabled(&self, name: &str) -> bool { self.programs.get(name).map(|c| c.enabled()).unwrap_or(true) } + + fn to_yaml(&self) -> Option { + let mut bpf = yaml::Hash::new(); + + if let Some(ringbuf_size) = self.ringbuf_size { + bpf.insert( + Yaml::String("ringbuf_size".into()), + Yaml::Integer(ringbuf_size as i64), + ); + } + + if let Some(inodes_max) = self.inodes_max { + bpf.insert( + Yaml::String("inodes_max".into()), + Yaml::Integer(inodes_max as i64), + ); + } + + if !self.programs.is_empty() { + let programs = self + .programs + .iter() + .filter_map(|(k, v)| v.to_yaml().map(|v| (Yaml::String(k.into()), v))) + .collect(); + + bpf.insert(Yaml::String("programs".into()), Yaml::Hash(programs)); + } + + if !bpf.is_empty() { + Some(Yaml::Hash(bpf)) + } else { + None + } + } } impl TryFrom<&yaml::Hash> for BpfConfig { @@ -652,7 +872,7 @@ impl TryFrom<&yaml::Hash> for BpfConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct BpfProgConfig { pub enabled: Option, } @@ -667,6 +887,20 @@ impl BpfProgConfig { pub fn enabled(&self) -> bool { self.enabled.unwrap_or(true) } + + fn to_yaml(&self) -> Option { + let mut prog = yaml::Hash::new(); + + if let Some(enabled) = self.enabled { + prog.insert(Yaml::String("enabled".into()), Yaml::Boolean(enabled)); + } + + if !prog.is_empty() { + Some(Yaml::Hash(prog)) + } else { + None + } + } } impl TryFrom<&yaml::Hash> for BpfProgConfig { diff --git a/fact/src/config/reloader/mod.rs b/fact-core/src/config/reloader/mod.rs similarity index 100% rename from fact/src/config/reloader/mod.rs rename to fact-core/src/config/reloader/mod.rs diff --git a/fact/src/config/reloader/tests.rs b/fact-core/src/config/reloader/tests.rs similarity index 100% rename from fact/src/config/reloader/tests.rs rename to fact-core/src/config/reloader/tests.rs diff --git a/fact/src/config/tests.rs b/fact-core/src/config/tests.rs similarity index 88% rename from fact/src/config/tests.rs rename to fact-core/src/config/tests.rs index f00bae2c..50e1e8d9 100644 --- a/fact/src/config/tests.rs +++ b/fact-core/src/config/tests.rs @@ -1946,6 +1946,10 @@ fn defaults() { assert_eq!(config.paths(), default_paths); assert_eq!(config.grpc.url(), None); assert_eq!(config.grpc.certs(), None); + assert_eq!(config.grpc.backoff.initial(), Duration::from_secs(1)); + assert_eq!(config.grpc.backoff.max(), Duration::from_secs(60)); + assert!(config.grpc.backoff.jitter()); + assert_eq!(config.grpc.backoff.multiplier(), 1.5); assert_eq!( config.endpoint.address(), SocketAddr::from(([0, 0, 0, 0], 9000)) @@ -2017,6 +2021,468 @@ fn bpf_prog_enabled() { } } +macro_rules! generate_to_yaml_test { + ($testname:ident, $config:expr, $expected:expr) => { + #[test] + fn $testname() { + let expected = YamlLoader::load_from_str($expected).unwrap().pop().unwrap(); + assert_eq!($config.to_yaml(), expected); + } + }; +} + +generate_to_yaml_test! { + test_empty_paths_to_yaml, + FactConfig { + paths: Some(Vec::new()), + ..Default::default() + }, + r#" + paths: [] + "# +} +generate_to_yaml_test! { + test_paths_to_yaml, + FactConfig { + paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + ..Default::default() + }, + r#" + paths: + - /etc + - /bin + "# +} + +macro_rules! generate_grpc_to_yaml_test { + ($testname:ident, $config:expr, $expected:literal) => { + generate_to_yaml_test! { + $testname, + FactConfig { + grpc: $config, + ..Default::default() + }, + $expected + } + }; +} + +generate_grpc_to_yaml_test! { + test_grpc_url_to_yaml, + GrpcConfig { + url: Some("http://localhost:9090".into()), + ..Default::default() + }, + r#" + grpc: + url: 'http://localhost:9090' + "# +} +generate_grpc_to_yaml_test! { + test_grpc_certs_to_yaml, + GrpcConfig { + certs: Some("/etc/stackrox/certs".into()), + ..Default::default() + }, + r#" + grpc: + certs: /etc/stackrox/certs + "# +} +generate_grpc_to_yaml_test! { + test_grpc_backoff_initial_to_yaml, + GrpcConfig { + backoff: BackoffConfig { + initial: Some(Duration::from_secs(5)), + ..Default::default() + }, + ..Default::default() + }, + r#" + grpc: + backoff: + initial: 5 + "# +} +generate_grpc_to_yaml_test! { + test_grpc_backoff_max_to_yaml, + GrpcConfig { + backoff: BackoffConfig { + max: Some(Duration::from_secs(120)), + ..Default::default() + }, + ..Default::default() + }, + r#" + grpc: + backoff: + max: 120 + "# +} +generate_grpc_to_yaml_test! { + test_grpc_backoff_jitter_to_yaml, + GrpcConfig { + backoff: BackoffConfig { + jitter: Some(false), + ..Default::default() + }, + ..Default::default() + }, + r#" + grpc: + backoff: + jitter: false + "# +} +generate_grpc_to_yaml_test! { + test_grpc_backoff_multiplier_to_yaml, + GrpcConfig { + backoff: BackoffConfig { + multiplier: Some(3.5), + ..Default::default() + }, + ..Default::default() + }, + r#" + grpc: + backoff: + multiplier: 3.5 + "# +} +generate_grpc_to_yaml_test! { + test_grpc_backoff_retries_to_yaml, + GrpcConfig { + backoff: BackoffConfig { + retries_max: Some(20), + ..Default::default() + }, + ..Default::default() + }, + r#" + grpc: + backoff: + retries: 20 + "# +} + +generate_to_yaml_test! { + test_otel_endpoint_to_yaml, + FactConfig { + otel: OTelConfig { endpoint: Some("http://localhost:4317".into()) }, + ..Default::default() + }, + r#" + otel: + endpoint: http://localhost:4317 + "# +} + +macro_rules! generate_endpoint_to_yaml_test { + ($testname:ident, $config:expr, $expected:literal) => { + generate_to_yaml_test! { + $testname, + FactConfig { + endpoint: $config, + ..Default::default() + }, + $expected + } + }; +} + +generate_endpoint_to_yaml_test! { + test_endpoint_address_to_yaml, + EndpointConfig { + address: Some(SocketAddr::from(([0, 0, 0, 0], 8080))), + ..Default::default() + }, + r#" + endpoint: + address: 0.0.0.0:8080 + "# +} +generate_endpoint_to_yaml_test! { + test_endpoint_address_local_to_yaml, + EndpointConfig { + address: Some(SocketAddr::from(([127, 0, 0, 1], 8080))), + ..Default::default() + }, + r#" + endpoint: + address: 127.0.0.1:8080 + "# +} +generate_endpoint_to_yaml_test! { + test_endpoint_address_v6_to_yaml, + EndpointConfig { + address: Some(SocketAddr::from(( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 8080, + ))), + ..Default::default() + }, + r#" + endpoint: + address: '[::]:8080' + "# + +} +generate_endpoint_to_yaml_test! { + test_endpoint_address_v6_local_to_yaml, + EndpointConfig { + address: Some(SocketAddr::from(( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + 8080, + ))), + ..Default::default() + }, + r#" + endpoint: + address: '[::1]:8080' + "# + +} +generate_endpoint_to_yaml_test! { + test_endpoint_expose_metrics_true_to_yaml, + EndpointConfig { + expose_metrics: Some(true), + ..Default::default() + }, + r#" + endpoint: + expose_metrics: true + "# +} +generate_endpoint_to_yaml_test! { + test_endpoint_expose_metrics_false_to_yaml, + EndpointConfig { + expose_metrics: Some(false), + ..Default::default() + }, + r#" + endpoint: + expose_metrics: false + "# +} +generate_endpoint_to_yaml_test! { + test_endpoint_health_check_true_to_yaml, + EndpointConfig { + health_check: Some(true), + ..Default::default() + }, + r#" + endpoint: + health_check: true + "# +} +generate_endpoint_to_yaml_test! { + test_endpoint_health_check_false_to_yaml, + EndpointConfig { + health_check: Some(false), + ..Default::default() + }, + r#" + endpoint: + health_check: false + "# +} + +generate_to_yaml_test! { + test_skip_pre_flight_true_to_yaml, + FactConfig { + skip_pre_flight: Some(true), + ..Default::default() + }, + "skip_pre_flight: true" +} +generate_to_yaml_test! { + test_skip_pre_flight_false_to_yaml, + FactConfig { + skip_pre_flight: Some(false), + ..Default::default() + }, + "skip_pre_flight: false" +} + +generate_to_yaml_test! { + test_json_true_to_yaml, + FactConfig { + json: Some(true), + ..Default::default() + }, + "json: true" +} +generate_to_yaml_test! { + test_json_false_to_yaml, + FactConfig { + json: Some(false), + ..Default::default() + }, + "json: false" +} + +macro_rules! generate_bpf_to_yaml_test { + ($testname:ident, $config:expr, $expected:literal) => { + generate_to_yaml_test! { + $testname, + FactConfig { + bpf: $config, + ..Default::default() + }, + $expected + } + }; +} + +generate_bpf_to_yaml_test! { + test_bpf_ringbuf_size_to_yaml, + BpfConfig { + ringbuf_size: Some(64), + ..Default::default() + }, + r#" + bpf: + ringbuf_size: 64 + "# +} +generate_bpf_to_yaml_test! { + test_bpf_inodes_max_to_yaml, + BpfConfig { + inodes_max: Some(64), + ..Default::default() + }, + r#" + bpf: + inodes_max: 64 + "# +} + +generate_to_yaml_test! { + test_hotreload_true_to_yaml, + FactConfig { + hotreload: Some(true), + ..Default::default() + }, + "hotreload: true" +} +generate_to_yaml_test! { + test_hotreload_false_to_yaml, + FactConfig { + hotreload: Some(false), + ..Default::default() + }, + "hotreload: false" +} + +generate_to_yaml_test! { + test_scan_interval_integer_to_yaml, + FactConfig { + scan_interval: Some(Duration::from_secs(60)), + ..Default::default() + }, + "scan_interval: 60" +} +generate_to_yaml_test! { + test_scan_interval_float_to_yaml, + FactConfig { + scan_interval: Some(Duration::from_secs_f64(30.5)), + ..Default::default() + }, + "scan_interval: 30.5" +} + +generate_to_yaml_test! { + test_rate_limit_zero_to_yaml, + FactConfig { + rate_limit: Some(0), + ..Default::default() + }, + "rate_limit: 0" +} +generate_to_yaml_test! { + test_rate_limit_to_yaml, + FactConfig { + rate_limit: Some(1000), + ..Default::default() + }, + "rate_limit: 1000" +} + +generate_to_yaml_test! { + test_replay_to_yaml, + FactConfig { + replay: Some("/etc/some-path".into()), + ..Default::default() + }, + "replay: /etc/some-path" +} + +generate_to_yaml_test! { + test_full_config_to_yaml, + FactConfig { + paths: Some(vec!["/etc".into()]), + grpc: GrpcConfig { + url: Some("https://svc.sensor.stackrox:9090".into()), + certs: Some("/etc/stackrox/certs".into()), + backoff: BackoffConfig { + initial: Some(Duration::from_secs_f64(0.5)), + max: Some(Duration::from_secs(120)), + jitter: Some(false), + multiplier: Some(3.5), + retries_max: Some(5), + }, + }, + otel: OTelConfig { + endpoint: Some("http://localhost:4317".into()), + }, + endpoint: EndpointConfig { + address: Some(SocketAddr::from(([0, 0, 0, 0], 8080))), + expose_metrics: Some(true), + health_check: Some(true), + }, + skip_pre_flight: Some(false), + json: Some(false), + bpf: BpfConfig { + ringbuf_size: Some(8192), + inodes_max: Some(64), + programs: HashMap::new(), + }, + hotreload: Some(false), + scan_interval: Some(Duration::from_secs(60)), + rate_limit: Some(1000), + replay: Some("/etc/some-path".into()), + }, + + r#" + paths: + - /etc + grpc: + url: 'https://svc.sensor.stackrox:9090' + certs: /etc/stackrox/certs + backoff: + initial: 0.5 + max: 120 + jitter: false + multiplier: 3.5 + retries: 5 + otel: + endpoint: 'http://localhost:4317' + endpoint: + address: 0.0.0.0:8080 + expose_metrics: true + health_check: true + bpf: + ringbuf_size: 8192 + inodes_max: 64 + skip_pre_flight: false + json: false + hotreload: false + scan_interval: 60 + rate_limit: 1000 + replay: /etc/some-path + "# +} + static ENV_MUTEX: Mutex<()> = Mutex::new(()); /// RAII guard that holds the `ENV_MUTEX` lock and removes the named environment diff --git a/fact-core/src/lib.rs b/fact-core/src/lib.rs new file mode 100644 index 00000000..70e47fd6 --- /dev/null +++ b/fact-core/src/lib.rs @@ -0,0 +1,11 @@ +use crate::version::FACT_VERSION; + +pub mod config; + +mod version { + include!(concat!(env!("OUT_DIR"), "/version.rs")); +} + +pub fn version() -> &'static str { + FACT_VERSION +} diff --git a/fact-operator/Cargo.toml b/fact-operator/Cargo.toml new file mode 100644 index 00000000..a244564c --- /dev/null +++ b/fact-operator/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "fact-operator" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +env_logger = { workspace = true } +futures = { version = "0.3.32", default-features = false } +kube = { version = "4.0.0", default-features = false, features = ["client", "openssl-tls", "derive", "runtime"] } +k8s-openapi = { version = "0.28.0", features = ["latest", "schemars"] } +log = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +yaml-rust2 = { workspace = true } + +fact-core = { path = "../fact-core" } diff --git a/fact-operator/src/lib.rs b/fact-operator/src/lib.rs new file mode 100644 index 00000000..41121823 --- /dev/null +++ b/fact-operator/src/lib.rs @@ -0,0 +1,76 @@ +use std::{sync::Arc, time::Duration}; + +use futures::StreamExt; +use k8s_openapi::api::{apps::v1::DaemonSet, core::v1::ConfigMap}; +use kube::{ + Api, Client, ResourceExt, + api::{Patch, PatchParams}, + runtime::{Controller, controller::Action, watcher}, +}; +use log::{info, warn}; + +use crate::spec::{DaemonSetBuilder, Fact}; + +mod spec; + +struct Context { + client: Client, +} + +async fn reconcile(fact: Arc, ctx: Arc) -> Result { + info!("Starting reconciliation loop"); + let ns = fact.namespace().unwrap(); + + let cm = spec::build_configmap(&fact); + let api = Api::::namespaced(ctx.client.clone(), &ns); + api.patch( + &format!("{}-config", fact.name_any()), + &PatchParams::apply("fact-operator"), + &Patch::Apply(cm), + ) + .await?; + + let ds = DaemonSetBuilder::from(&*fact).build(); + let api = Api::::namespaced(ctx.client.clone(), &ns); + api.patch( + &fact.name_any(), + &PatchParams::apply("fact-operator"), + &Patch::Apply(ds), + ) + .await?; + + info!("Reconciliation done"); + Ok(Action::requeue(Duration::from_secs(300))) +} + +fn error_policy(_obj: Arc, _err: &kube::Error, _ctx: Arc) -> Action { + Action::requeue(Duration::from_secs(60)) +} + +pub async fn run() -> anyhow::Result<()> { + env_logger::init(); + info!("Operator starting..."); + let client = Client::try_default().await?; + let fact = Api::::all(client.clone()); + + Controller::new(fact, watcher::Config::default()) + .owns( + Api::::all(client.clone()), + watcher::Config::default(), + ) + .owns( + Api::::all(client.clone()), + watcher::Config::default(), + ) + .run(reconcile, error_policy, Arc::new(Context { client })) + .for_each(|res| async move { + match res { + Ok(o) => info!("reconciled: {o:?}"), + Err(e) => warn!("reconciler failed: {e:?}"), + } + }) + .await; + + info!("Operator done"); + Ok(()) +} diff --git a/fact-operator/src/main.rs b/fact-operator/src/main.rs new file mode 100644 index 00000000..289b177b --- /dev/null +++ b/fact-operator/src/main.rs @@ -0,0 +1,4 @@ +#[tokio::main] +async fn main() -> anyhow::Result<()> { + fact_operator::run().await +} diff --git a/fact-operator/src/spec.rs b/fact-operator/src/spec.rs new file mode 100644 index 00000000..4056dedd --- /dev/null +++ b/fact-operator/src/spec.rs @@ -0,0 +1,378 @@ +use std::collections::BTreeMap; + +use fact_core::config::FactConfig; +use k8s_openapi::{ + api::{ + apps::v1::{DaemonSet, DaemonSetSpec}, + core::v1::{ + Capabilities, ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, EnvVar, + HostPathVolumeSource, PodSpec, PodTemplateSpec, SecurityContext, Volume, VolumeMount, + }, + }, + apimachinery::pkg::apis::meta::v1::{LabelSelector, OwnerReference}, +}; +use kube::{CustomResource, Resource, ResourceExt, api::ObjectMeta}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use yaml_rust2::YamlEmitter; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, CustomResource)] +#[serde(rename_all = "camelCase")] +#[kube( + group = "fact.stackrox.io", + version = "v1alpha1", + kind = "Fact", + namespaced +)] +pub(crate) struct FactSpec { + // Configuration requiring restarts + pub image: String, + #[serde(default = "default_log_level")] + pub log_level: String, + + // Configuration hot-reloaded via configmap + pub config: FactConfig, +} + +fn default_log_level() -> String { + String::from("info") +} + +pub(crate) fn build_configmap(fact: &Fact) -> ConfigMap { + let config = fact.spec.config.to_yaml(); + let mut output = String::new(); + YamlEmitter::new(&mut output).dump(&config).unwrap(); + + let data = BTreeMap::from([("fact.yml".into(), output)]); + ConfigMap { + data: Some(data), + metadata: ObjectMeta { + name: Some(format!("{}-config", fact.name_any())), + namespace: fact.namespace(), + owner_references: fact.controller_owner_ref(&()).map(|r| vec![r]), + ..Default::default() + }, + ..Default::default() + } +} + +#[derive(Debug, Default)] +pub(crate) struct DaemonSetBuilder { + name: String, + namespace: Option, + owner_references: Option>, + + image: Option, + log_level: String, +} + +impl DaemonSetBuilder { + #[cfg(test)] + fn set_name(self, name: &str) -> Self { + DaemonSetBuilder { + name: name.to_string(), + ..self + } + } + + #[cfg(test)] + fn set_image(self, image: &str) -> Self { + DaemonSetBuilder { + image: Some(image.to_string()), + ..self + } + } + + #[cfg(test)] + fn set_log_level(self, log_level: &str) -> Self { + DaemonSetBuilder { + log_level: log_level.to_string(), + ..self + } + } + + pub fn build(self) -> DaemonSet { + let DaemonSetBuilder { + name, + namespace, + owner_references, + image, + log_level, + } = self; + let labels = BTreeMap::from([("app".into(), "fact".into())]); + + let metadata = ObjectMeta { + labels: Some(labels.clone()), + name: Some(name.clone()), + namespace, + owner_references, + ..Default::default() + }; + + let container = Container { + name: "fact".into(), + image, + image_pull_policy: Some("IfNotPresent".into()), + ports: Some(vec![ContainerPort { + container_port: 9000, + name: Some("monitoring".into()), + ..Default::default() + }]), + env: Some(vec![ + EnvVar { + name: "FACT_LOGLEVEL".into(), + value: Some(log_level), + ..Default::default() + }, + EnvVar { + name: "FACT_HOST_MOUNT".into(), + value: Some("/host".into()), + ..Default::default() + }, + ]), + security_context: Some(SecurityContext { + capabilities: Some(Capabilities { + drop: Some(vec!["NET_RAW".into()]), + ..Default::default() + }), + privileged: Some(true), + read_only_root_filesystem: Some(true), + ..Default::default() + }), + volume_mounts: Some(vec![ + VolumeMount { + name: "root-ro".into(), + mount_path: "/host".into(), + read_only: Some(true), + mount_propagation: Some("HostToContainer".into()), + ..Default::default() + }, + VolumeMount { + mount_path: "/etc/stackrox".into(), + name: "fact-config".into(), + read_only: Some(true), + ..Default::default() + }, + ]), + ..Default::default() + }; + + let volumes = vec![ + Volume { + host_path: Some(HostPathVolumeSource { + path: "/".into(), + ..Default::default() + }), + name: "root-ro".into(), + ..Default::default() + }, + Volume { + name: "fact-config".into(), + config_map: Some(ConfigMapVolumeSource { + name: format!("{name}-config"), + ..Default::default() + }), + ..Default::default() + }, + ]; + + let spec = DaemonSetSpec { + selector: LabelSelector { + match_labels: Some(labels.clone()), + ..Default::default() + }, + template: PodTemplateSpec { + metadata: Some(ObjectMeta { + labels: Some(labels), + ..Default::default() + }), + spec: Some(PodSpec { + containers: vec![container], + volumes: Some(volumes), + ..Default::default() + }), + }, + ..Default::default() + }; + + DaemonSet { + metadata, + spec: Some(spec), + ..Default::default() + } + } +} + +impl From<&Fact> for DaemonSetBuilder { + fn from(fact: &Fact) -> Self { + let name = fact.name_any(); + let namespace = fact.namespace(); + let owner_references = fact.controller_owner_ref(&()).map(|r| vec![r]); + let image = fact.spec.image.clone(); + let log_level = fact.spec.log_level.clone(); + + DaemonSetBuilder { + name, + namespace, + owner_references, + image: Some(image), + log_level, + } + } +} + +#[cfg(test)] +mod tests { + use yaml_rust2::Yaml; + + use super::*; + + fn yaml_to_string(input: &Yaml) -> String { + let mut output = String::new(); + YamlEmitter::new(&mut output).dump(input).unwrap(); + output + } + + #[test] + fn test_build_configmap() { + fn full_config() -> FactConfig { + FactConfig::try_from( + r#" + paths: + - /etc + grpc: + url: 'https://svc.sensor.stackrox:9090' + certs: /etc/stackrox/certs + endpoint: + address: 0.0.0.0:8080 + expose_metrics: true + health_check: true + skip_pre_flight: false + json: false + bpf: + ringbuf_size: 8192 + inodes_max: 64 + hotreload: false + scan_interval: 60 + rate_limit: 1000 + "#, + ) + .unwrap() + } + + let tests = [ + ( + Fact::new( + "fact", + FactSpec { + image: "quay.io/stackrox-io/fact:0.3.0".into(), + log_level: "debug".into(), + config: FactConfig::default(), + }, + ), + ConfigMap { + data: Some(BTreeMap::from([( + "fact.yml".to_string(), + yaml_to_string(&FactConfig::default().to_yaml()), + )])), + metadata: ObjectMeta { + name: Some("fact-config".into()), + namespace: None, + ..Default::default() + }, + ..Default::default() + }, + ), + ( + Fact::new( + "not-fact", + FactSpec { + image: "quay.io/stackrox-io/fact:0.3.0".into(), + log_level: "debug".into(), + config: FactConfig::default(), + }, + ), + ConfigMap { + data: Some(BTreeMap::from([( + "fact.yml".to_string(), + yaml_to_string(&FactConfig::default().to_yaml()), + )])), + metadata: ObjectMeta { + name: Some("not-fact-config".into()), + namespace: None, + ..Default::default() + }, + ..Default::default() + }, + ), + ( + Fact::new( + "fact", + FactSpec { + image: "quay.io/stackrox-io/fact:0.3.0".into(), + log_level: "debug".into(), + config: full_config(), + }, + ), + ConfigMap { + data: Some(BTreeMap::from([( + "fact.yml".to_string(), + yaml_to_string(&full_config().to_yaml()), + )])), + metadata: ObjectMeta { + name: Some("fact-config".into()), + namespace: None, + ..Default::default() + }, + ..Default::default() + }, + ), + ]; + + for (fact, expected) in tests { + let cm = build_configmap(&fact); + assert_eq!(cm, expected); + } + } + + #[test] + fn test_build_daemonset() { + let tests = [ + ( + Fact::new( + "fact", + FactSpec { + image: "quay.io/stackrox-io/fact:0.3.0".into(), + log_level: "info".into(), + config: FactConfig::default(), + }, + ), + DaemonSetBuilder::default() + .set_image("quay.io/stackrox-io/fact:0.3.0") + .set_name("fact") + .set_log_level("info") + .build(), + ), + ( + Fact::new( + "other-fact", + FactSpec { + image: "quay.io/rhacs-eng/fact:0.3.0".into(), + log_level: "warn".into(), + config: FactConfig::default(), + }, + ), + DaemonSetBuilder::default() + .set_image("quay.io/rhacs-eng/fact:0.3.0") + .set_name("other-fact") + .set_log_level("warn") + .build(), + ), + ]; + + for (fact, expected) in tests { + let res = DaemonSetBuilder::from(&fact).build(); + assert_eq!(res, expected); + } + } +} diff --git a/fact/Cargo.toml b/fact/Cargo.toml index 4770feb6..06e10bb2 100644 --- a/fact/Cargo.toml +++ b/fact/Cargo.toml @@ -40,6 +40,7 @@ uuid = { workspace = true } yaml-rust2 = { workspace = true } fact-api = { path = "../fact-api" } +fact-core = { path = "../fact-core" } fact-ebpf = { path = "../fact-ebpf" } [dev-dependencies] diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index 2801b155..689ab2ab 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -16,8 +16,9 @@ use tokio::{ task::JoinSet, }; -use crate::{config::BpfConfig, event::Event, host_info, metrics::EventCounter}; +use crate::{event::Event, host_info, metrics::EventCounter}; +use fact_core::config::BpfConfig; use fact_ebpf::{LPM_SIZE_MAX, event_t, inode_key_t, inode_value_t, metrics_t, path_prefix_t}; mod checks; @@ -346,12 +347,13 @@ mod bpf_tests { use tokio::{sync::watch, time::timeout}; use crate::{ - config::{BpfProgConfig, FactConfig, reloader::Reloader}, event::{EventTestData, process::Process}, host_info, metrics::Metrics, }; + use fact_core::config::{BpfProgConfig, FactConfig, reloader::Reloader}; + use super::*; #[tokio::test] diff --git a/fact/src/endpoints.rs b/fact/src/endpoints.rs index cc97415a..c23946a8 100644 --- a/fact/src/endpoints.rs +++ b/fact/src/endpoints.rs @@ -11,7 +11,9 @@ use hyper_util::rt::TokioIo; use log::{info, warn}; use tokio::{net::TcpListener, sync::watch, task::JoinHandle}; -use crate::{config::EndpointConfig, metrics::exporter::Exporter}; +use crate::metrics::exporter::Exporter; + +use fact_core::config::EndpointConfig; #[derive(Clone)] pub struct Server { diff --git a/fact/src/lib.rs b/fact/src/lib.rs index 08e02846..02d1bd16 100644 --- a/fact/src/lib.rs +++ b/fact/src/lib.rs @@ -15,7 +15,6 @@ use tokio::{ }; mod bpf; -pub mod config; mod endpoints; mod event; mod host_info; @@ -26,15 +25,15 @@ mod pre_flight; mod rate_limiter; mod replay; -use config::FactConfig; use pre_flight::pre_flight; use crate::{ - config::BpfConfig, event::Event, metrics::{Metrics, kernel_metrics::KernelMetrics}, }; +use fact_core::config::{BpfConfig, FactConfig, reloader::Reloader}; + pub fn init_log() -> anyhow::Result<()> { let log_level = std::env::var("FACT_LOGLEVEL").unwrap_or("info".to_owned()); let log_level = LevelFilter::from_str(&log_level)?; @@ -56,12 +55,8 @@ pub fn init_log() -> anyhow::Result<()> { Ok(()) } -mod version { - include!(concat!(env!("OUT_DIR"), "/version.rs")); -} - pub fn log_system_information() { - info!("fact version: {}", version::FACT_VERSION); + info!("fact version: {}", fact_core::version()); info!("OS: {}", get_distro()); match SystemInfo::new() { Ok(sysinfo) => { @@ -94,7 +89,7 @@ struct SetupArgs<'a> { skip_pre_flight: bool, running: watch::Receiver, task_set: &'a mut JoinSet>, - reloader: &'a config::reloader::Reloader, + reloader: &'a Reloader, metrics: &'a Metrics, // Replay mode @@ -118,7 +113,7 @@ pub async fn run(config: FactConfig) -> anyhow::Result<()> { let metrics_userspace = Metrics::new(); let mut task_set = JoinSet::new(); - let reloader = config::reloader::Reloader::from(config); + let reloader = Reloader::from(config); let setup_args = SetupArgs { skip_pre_flight, diff --git a/fact/src/main.rs b/fact/src/main.rs index 1ae88408..a5fcca06 100644 --- a/fact/src/main.rs +++ b/fact/src/main.rs @@ -1,4 +1,4 @@ -use fact::config::FactConfig; +use fact_core::config::FactConfig; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/fact/src/output/grpc.rs b/fact/src/output/grpc.rs index e91eb897..f430114a 100644 --- a/fact/src/output/grpc.rs +++ b/fact/src/output/grpc.rs @@ -19,11 +19,9 @@ use tokio_stream::{ }; use tonic::transport::Channel; -use crate::{ - config::{BackoffConfig, GrpcConfig}, - metrics::EventCounter, - output::EventReceiver, -}; +use crate::{metrics::EventCounter, output::EventReceiver}; + +use fact_core::config::{BackoffConfig, GrpcConfig}; struct Backoff { initial: Duration, diff --git a/fact/src/output/mod.rs b/fact/src/output/mod.rs index 049f6e9c..078f9a6c 100644 --- a/fact/src/output/mod.rs +++ b/fact/src/output/mod.rs @@ -6,12 +6,9 @@ use tokio::{ task::JoinSet, }; -use crate::{ - config::{GrpcConfig, OTelConfig}, - event::Event, - flatten_task_result, join_all_tasks, - metrics::OutputMetrics, -}; +use crate::{event::Event, flatten_task_result, join_all_tasks, metrics::OutputMetrics}; + +use fact_core::config::{GrpcConfig, OTelConfig}; mod grpc; #[cfg(feature = "otel")] diff --git a/fact/src/output/otel.rs b/fact/src/output/otel.rs index 3af202df..bcb505e7 100644 --- a/fact/src/output/otel.rs +++ b/fact/src/output/otel.rs @@ -11,7 +11,9 @@ use tokio::{ task::JoinSet, }; -use crate::{config::OTelConfig, metrics::EventCounter, output::EventReceiver}; +use crate::{metrics::EventCounter, output::EventReceiver}; + +use fact_core::config::OTelConfig; pub(super) struct Client { subscriber: mpsc::Sender>, diff --git a/k8s/example-cr.yml b/k8s/example-cr.yml new file mode 100644 index 00000000..7a141599 --- /dev/null +++ b/k8s/example-cr.yml @@ -0,0 +1,21 @@ +apiVersion: fact.stackrox.io/v1alpha1 +kind: Fact +metadata: + name: fact + namespace: default +spec: + image: quay.io/mmoltras/fact:operator-test + logLevel: debug + config: + rateLimit: 50000 + paths: + - /etc + - /etc/**/* + - /bin + - /bin/**/* + - /sbin + - /sbin/**/* + - /usr/bin + - /usr/bin/**/* + - /usr/sbin + - /usr/sbin/**/* diff --git a/k8s/operator.yml b/k8s/operator.yml new file mode 100644 index 00000000..9db0bd3f --- /dev/null +++ b/k8s/operator.yml @@ -0,0 +1,189 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: facts.fact.stackrox.io +spec: + group: fact.stackrox.io + names: + kind: Fact + plural: facts + singular: fact + shortNames: + - ft + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [image] + properties: + image: + type: string + logLevel: + type: string + default: info + config: + type: object + properties: + paths: + type: array + items: + type: string + default: null + grpc: + type: object + default: null + properties: + url: + type: string + default: null + certs: + type: string + default: null + otel: + type: object + default: null + properties: + endpoint: + type: string + default: null + endpoint: + type: object + default: null + properties: + address: + type: string + default: null + exposeMetrics: + type: boolean + default: null + healthCheck: + type: boolean + default: null + bpf: + type: object + default: null + properties: + ringbufSize: + type: integer + default: null + inodesMax: + type: integer + default: null + skipPreFlight: + type: boolean + default: null + json: + type: boolean + default: null + hotreload: + type: boolean + default: null + scanInterval: + type: integer + default: null + rateLimit: + type: integer + default: 0 + status: + type: object + properties: + readyNodes: + type: integer + desiredNodes: + type: integer + subresources: + status: {} + additionalPrinterColumns: + - name: Image + type: string + jsonPath: .spec.image + - name: Ready + type: string + jsonPath: .status.readyNodes + - name: Desired + type: string + jsonPath: .status.desiredNodes +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fact-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: fact-operator + namespace: fact-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: fact-operator +rules: + # Watch and update our CRD instances + - apiGroups: [fact.stackrox.io] + resources: [facts, facts/status] + verbs: [get, list, watch, patch, update] + # Manage ConfigMaps + - apiGroups: [""] + resources: [configmaps] + verbs: [get, list, watch, create, update, patch, delete] + # Manage DaemonSets + - apiGroups: [apps] + resources: [daemonsets] + verbs: [get, list, watch, create, update, patch, delete] + # Emit events + - apiGroups: [""] + resources: [events] + verbs: [create, patch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: fact-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: fact-operator +subjects: + - kind: ServiceAccount + name: fact-operator + namespace: fact-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fact-operator + namespace: fact-system +spec: + replicas: 1 + selector: + matchLabels: + app: fact-operator + template: + metadata: + labels: + app: fact-operator + spec: + serviceAccountName: fact-operator + containers: + - name: fact-operator + image: quay.io/mmoltras/fact:operator + imagePullPolicy: Always + env: + - name: RUST_LOG + value: debug + resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi