diff --git a/Cargo.lock b/Cargo.lock index 2a5c7b60..2e47965f 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.3" @@ -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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86cd1c51b95d71dde52bca69ed225008f6ff4c8cc825b08042aa1ef823e1980" +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", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -167,7 +225,7 @@ dependencies = [ "log", "object", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -181,7 +239,18 @@ dependencies = [ "hashbrown 0.15.4", "log", "object", - "thiserror", + "thiserror 1.0.69", +] + +[[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]] @@ -216,6 +285,15 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.12.1" @@ -319,6 +397,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-error" version = "0.0.0" @@ -344,6 +431,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -359,6 +455,50 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[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", +] + +[[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", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -373,12 +513,61 @@ dependencies = [ "parking_lot_core", ] +[[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", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dtoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[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", +] + [[package]] name = "either" version = "1.15.0" @@ -394,6 +583,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.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "1.0.0" @@ -430,6 +648,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "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" @@ -439,6 +678,7 @@ dependencies = [ "clap", "env_logger", "fact-api", + "fact-core", "fact-ebpf", "glob", "globset", @@ -480,6 +720,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" @@ -491,6 +744,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.3.0" @@ -536,32 +807,73 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -571,18 +883,31 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", - "pin-utils", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -629,6 +954,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" @@ -652,6 +989,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "granit-parser" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50ba32164f9e098d5da618776a32afbb32270adcbe3d3d006107dae11e37c91" +dependencies = [ + "arraydeque", + "smallvec", +] + [[package]] name = "h2" version = "0.4.14" @@ -714,6 +1061,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.3.1" @@ -782,6 +1140,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" @@ -832,75 +1209,237 @@ dependencies = [ ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "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 2.0.18", +] + +[[package]] +name = "jsonpath-rust" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] [[package]] -name = "indexmap" -version = "2.10.0" +name = "k8s-openapi" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "d9c6922f6afe80418dd6019818af5d0d34584c371780ff09b9752370c25b4abb" dependencies = [ - "equivalent", - "hashbrown 0.15.4", + "base64", + "jiff", + "schemars", "serde", + "serde_json", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "kube" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "4bb9108095346a7096d11feeaff419c75dddcac1b2f59acb38d7bf3d13c3e146" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] [[package]] -name = "itertools" -version = "0.13.0" +name = "kube-client" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "d0f628e05bc2264c21fe10d3d675117dc9b43ea3bf4fb07262a222679757537b" 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 2.0.18", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", ] [[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.23" +name = "kube-core" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "c1b02f5933ba06140d58c7d6727f6c319f0962ec6a344aa5e21e475e891deaa8" dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", + "derive_more", + "form_urlencoded", + "http", + "jiff", + "json-patch", + "k8s-openapi", + "schemars", + "serde", + "serde-value", + "serde_json", + "thiserror 2.0.18", ] [[package]] -name = "jiff-static" -version = "0.2.23" +name = "kube-derive" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "fe171898707dadf1818ef94e81ef57f6beb7edf9ba87b9e814c045dad356c7aa" dependencies = [ + "darling", "proc-macro2", "quote", + "serde", + "serde_json", "syn", ] [[package]] -name = "js-sys" -version = "0.3.77" +name = "kube-runtime" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "99ddec66c540c7cf29a5b41fe4a657a53687f95c346e03bdf00585b70a1bab21" dependencies = [ - "once_cell", - "wasm-bindgen", + "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 2.0.18", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -925,6 +1464,21 @@ dependencies = [ "windows-targets", ] +[[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" @@ -1005,6 +1559,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" @@ -1021,6 +1581,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.36.7" @@ -1088,6 +1657,21 @@ dependencies = [ "vcpkg", ] +[[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.4" @@ -1111,12 +1695,65 @@ dependencies = [ "windows-targets", ] +[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.7.1" @@ -1153,12 +1790,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.32" @@ -1382,6 +2013,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.4" @@ -1417,6 +2068,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[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" @@ -1445,12 +2105,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[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.6.0" @@ -1490,6 +2184,35 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "0.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5897b4c3faadadd35fdb6689f015641f3bc481d5adaaac56231ea15aeb243db3" +dependencies = [ + "ahash", + "annotate-snippets", + "base64", + "encoding_rs_io", + "getrandom 0.3.4", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde", + "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.228" @@ -1510,6 +2233,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.150" @@ -1523,6 +2257,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1617,7 +2362,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -1631,6 +2385,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.52.3" @@ -1690,6 +2455,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project-lite", + "slab", "tokio", ] @@ -1780,6 +2546,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "base64", + "bitflags", + "bytes", + "http", + "http-body", + "mime", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1798,6 +2583,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1829,6 +2615,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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.8.1" @@ -1841,6 +2639,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -1917,6 +2721,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] @@ -2249,6 +3054,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zmij" version = "1.0.14" diff --git a/Cargo.toml b/Cargo.toml index f92d5b0c..05080776 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"] @@ -30,6 +32,7 @@ openssl = "0.10.75" prometheus-client = { version = "0.25.0", default-features = false } prost = "0.14.0" prost-types = "0.14.0" +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 68869141..8c44d19d 100644 --- a/Containerfile +++ b/Containerfile @@ -42,10 +42,11 @@ FROM builder AS build ARG FACT_VERSION RUN --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/app/target \ - cargo build --release && \ - cp target/release/fact fact + cargo build --release --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" \ @@ -64,3 +65,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 f1739a18..d40d3720 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ version: image-name: @echo "$(FACT_IMAGE_NAME)" +operator-name: + @echo "$(FACT_OPERATOR_NAME)" + mock-server: make -C mock-server @@ -17,9 +20,21 @@ image: -f Containerfile \ --build-arg FACT_VERSION=$(FACT_VERSION) \ --build-arg RUST_VERSION=$(RUST_VERSION) \ + --target fact \ -t $(FACT_IMAGE_NAME) \ $(CURDIR) +operator: + $(DOCKER) build \ + -f Containerfile \ + --build-arg FACT_VERSION=$(FACT_VERSION) \ + --build-arg RUST_VERSION=$(RUST_VERSION) \ + --target fact-operator \ + -t $(FACT_OPERATOR_NAME) \ + $(CURDIR) + +images: image operator + licenses:THIRD_PARTY_LICENSES.html THIRD_PARTY_LICENSES.html:Cargo.lock @@ -54,4 +69,5 @@ format: make -C fact-ebpf format ruff format tests/ -.PHONY: tag mock-server integration-tests image image-name licenses coverage lint clean +.PHONY: tag mock-server integration-tests image images 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 80% rename from fact/src/config/mod.rs rename to fact-core/src/config/mod.rs index b7530176..e129b88c 100644 --- a/fact/src/config/mod.rs +++ b/fact-core/src/config/mod.rs @@ -10,6 +10,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; @@ -23,11 +25,14 @@ const CONFIG_FILES: [&str; 4] = [ "fact.yaml", ]; -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct FactConfig { paths: Option>, + #[serde(default)] pub grpc: GrpcConfig, + #[serde(default)] pub endpoint: EndpointConfig, + #[serde(default)] pub bpf: BpfConfig, skip_pre_flight: Option, json: Option, @@ -125,10 +130,59 @@ impl FactConfig { self.rate_limit.unwrap_or(0) } - #[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)); + } + + config.insert(Yaml::String("grpc".into()), self.grpc.to_yaml()); + config.insert(Yaml::String("endpoint".into()), self.endpoint.to_yaml()); + config.insert(Yaml::String("bpf".into()), self.bpf.to_yaml()); + + 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 { + let scan_interval = scan_interval.as_secs_f64(); + let scan_interval = if scan_interval.fract() != 0.0 { + Yaml::Real(scan_interval.to_string()) + } else { + Yaml::Integer(scan_interval as i64) + }; + config.insert(Yaml::String("scan_interval".into()), scan_interval); + } + + if let Some(rate_limit) = self.rate_limit { + config.insert( + Yaml::String("rate_limit".into()), + Yaml::Integer(rate_limit as i64), + ); + } + + Yaml::Hash(config) + } } impl TryFrom<&str> for FactConfig { @@ -189,10 +243,16 @@ impl TryFrom> for FactConfig { let grpc = v.as_hash().unwrap(); config.grpc = GrpcConfig::try_from(grpc)?; } + "grpc" if v.is_null() => { + // Nothing to do + } "endpoint" if v.is_hash() => { let endpoint = v.as_hash().unwrap(); config.endpoint = EndpointConfig::try_from(endpoint)?; } + "endpoint" if v.is_null() => { + // Nothing to do + } "skip_pre_flight" => { let Some(spf) = v.as_bool() else { bail!("skip_pre_flight field has incorrect type: {v:?}"); @@ -205,12 +265,13 @@ 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)?; } + "bpf" if v.is_null() => { + // Nothing to do + } "hotreload" => { let Some(hotreload) = v.as_bool() else { bail!("hotreload field has incorrect type: {v:?}"); @@ -251,7 +312,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, @@ -285,6 +346,37 @@ impl EndpointConfig { pub fn health_check(&self) -> bool { self.health_check.unwrap_or(false) } + + fn to_yaml(&self) -> Yaml { + 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() { + Yaml::Hash(endpoint) + } else { + Yaml::Null + } + } } impl TryFrom<&yaml::Hash> for EndpointConfig { @@ -328,7 +420,7 @@ impl TryFrom<&yaml::Hash> for EndpointConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct GrpcConfig { url: Option, certs: Option, @@ -352,6 +444,26 @@ impl GrpcConfig { pub fn certs(&self) -> Option<&Path> { self.certs.as_deref() } + + fn to_yaml(&self) -> Yaml { + 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 !grpc.is_empty() { + Yaml::Hash(grpc) + } else { + Yaml::Null + } + } } impl TryFrom<&yaml::Hash> for GrpcConfig { @@ -385,7 +497,7 @@ impl TryFrom<&yaml::Hash> for GrpcConfig { } } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, JsonSchema, Serialize, Deserialize)] pub struct BpfConfig { ringbuf_size: Option, inodes_max: Option, @@ -409,6 +521,30 @@ impl BpfConfig { pub fn inodes_max(&self) -> u32 { self.inodes_max.unwrap_or(65536) } + + fn to_yaml(&self) -> Yaml { + 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 !bpf.is_empty() { + Yaml::Hash(bpf) + } else { + Yaml::Null + } + } } impl TryFrom<&yaml::Hash> for BpfConfig { diff --git a/fact/src/config/reloader.rs b/fact-core/src/config/reloader.rs similarity index 100% rename from fact/src/config/reloader.rs rename to fact-core/src/config/reloader.rs diff --git a/fact/src/config/tests.rs b/fact-core/src/config/tests.rs similarity index 77% rename from fact/src/config/tests.rs rename to fact-core/src/config/tests.rs index f25d86c5..959b425e 100644 --- a/fact/src/config/tests.rs +++ b/fact-core/src/config/tests.rs @@ -23,6 +23,12 @@ fn parsing() { ..Default::default() }, ), + ( + r#" + grpc: null + "#, + FactConfig::default(), + ), ( r#" grpc: @@ -49,6 +55,12 @@ fn parsing() { ..Default::default() }, ), + ( + r#" + endpoint: null + "#, + FactConfig::default(), + ), ( r#" endpoint: @@ -187,6 +199,12 @@ fn parsing() { ..Default::default() }, ), + ( + r#" + bpf: null + "#, + FactConfig::default(), + ), ( r#" bpf: @@ -273,6 +291,7 @@ fn parsing() { inodes_max: 64 hotreload: false scan_interval: 60 + rate_limit: 1000 "#, FactConfig { paths: Some(vec![PathBuf::from("/etc")]), @@ -293,7 +312,7 @@ fn parsing() { }, hotreload: Some(false), scan_interval: Some(Duration::from_secs(60)), - rate_limit: None, + rate_limit: Some(1000), }, ), ]; @@ -1104,6 +1123,492 @@ fn defaults() { assert_eq!(config.bpf.ringbuf_size(), 8192); assert_eq!(config.bpf.inodes_max(), 65536); assert!(config.hotreload()); + assert_eq!(config.scan_interval(), Duration::from_secs(30)); + assert_eq!(config.rate_limit(), 0); +} + +#[test] +fn test_to_yaml() { + let tests = [ + ( + FactConfig { + paths: Some(Vec::new()), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + paths: [] + grpc: null + endpoint: null + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + paths: Some(vec![PathBuf::from("/etc"), PathBuf::from("/bin")]), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + paths: + - /etc + - /bin + grpc: null + endpoint: null + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + grpc: GrpcConfig { + url: Some(String::from("http://localhost:9090")), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: + url: 'http://localhost:9090' + endpoint: null + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + grpc: GrpcConfig { + certs: Some(PathBuf::from("/etc/stackrox/certs")), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: + certs: /etc/stackrox/certs + endpoint: null + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + address: Some(SocketAddr::from(([0, 0, 0, 0], 8080))), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + address: 0.0.0.0:8080 + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + address: Some(SocketAddr::from(([127, 0, 0, 1], 8080))), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + address: 127.0.0.1:8080 + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + address: Some(SocketAddr::from(( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 8080, + ))), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + address: '[::]:8080' + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + address: Some(SocketAddr::from(( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + 8080, + ))), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + address: '[::1]:8080' + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + expose_metrics: Some(true), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + expose_metrics: true + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + expose_metrics: Some(false), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + expose_metrics: false + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + health_check: Some(true), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + health_check: true + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + endpoint: EndpointConfig { + health_check: Some(false), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: + health_check: false + bpf: null + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + skip_pre_flight: Some(true), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + skip_pre_flight: true + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + skip_pre_flight: Some(false), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + skip_pre_flight: false + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + json: Some(false), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + json: false + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + json: Some(false), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + json: false + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + bpf: BpfConfig { + ringbuf_size: Some(64), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: + ringbuf_size: 64 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + bpf: BpfConfig { + inodes_max: Some(64), + ..Default::default() + }, + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: + inodes_max: 64 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + hotreload: Some(true), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + hotreload: true + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + hotreload: Some(false), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + hotreload: false + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + scan_interval: Some(Duration::from_secs(60)), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + scan_interval: 60 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + scan_interval: Some(Duration::from_secs_f64(30.5)), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + scan_interval: 30.5 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + rate_limit: Some(0), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + rate_limit: 0 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + rate_limit: Some(1000), + ..Default::default() + }, + YamlLoader::load_from_str( + r#" + grpc: null + endpoint: null + bpf: null + rate_limit: 1000 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ( + FactConfig { + paths: Some(vec!["/etc".into()]), + grpc: GrpcConfig { + url: Some("https://svc.sensor.stackrox:9090".into()), + certs: Some("/etc/stackrox/certs".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), + }, + hotreload: Some(false), + scan_interval: Some(Duration::from_secs(60)), + rate_limit: Some(1000), + }, + YamlLoader::load_from_str( + 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 + bpf: + ringbuf_size: 8192 + inodes_max: 64 + skip_pre_flight: false + json: false + hotreload: false + scan_interval: 60 + rate_limit: 1000 + "#, + ) + .unwrap()[0] + .to_owned(), + ), + ]; + + for (config, expected) in tests { + assert_eq!(config.to_yaml(), expected); + } } static ENV_MUTEX: Mutex<()> = Mutex::new(()); 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 1b2b63c8..d39b45de 100644 --- a/fact/Cargo.toml +++ b/fact/Cargo.toml @@ -35,6 +35,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 917eae3a..4e70038d 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -16,8 +16,9 @@ use tokio::{ task::JoinHandle, }; -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; @@ -281,12 +282,13 @@ mod bpf_tests { use tokio::{sync::watch, time::timeout}; use crate::{ - config::{FactConfig, reloader::Reloader}, event::{EventTestData, process::Process}, host_info, metrics::exporter::Exporter, }; + use fact_core::config::{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 bd9d708b..a7944609 100644 --- a/fact/src/lib.rs +++ b/fact/src/lib.rs @@ -13,7 +13,6 @@ use tokio::{ }; mod bpf; -pub mod config; mod endpoints; mod event; mod host_info; @@ -23,9 +22,10 @@ mod output; mod pre_flight; mod rate_limiter; -use config::FactConfig; use pre_flight::pre_flight; +use fact_core::config::{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)?; @@ -47,12 +47,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) => { @@ -77,7 +73,7 @@ pub async fn run(config: FactConfig) -> anyhow::Result<()> { debug!("Skipping pre-flight checks"); } - let reloader = config::reloader::Reloader::from(config); + let reloader = Reloader::from(config); let config_trigger = reloader.get_trigger(); let (mut bpf, rx) = Bpf::new(reloader.paths(), &reloader.config().bpf)?; 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 259d6004..66f1b91f 100644 --- a/fact/src/output/grpc.rs +++ b/fact/src/output/grpc.rs @@ -18,7 +18,9 @@ use tokio_stream::{ }; use tonic::transport::Channel; -use crate::{config::GrpcConfig, event::Event, metrics::EventCounter}; +use crate::{event::Event, metrics::EventCounter}; + +use fact_core::config::GrpcConfig; pub struct Client { rx: broadcast::Receiver>, diff --git a/fact/src/output/mod.rs b/fact/src/output/mod.rs index 23cda0b9..97b5421b 100644 --- a/fact/src/output/mod.rs +++ b/fact/src/output/mod.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use log::{debug, warn}; use tokio::sync::{broadcast, mpsc, watch}; -use crate::{config::GrpcConfig, event::Event, metrics::OutputMetrics}; +use crate::{event::Event, metrics::OutputMetrics}; + +use fact_core::config::GrpcConfig; mod grpc; mod stdout; 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..554c357a --- /dev/null +++ b/k8s/operator.yml @@ -0,0 +1,182 @@ +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 + 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