diff --git a/.gitignore b/.gitignore index 686ddf2..5fb7f9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /libcollar_store **/libcollar_store +crates/api/test_api* diff --git a/Cargo.lock b/Cargo.lock index a6a646b..75b0884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,2088 +1,2730 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "api" version = "0.1.0" dependencies = [ "axum", "axum-test", "libcollar", "log", "serde", "serde_json", "store", "tempfile", "thiserror", "tokio", "tokio-test", "tower-http", "tracing", ] [[package]] name = "assert-json-diff" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" dependencies = [ "serde", "serde_json", ] [[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 = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "auto-future" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", "bytes", "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "rustversion", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-core" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", "sync_wrapper", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-test" version = "17.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac" dependencies = [ "anyhow", "assert-json-diff", "auto-future", "axum", "bytes", "bytesize", "cookie", "http", "http-body-util", "hyper", "hyper-util", "mime", "pretty_assertions", "reserve-port", "rust-multipart-rfc7578_2", "serde", "serde_json", "serde_urlencoded", "smallvec", "tokio", "tower", "url", ] [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bindgen" version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", "itertools", "log", "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytesize" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" +[[package]] +name = "cc" +version = "1.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +dependencies = [ + "shlex", +] + [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] +[[package]] +name = "clap" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "collar" version = "0.1.0" dependencies = [ "api", "env_logger", "libcollar", "log", "thiserror", "tokio", "tracing", "tracing-subscriber", ] +[[package]] +name = "collar-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "reqwest", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "cookie" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "time", "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", "winapi", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "fxhash" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ "byteorder", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ + "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "ifconfig" version = "0.1.0" dependencies = [ "errno", "libc", "log", "nix", "thiserror", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "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.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 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 = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libcollar" version = "0.1.0" dependencies = [ "ifconfig", "log", "ng", "serde", "store", "thiserror", "tokio", "tracing", ] [[package]] name = "libloading" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", "windows-targets 0.53.0", ] [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ng" version = "0.1.0" dependencies = [ "bindgen", "errno", "libc", "log", "socket2", "thiserror", ] [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", "parking_lot_core 0.8.6", ] [[package]] name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core 0.9.11", ] [[package]] name = "parking_lot_core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if", "instant", "libc", "redox_syscall 0.2.16", "smallvec", "winapi", ] [[package]] name = "parking_lot_core" version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.12", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "prettyplease" version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.3", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags 2.9.1", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reserve-port" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" dependencies = [ "thiserror", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rust-multipart-rfc7578_2" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" dependencies = [ "bytes", "futures-core", "futures-util", "http", "mime", "rand", "thiserror", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ "itoa", "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "sled" version = "0.34.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" dependencies = [ "crc32fast", "crossbeam-epoch", "crossbeam-utils", "fs2", "fxhash", "libc", "log", "parking_lot 0.11.2", ] [[package]] name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "store" version = "0.1.0" dependencies = [ "log", "serde", "serde_json", "sled", "tempfile", "thiserror", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "time" version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tokio" version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot 0.12.4", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-test" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ "async-stream", "bytes", "futures-core", "tokio", "tokio-stream", ] +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-http" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" dependencies = [ "bitflags 2.9.1", "bytes", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" 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", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.1", ] [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn", ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..53fc84e --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "collar-cli" +version = "0.1.0" +license.workspace = true +edition.workspace = true +authors.workspace = true + +[[bin]] +name = "collar" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +reqwest = { version = "0.12", features = ["json"] } +tokio = { version = "1.45.1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +url = "2.5" \ No newline at end of file diff --git a/crates/cli/README.md b/crates/cli/README.md new file mode 100644 index 0000000..5cc6c68 --- /dev/null +++ b/crates/cli/README.md @@ -0,0 +1,259 @@ +# Collar CLI + +Command-line interface for the Collar API server. Provides commands for managing networks and instances through the HTTP API. + +## Installation + +From the project root directory: + +```bash +cargo build --release +``` + +The binary will be available at `target/release/collar`. + +## Usage + +```bash +collar [OPTIONS] <COMMAND> +``` + +### Options + +- `--url <URL>` - Base URL of the Collar API server (default: http://localhost:3000) + +### Commands + +- `network` - Network management commands +- `instance` - Instance management commands + +## Network Management + +### List Networks + +```bash +collar network list +``` + +### Create Network + +Basic network: +```bash +collar network create my-network 192.168.1.0/24 +``` + +Network with basic assigner: +```bash +collar network create my-network 192.168.1.0/24 \ + --assigner-type basic \ + --assigner-config '{"strategy": "sequential"}' +``` + +Network with range assigner: +```bash +collar network create my-network 192.168.1.0/24 \ + --assigner-type range \ + --assigner-config '{"range_name": "my-range"}' +``` + +### Get Network Details + +```bash +collar network get my-network +``` + +### Delete Network + +```bash +collar network delete my-network +``` + +### Show Examples + +```bash +collar network create --examples +``` + +## Instance Management + +### List Instances + +```bash +collar instance list +``` + +### Create Instance + +Jail instance: +```bash +collar instance create my-jail \ + --provider-type jail \ + --provider-config '{"jail_conf": "/etc/jail.conf", "dataset": "zroot/jails"}' +``` + +Container instance: +```bash +collar instance create my-container \ + --provider-type container \ + --provider-config '{"runtime": "docker", "image": "alpine:latest"}' +``` + +Instance with network interfaces: +```bash +collar instance create networked-instance \ + --provider-type jail \ + --provider-config '{}' \ + --network-interfaces '[{"network_name": "test-network", "interface_name": "eth0"}]' +``` + +Instance with metadata: +```bash +collar instance create my-instance \ + --provider-type jail \ + --provider-config '{}' \ + --metadata '{"env": "production", "version": "1.0"}' +``` + +### Get Instance Details + +```bash +collar instance get my-instance +``` + +### Delete Instance + +```bash +collar instance delete my-instance +``` + +### Show Examples + +```bash +collar instance create --examples +``` + +## Configuration Examples + +### Network Provider Types + +Currently, the API supports basic network providers. Provider configuration is optional and can be passed as JSON. + +### Network Assigner Types + +- **basic**: Sequential IP assignment + ```json + {"strategy": "sequential"} + ``` + +- **range**: Range-based IP assignment + ```json + {"range_name": "my-range"} + ``` + +### Instance Provider Types + +- **jail**: FreeBSD jail instances + ```json + { + "jail_conf": "/etc/jail.conf", + "dataset": "zroot/jails" + } + ``` + +- **container**: Container instances + ```json + { + "runtime": "docker", + "image": "alpine:latest" + } + ``` + +### Network Interfaces + +Network interfaces can be specified as a JSON array: +```json +[ + { + "network_name": "test-network", + "interface_name": "eth0" + } +] +``` + +### Metadata + +Instance metadata can be specified as a JSON object: +```json +{ + "env": "production", + "version": "1.0", + "owner": "team-infrastructure" +} +``` + +## Examples + +### Complete Workflow + +1. Create a network: +```bash +collar network create app-network 10.0.1.0/24 \ + --assigner-type basic \ + --assigner-config '{"strategy": "sequential"}' +``` + +2. Create an instance connected to the network: +```bash +collar instance create web-server \ + --provider-type container \ + --provider-config '{"runtime": "docker", "image": "nginx:latest"}' \ + --network-interfaces '[{"network_name": "app-network", "interface_name": "eth0"}]' \ + --metadata '{"role": "web-server", "env": "production"}' +``` + +3. List resources: +```bash +collar network list +collar instance list +``` + +4. Get details: +```bash +collar network get app-network +collar instance get web-server +``` + +5. Clean up: +```bash +collar instance delete web-server +collar network delete app-network +``` + +## Error Handling + +The CLI provides descriptive error messages for common issues: + +- Invalid JSON configuration +- Network/instance not found +- Server connection errors +- Invalid provider or assigner types + +## Development + +### Running Tests + +```bash +cargo test -p collar-cli +``` + +### Building + +```bash +cargo build -p collar-cli +``` + +### Running from Source + +```bash +cargo run -p collar-cli -- network list +``` \ No newline at end of file diff --git a/crates/cli/examples/demo.sh b/crates/cli/examples/demo.sh new file mode 100755 index 0000000..c1d321c --- /dev/null +++ b/crates/cli/examples/demo.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# Collar CLI Demo Script +# Demonstrates various CLI operations for network and instance management + +set -e + +# Configuration +API_URL="http://localhost:3000" +COLLAR_CLI="cargo run -p collar-cli --" + +echo "=== Collar CLI Demo ===" +echo "API URL: $API_URL" +echo + +# Function to run CLI command with proper formatting +run_cli() { + echo "$ collar $*" + $COLLAR_CLI --url "$API_URL" "$@" + echo +} + +# Function to handle errors gracefully +run_cli_safe() { + echo "$ collar $*" + if ! $COLLAR_CLI --url "$API_URL" "$@"; then + echo "Command failed (this might be expected if server is not running)" + fi + echo +} + +echo "=== CLI Help ===" +run_cli --help + +echo "=== Network Management ===" + +echo "--- Show network examples ---" +run_cli network create --examples + +echo "--- List networks (may fail if server not running) ---" +run_cli_safe network list + +echo "--- Create a basic network (may fail if server not running) ---" +run_cli_safe network create demo-network 192.168.100.0/24 + +echo "--- Create network with basic assigner (may fail if server not running) ---" +run_cli_safe network create demo-basic-network 192.168.101.0/24 \ + --assigner-type basic \ + --assigner-config '{"strategy": "sequential"}' + +echo "--- Get network details (may fail if server not running) ---" +run_cli_safe network get demo-network + +echo "=== Instance Management ===" + +echo "--- Show instance examples ---" +run_cli instance create --examples + +echo "--- List instances (may fail if server not running) ---" +run_cli_safe instance list + +echo "--- Create jail instance (may fail if server not running) ---" +run_cli_safe instance create demo-jail \ + --provider-type jail \ + --provider-config '{"jail_conf": "/etc/jail.conf", "dataset": "zroot/jails"}' + +echo "--- Create container instance (may fail if server not running) ---" +run_cli_safe instance create demo-container \ + --provider-type container \ + --provider-config '{"runtime": "docker", "image": "alpine:latest"}' + +echo "--- Create instance with network interface (may fail if server not running) ---" +run_cli_safe instance create demo-networked \ + --provider-type jail \ + --provider-config '{}' \ + --network-interfaces '[{"network_name": "demo-network", "interface_name": "eth0"}]' \ + --metadata '{"env": "demo", "purpose": "testing"}' + +echo "--- Get instance details (may fail if server not running) ---" +run_cli_safe instance get demo-jail + +echo "=== Cleanup ===" + +echo "--- Delete instances (may fail if server not running) ---" +run_cli_safe instance delete demo-jail +run_cli_safe instance delete demo-container +run_cli_safe instance delete demo-networked + +echo "--- Delete networks (may fail if server not running) ---" +run_cli_safe network delete demo-network +run_cli_safe network delete demo-basic-network + +echo "=== Demo Complete ===" +echo "Note: Some commands may have failed if the Collar API server is not running." +echo "To start the server, run the appropriate collar daemon command first." \ No newline at end of file diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000..339946c --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,416 @@ +//! # Collar CLI +//! +//! Command-line interface for the Collar API server. +//! +//! Provides commands for managing networks and instances through the HTTP API. + +use std::collections::HashMap; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use url::Url; + +mod utils; +use utils::*; + +#[derive(Parser)] +#[command(name = "collar")] +#[command(about = "CLI client for Collar API server")] +#[command(version)] +struct Cli { + #[arg(long, default_value = "http://localhost:3000")] + /// Base URL of the Collar API server + url: String, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Network management commands + Network { + #[command(subcommand)] + command: NetworkCommands, + }, + /// Instance management commands + Instance { + #[command(subcommand)] + command: InstanceCommands, + }, +} + +#[derive(Subcommand)] +enum NetworkCommands { + /// List all networks + List, + /// Create a new network + Create { + /// Network name + name: Option<String>, + /// Network CIDR (e.g., 192.168.1.0/24) + cidr: Option<String>, + /// Provider type (optional) + #[arg(long)] + provider_type: Option<String>, + /// Provider config as JSON string (optional) + #[arg(long)] + provider_config: Option<String>, + /// Assigner type (basic, range) + #[arg(long)] + assigner_type: Option<String>, + /// Assigner config as JSON string (optional) + #[arg(long)] + assigner_config: Option<String>, + /// Show example configurations + #[arg(long)] + examples: bool, + }, + /// Get network details + Get { + /// Network name + name: String, + }, + /// Delete a network + Delete { + /// Network name + name: String, + }, +} + +#[derive(Subcommand)] +enum InstanceCommands { + /// List all instances + List, + /// Create a new instance + Create { + /// Instance name + name: Option<String>, + /// Provider type (jail, container) + #[arg(long)] + provider_type: Option<String>, + /// Provider config as JSON string + #[arg(long)] + provider_config: Option<String>, + /// Network interfaces as JSON array string (optional) + #[arg(long)] + network_interfaces: Option<String>, + /// Metadata as JSON object string (optional) + #[arg(long)] + metadata: Option<String>, + /// Show example configurations + #[arg(long)] + examples: bool, + }, + /// Get instance details + Get { + /// Instance name + name: String, + }, + /// Delete an instance + Delete { + /// Instance name + name: String, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +struct CreateNetworkRequest { + name: String, + cidr: String, + provider_type: Option<String>, + provider_config: Option<Value>, + assigner_type: Option<String>, + assigner_config: Option<Value>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct CreateInstanceRequest { + name: String, + provider_type: String, + provider_config: Value, + network_interfaces: Option<Vec<NetworkInterfaceRequest>>, + metadata: Option<HashMap<String, String>>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct NetworkInterfaceRequest { + network_name: String, + interface_name: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + let client = Client::new(); + let base_url = Url::parse(&cli.url)?; + + match cli.command { + Commands::Network { command } => handle_network_command(&client, &base_url, command).await?, + Commands::Instance { command } => handle_instance_command(&client, &base_url, command).await?, + } + + Ok(()) +} + +async fn handle_network_command(client: &Client, base_url: &Url, command: NetworkCommands) -> Result<()> { + match command { + NetworkCommands::List => { + let url = base_url.join("/networks")?; + let response = client.get(url).send().await?; + + if response.status().is_success() { + let networks: Value = response.json().await?; + println!("{}", format_response(&networks)?); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + NetworkCommands::Create { + name, + cidr, + provider_type, + provider_config, + assigner_type, + assigner_config, + examples, + } => { + if examples { + println!("Example network configurations:"); + println!("\nBasic network:"); + println!("collar --url http://localhost:3000 network create my-network 192.168.1.0/24"); + + println!("\nNetwork with basic assigner:"); + println!("collar network create my-network 192.168.1.0/24 \\"); + println!(" --assigner-type basic \\"); + println!(" --assigner-config '{}'", serde_json::to_string(&example_basic_assigner_config())?); + + println!("\nNetwork with range assigner:"); + println!("collar network create my-network 192.168.1.0/24 \\"); + println!(" --assigner-type range \\"); + println!(" --assigner-config '{}'", serde_json::to_string(&example_range_assigner_config())?); + + return Ok(()); + } + + // Require name and cidr if not showing examples + let name = name.ok_or_else(|| anyhow::anyhow!("Network name is required"))?; + let cidr = cidr.ok_or_else(|| anyhow::anyhow!("Network CIDR is required"))?; + + // Validate assigner type if provided + if let Some(ref assigner) = assigner_type { + validate_assigner_type(assigner)?; + } + let provider_config = if let Some(config) = provider_config { + Some(parse_json_string(&config)?) + } else { + None + }; + + let assigner_config = if let Some(config) = assigner_config { + Some(parse_json_string(&config)?) + } else { + None + }; + + let request = CreateNetworkRequest { + name: name.clone(), + cidr: cidr.clone(), + provider_type, + provider_config, + assigner_type, + assigner_config, + }; + + let url = base_url.join("/networks")?; + let response = client.post(url).json(&request).send().await?; + + if response.status().is_success() { + println!("Network '{}' created successfully", name); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + NetworkCommands::Get { name } => { + let url = base_url.join(&format!("/networks/{}", name))?; + let response = client.get(url).send().await?; + + if response.status().is_success() { + let network: Value = response.json().await?; + println!("{}", format_response(&network)?); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + NetworkCommands::Delete { name } => { + let url = base_url.join(&format!("/networks/{}", name))?; + let response = client.delete(url).send().await?; + + if response.status().is_success() { + println!("Network '{}' deleted successfully", name); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + } + + Ok(()) +} + +async fn handle_instance_command(client: &Client, base_url: &Url, command: InstanceCommands) -> Result<()> { + match command { + InstanceCommands::List => { + let url = base_url.join("/instances")?; + let response = client.get(url).send().await?; + + if response.status().is_success() { + let instances: Value = response.json().await?; + println!("{}", format_response(&instances)?); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + InstanceCommands::Create { + name, + provider_type, + provider_config, + network_interfaces, + metadata, + examples, + } => { + if examples { + println!("Example instance configurations:"); + println!("\nJail instance:"); + println!("collar instance create my-jail \\"); + println!(" --provider-type jail \\"); + println!(" --provider-config '{}'", serde_json::to_string(&example_jail_config())?); + + println!("\nContainer instance:"); + println!("collar instance create my-container \\"); + println!(" --provider-type container \\"); + println!(" --provider-config '{}'", serde_json::to_string(&example_container_config())?); + + println!("\nInstance with network interfaces:"); + println!("collar instance create networked-instance \\"); + println!(" --provider-type jail \\"); + println!(" --provider-config '{{}}' \\"); + println!(" --network-interfaces '{}'", serde_json::to_string(&example_network_interfaces())?); + + return Ok(()); + } + + // Require name and provider details if not showing examples + let name = name.ok_or_else(|| anyhow::anyhow!("Instance name is required"))?; + let provider_type = provider_type.ok_or_else(|| anyhow::anyhow!("Provider type is required"))?; + let provider_config = provider_config.ok_or_else(|| anyhow::anyhow!("Provider config is required"))?; + + // Validate provider type + validate_provider_type(&provider_type)?; + let provider_config: Value = parse_json_string(&provider_config)?; + + let network_interfaces = if let Some(interfaces) = network_interfaces { + Some(serde_json::from_str(&interfaces)?) + } else { + None + }; + + let metadata = if let Some(meta) = metadata { + Some(parse_metadata(&meta)?) + } else { + None + }; + + let request = CreateInstanceRequest { + name: name.clone(), + provider_type, + provider_config, + network_interfaces, + metadata, + }; + + let url = base_url.join("/instances")?; + let response = client.post(url).json(&request).send().await?; + + if response.status().is_success() { + println!("Instance '{}' created successfully", name); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + InstanceCommands::Get { name } => { + let url = base_url.join(&format!("/instances/{}", name))?; + let response = client.get(url).send().await?; + + if response.status().is_success() { + let instance: Value = response.json().await?; + println!("{}", format_response(&instance)?); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + InstanceCommands::Delete { name } => { + let url = base_url.join(&format!("/instances/{}", name))?; + let response = client.delete(url).send().await?; + + if response.status().is_success() { + println!("Instance '{}' deleted successfully", name); + } else { + let error = response.text().await?; + eprintln!("Error: {}", error); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::process::Command; + + #[test] + fn test_cli_help() { + let output = Command::new("cargo") + .args(&["run", "--bin", "collar", "--", "--help"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("CLI client for Collar API server")); + } + + #[test] + fn test_network_help() { + let output = Command::new("cargo") + .args(&["run", "--bin", "collar", "--", "network", "--help"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Network management commands")); + } + + #[test] + fn test_instance_help() { + let output = Command::new("cargo") + .args(&["run", "--bin", "collar", "--", "instance", "--help"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Instance management commands")); + } +} \ No newline at end of file diff --git a/crates/cli/src/utils.rs b/crates/cli/src/utils.rs new file mode 100644 index 0000000..9a5cb43 --- /dev/null +++ b/crates/cli/src/utils.rs @@ -0,0 +1,120 @@ +//! Utility functions for the CLI client + +use anyhow::{Context, Result}; +use serde_json::Value; +use std::collections::HashMap; + +/// Parse a JSON string into a serde_json::Value +pub fn parse_json_string(json_str: &str) -> Result<Value> { + serde_json::from_str(json_str).context("Failed to parse JSON string") +} + +/// Parse a JSON string into a HashMap for metadata +pub fn parse_metadata(json_str: &str) -> Result<HashMap<String, String>> { + serde_json::from_str(json_str).context("Failed to parse metadata JSON") +} + +/// Format response output with proper indentation +pub fn format_response(value: &Value) -> Result<String> { + serde_json::to_string_pretty(value).context("Failed to format response") +} + +/// Validate provider type for instances +pub fn validate_provider_type(provider_type: &str) -> Result<()> { + match provider_type { + "jail" | "container" => Ok(()), + _ => Err(anyhow::anyhow!( + "Invalid provider type '{}'. Must be 'jail' or 'container'", + provider_type + )), + } +} + +/// Validate assigner type for networks +pub fn validate_assigner_type(assigner_type: &str) -> Result<()> { + match assigner_type { + "basic" | "range" => Ok(()), + _ => Err(anyhow::anyhow!( + "Invalid assigner type '{}'. Must be 'basic' or 'range'", + assigner_type + )), + } +} + +/// Build example configurations for different provider types +pub fn example_jail_config() -> Value { + serde_json::json!({ + "jail_conf": "/etc/jail.conf", + "dataset": "zroot/jails" + }) +} + +pub fn example_container_config() -> Value { + serde_json::json!({ + "runtime": "docker", + "image": "alpine:latest" + }) +} + +pub fn example_basic_assigner_config() -> Value { + serde_json::json!({ + "strategy": "sequential" + }) +} + +pub fn example_range_assigner_config() -> Value { + serde_json::json!({ + "range_name": "my-range" + }) +} + +pub fn example_network_interfaces() -> Value { + serde_json::json!([ + { + "network_name": "test-network", + "interface_name": "eth0" + } + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_json_string() { + let json_str = r#"{"key": "value"}"#; + let result = parse_json_string(json_str).unwrap(); + assert_eq!(result["key"], "value"); + } + + #[test] + fn test_parse_metadata() { + let json_str = r#"{"env": "production", "version": "1.0"}"#; + let result = parse_metadata(json_str).unwrap(); + assert_eq!(result.get("env"), Some(&"production".to_string())); + assert_eq!(result.get("version"), Some(&"1.0".to_string())); + } + + #[test] + fn test_validate_provider_type() { + assert!(validate_provider_type("jail").is_ok()); + assert!(validate_provider_type("container").is_ok()); + assert!(validate_provider_type("invalid").is_err()); + } + + #[test] + fn test_validate_assigner_type() { + assert!(validate_assigner_type("basic").is_ok()); + assert!(validate_assigner_type("range").is_ok()); + assert!(validate_assigner_type("invalid").is_err()); + } + + #[test] + fn test_format_response() { + let value = serde_json::json!({"name": "test", "value": 42}); + let formatted = format_response(&value).unwrap(); + assert!(formatted.contains("\"name\": \"test\"")); + assert!(formatted.contains("\"value\": 42")); + } +} \ No newline at end of file diff --git a/crates/cli/tests/integration_tests.rs b/crates/cli/tests/integration_tests.rs new file mode 100644 index 0000000..864637d --- /dev/null +++ b/crates/cli/tests/integration_tests.rs @@ -0,0 +1,177 @@ +//! Integration tests for the Collar CLI +//! +//! These tests verify CLI functionality by spawning the CLI process +//! and checking its output and behavior. + +use std::process::Command; + +#[tokio::test] +async fn test_cli_help() { + let output = Command::new("cargo") + .args(&["run", "-p", "collar-cli", "--", "--help"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("CLI client for Collar API server")); + assert!(stdout.contains("network")); + assert!(stdout.contains("instance")); +} + +#[tokio::test] +async fn test_network_help() { + let output = Command::new("cargo") + .args(&["run", "-p", "collar-cli", "--", "network", "--help"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Network management commands")); + assert!(stdout.contains("list")); + assert!(stdout.contains("create")); + assert!(stdout.contains("get")); + assert!(stdout.contains("delete")); +} + +#[tokio::test] +async fn test_instance_help() { + let output = Command::new("cargo") + .args(&["run", "-p", "collar-cli", "--", "instance", "--help"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Instance management commands")); + assert!(stdout.contains("list")); + assert!(stdout.contains("create")); + assert!(stdout.contains("get")); + assert!(stdout.contains("delete")); +} + +#[tokio::test] +async fn test_network_create_examples() { + let output = Command::new("cargo") + .args(&["run", "-p", "collar-cli", "--", "network", "create", "--examples"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Example network configurations")); + assert!(stdout.contains("Basic network")); + assert!(stdout.contains("basic assigner")); + assert!(stdout.contains("range assigner")); +} + +#[tokio::test] +async fn test_instance_create_examples() { + let output = Command::new("cargo") + .args(&["run", "-p", "collar-cli", "--", "instance", "create", "--examples"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("Example instance configurations")); + assert!(stdout.contains("Jail instance")); + assert!(stdout.contains("Container instance")); + assert!(stdout.contains("network interfaces")); +} + +#[tokio::test] +async fn test_network_list_connection_error() { + let output = Command::new("cargo") + .args(&[ + "run", "-p", "collar-cli", "--", + "--url", "http://localhost:9999", + "network", "list" + ]) + .output() + .expect("Failed to execute command"); + + // Should fail with connection error since server isn't running on port 9999 + assert!(!output.status.success() || !String::from_utf8(output.stderr).unwrap().is_empty()); +} + +#[tokio::test] +async fn test_instance_list_connection_error() { + let output = Command::new("cargo") + .args(&[ + "run", "-p", "collar-cli", "--", + "--url", "http://localhost:9999", + "instance", "list" + ]) + .output() + .expect("Failed to execute command"); + + // Should fail with connection error since server isn't running on port 9999 + assert!(!output.status.success() || !String::from_utf8(output.stderr).unwrap().is_empty()); +} + +#[tokio::test] +async fn test_invalid_provider_type_validation() { + let output = Command::new("cargo") + .args(&[ + "run", "-p", "collar-cli", "--", + "--url", "http://localhost:9999", + "instance", "create", "test-instance", + "--provider-type", "invalid", + "--provider-config", "{}" + ]) + .output() + .expect("Failed to execute command"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains("Invalid provider type")); +} + +#[tokio::test] +async fn test_invalid_assigner_type_validation() { + let output = Command::new("cargo") + .args(&[ + "run", "-p", "collar-cli", "--", + "--url", "http://localhost:9999", + "network", "create", "test-network", "192.168.1.0/24", + "--assigner-type", "invalid" + ]) + .output() + .expect("Failed to execute command"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains("Invalid assigner type")); +} + +#[tokio::test] +async fn test_invalid_json_config() { + let output = Command::new("cargo") + .args(&[ + "run", "-p", "collar-cli", "--", + "--url", "http://localhost:9999", + "instance", "create", "test-instance", + "--provider-type", "jail", + "--provider-config", "invalid-json" + ]) + .output() + .expect("Failed to execute command"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains("Failed to parse JSON")); +} + +#[tokio::test] +async fn test_version_flag() { + let output = Command::new("cargo") + .args(&["run", "-p", "collar-cli", "--", "--version"]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("collar")); +} \ No newline at end of file diff --git a/crates/store/examples/jail_auto_ip_demo.rs b/crates/store/examples/jail_auto_ip_demo.rs new file mode 100644 index 0000000..68dc122 --- /dev/null +++ b/crates/store/examples/jail_auto_ip_demo.rs @@ -0,0 +1,229 @@ +use store::{ + InstanceStore, Instance, InstanceConfig, NetworkInterface, + NamespaceStore, NetworkStore, RangeStore, + JailProvider, Provider, + network::{NetRange, BasicProvider, RangeAssigner}, +}; +use std::collections::HashMap; + +fn main() -> store::Result<()> { + println!("=== Jail Automatic IP Assignment Demo ===\n"); + + // Open the store + let db = sled::open("jail_auto_ip_demo.db")?; + + // Set up stores + let namespaces = NamespaceStore::open(&db)?; + let ranges = RangeStore::open(&db)?; + let networks = NetworkStore::open_with_ranges(&db, ranges)?; + let instances = InstanceStore::open(&db, namespaces, networks)?; + + println!("1. Creating network with RangeAssigner..."); + + // Create a network with automatic IP assignment + let provider = BasicProvider::new() + .with_config("type", "bridge") + .with_config("driver", "epair"); + + let assigner = RangeAssigner::with_range_name("jail_ips") + .with_config("strategy", "sequential") + .with_config("reserve_gateway", "true"); + + let netrange = NetRange::from_cidr("10.0.20.0/24")?; + + instances.networks().create( + "jail_network", + netrange, + provider, + Some(assigner), + )?; + + println!(" ✓ Created network 'jail_network' with range 10.0.20.0/24"); + + println!("\n2. Creating jails with automatic IP assignment..."); + + // Create multiple jail instances - they should automatically get IP assignments + let jail_names = ["web-jail", "db-jail", "cache-jail"]; + + for jail_name in &jail_names { + let jail_provider = JailProvider::new() + .with_dataset("tank/jails".to_string()); + + let jail_config = InstanceConfig { + provider_config: jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "jail_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, // Will be automatically assigned + } + ], + metadata: { + let mut meta = HashMap::new(); + meta.insert("role".to_string(), jail_name.replace("-jail", "")); + meta + }, + }; + + let jail_instance = Instance { + name: jail_name.to_string(), + provider_type: "jail".to_string(), + config: jail_config, + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + updated_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }; + + let created = instances.create(jail_instance)?; + + if let Some(ref assignment) = created.config.network_interfaces[0].assignment { + println!(" ✓ Created jail '{}' with IP: {}", jail_name, assignment); + } else { + println!(" ✗ Failed to assign IP to jail '{}'", jail_name); + } + } + + println!("\n3. Verifying all jails have unique IP assignments..."); + + let mut assigned_ips = std::collections::HashSet::new(); + + for jail_name in &jail_names { + let instance = instances.get(jail_name)?; + let interface = &instance.config.network_interfaces[0]; + + if let Some(ref assignment) = interface.assignment { + if assigned_ips.insert(assignment.clone()) { + println!(" ✓ {} -> {}", jail_name, assignment); + } else { + println!(" ✗ Duplicate IP assignment for {}: {}", jail_name, assignment); + return Err(store::Error::StoreError(sled::Error::Unsupported( + "Duplicate IP assignment detected".to_string() + ))); + } + } else { + println!(" ✗ No IP assignment found for {}", jail_name); + } + } + + println!("\n4. Creating jail with multiple network interfaces..."); + + // Create another network for demonstration + let mgmt_provider = BasicProvider::new(); + let mgmt_assigner = RangeAssigner::with_range_name("mgmt_ips"); + instances.networks().create( + "management", + NetRange::from_cidr("192.168.1.0/24")?, + mgmt_provider, + Some(mgmt_assigner), + )?; + + // Create a jail with multiple interfaces + let multi_jail_provider = JailProvider::new(); + let multi_jail_config = InstanceConfig { + provider_config: multi_jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "jail_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, + }, + NetworkInterface { + network_name: "management".to_string(), + interface_name: "em1".to_string(), + assignment: None, + }, + ], + metadata: HashMap::new(), + }; + + let multi_jail = Instance { + name: "multi-interface-jail".to_string(), + provider_type: "jail".to_string(), + config: multi_jail_config, + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + updated_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }; + + let created_multi = instances.create(multi_jail)?; + + println!(" ✓ Created multi-interface jail:"); + for (i, interface) in created_multi.config.network_interfaces.iter().enumerate() { + if let Some(ref assignment) = interface.assignment { + println!(" Interface {}: {} -> {}", i, interface.network_name, assignment); + } + } + + println!("\n5. Testing deletion and IP cleanup..."); + + // Delete the first jail + instances.delete("web-jail")?; + println!(" ✓ Deleted jail 'web-jail'"); + + // Verify the IP is cleaned up + let network = instances.networks().get("jail_network")?.unwrap(); + let assignment_after_delete = network.get_assignment("web-jail", instances.networks().ranges())?; + if assignment_after_delete.is_none() { + println!(" ✓ IP assignment cleaned up after deletion"); + } else { + println!(" ✗ IP assignment still exists after deletion"); + } + + // Create a new jail to demonstrate IP reuse + let reuse_jail_provider = JailProvider::new(); + let reuse_jail_config = InstanceConfig { + provider_config: reuse_jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "jail_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, + } + ], + metadata: HashMap::new(), + }; + + let reuse_jail = Instance { + name: "replacement-jail".to_string(), + provider_type: "jail".to_string(), + config: reuse_jail_config, + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + updated_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }; + + let created_reuse = instances.create(reuse_jail)?; + + if let Some(ref new_assignment) = created_reuse.config.network_interfaces[0].assignment { + println!(" ✓ New jail 'replacement-jail' reused IP: {}", new_assignment); + } + + println!("\n✅ Demonstration completed successfully!"); + println!("\nKey points:"); + println!(" • Jails automatically receive IP assignments when created"); + println!(" • Each jail gets a unique IP from the network's range"); + println!(" • Multiple network interfaces are supported"); + println!(" • RangeAssigner handles sequential IP allocation"); + println!(" • IP assignments are automatically cleaned up on deletion"); + println!(" • Freed IPs can be reused by new instances"); + + // Clean up + std::fs::remove_dir_all("jail_auto_ip_demo.db").ok(); + + Ok(()) +} \ No newline at end of file diff --git a/crates/store/src/instance.rs b/crates/store/src/instance.rs index 3cf1d85..5d03794 100644 --- a/crates/store/src/instance.rs +++ b/crates/store/src/instance.rs @@ -1,946 +1,1022 @@ use crate::{Result, Error, NamespaceStore, NetworkStore}; use serde::{Serialize, Deserialize}; use std::collections::HashMap; /// Generic provider trait for different virtualization backends pub trait Provider: Send + Sync { /// Get the provider type identifier fn provider_type(&self) -> &'static str; /// Serialize provider configuration to JSON fn to_json(&self) -> Result<String>; /// Deserialize provider configuration from JSON fn from_json(json: &str) -> Result<Self> where Self: Sized; /// Start the instance with the given configuration fn start(&self, name: &str, config: &InstanceConfig) -> Result<()>; /// Stop the instance fn stop(&self, name: &str) -> Result<()>; /// Check if instance is running fn is_running(&self, name: &str) -> Result<bool>; /// Get instance status information fn status(&self, name: &str) -> Result<ProviderStatus>; } /// Status information from a provider #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderStatus { pub running: bool, pub pid: Option<u32>, pub uptime: Option<u64>, pub memory_usage: Option<u64>, pub cpu_usage: Option<f64>, } /// Network interface assignment for an instance #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkInterface { pub network_name: String, pub interface_name: String, pub assignment: Option<String>, // IP assignment from network } /// Instance configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstanceConfig { pub provider_config: String, // JSON config specific to provider pub network_interfaces: Vec<NetworkInterface>, pub metadata: HashMap<String, String>, } /// Instance representation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Instance { pub name: String, pub provider_type: String, pub config: InstanceConfig, pub created_at: u64, pub updated_at: u64, } impl Instance { /// Create a new instance pub fn new(name: String, provider_type: String, config: InstanceConfig) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); Self { name, provider_type, config, created_at: now, updated_at: now, } } /// Update instance configuration pub fn update_config(&mut self, config: InstanceConfig) { self.config = config; self.updated_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); } } /// Store for managing instances #[derive(Debug, Clone)] pub struct InstanceStore { pub(crate) instances: sled::Tree, pub(crate) namespaces: NamespaceStore, pub(crate) networks: NetworkStore, } impl InstanceStore { const INSTANCE_NAMESPACE: &'static str = "instances"; /// Open instance store with existing namespace and network stores pub fn open( db: &sled::Db, namespaces: NamespaceStore, networks: NetworkStore, ) -> Result<Self> { let instances = db.open_tree("instances")?; let store = Self { instances, namespaces, networks, }; // Ensure instance namespace exists store.namespaces.define(Self::INSTANCE_NAMESPACE)?; Ok(store) } - /// Create a new instance with network assignments + /// Get reference to the network store + pub fn networks(&self) -> &NetworkStore { + &self.networks + } + + /// Create a new instance with automatic network IP assignments + /// + /// This method creates a new instance and automatically assigns IP addresses + /// to any network interfaces that are configured but don't have existing assignments. + /// The automatic assignment works with networks that have RangeAssigners configured. + /// + /// # Automatic IP Assignment Process + /// + /// For each network interface: + /// 1. Check if an IP assignment already exists + /// 2. If not, and the network has a RangeAssigner, automatically assign an IP + /// 3. Update the interface with the assigned IP address + /// + /// This enables seamless provisioning of jails, containers, and other instances + /// with proper network connectivity without manual IP management. pub fn create(&self, mut instance: Instance) -> Result<Instance> { // Reserve name in namespace self.namespaces.reserve(Self::INSTANCE_NAMESPACE, &instance.name, &instance.name)?; // Get network assignments for interfaces for interface in &mut instance.config.network_interfaces { if let Ok(Some(network)) = self.networks.get(&interface.network_name) { match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) { Ok(Some(assignment)) => { interface.assignment = Some(assignment.to_string()); } Ok(None) => { - // No assignment available + // No existing assignment, try to auto-assign if network has RangeAssigner + // This enables automatic IP allocation for instances with network interfaces + if let Some(ref ranges) = self.networks.ranges { + // Attempt automatic IP assignment using the network's RangeAssigner + if let Err(e) = network.auto_assign_ip(&instance.name, ranges) { + // Clean up reserved name on failure + let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name); + return Err(e); + } + + // Retrieve the newly assigned IP address + match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) { + Ok(Some(assignment)) => { + interface.assignment = Some(assignment.to_string()); + } + Ok(None) => { + // Auto-assignment failed silently or range is full + // This is not necessarily an error - some networks may not support auto-assignment + } + Err(e) => { + // Clean up reserved name on failure + let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name); + return Err(e); + } + } + } } Err(e) => { // Clean up reserved name on failure let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name); return Err(e); } } } } // Store instance let instance_json = serde_json::to_string(&instance) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))?; match self.instances.insert(&instance.name, instance_json.as_bytes()) { Ok(_) => Ok(instance), Err(e) => { // Clean up on failure let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name); Err(Error::StoreError(e)) } } } /// Create instance within a transaction pub fn create_in_transaction( &self, names_tree: &sled::transaction::TransactionalTree, spaces_tree: &sled::transaction::TransactionalTree, instances_tree: &sled::transaction::TransactionalTree, mut instance: Instance, ) -> std::result::Result<Instance, sled::transaction::ConflictableTransactionError<Error>> { // Reserve name in namespace within transaction match self.namespaces.reserve_in_transaction(names_tree, spaces_tree, &instance.name, Self::INSTANCE_NAMESPACE, &instance.name) { Ok(_) => {}, Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort( Error::StoreError(sled::Error::Unsupported(format!("Namespace reservation error: {:?}", e))) )), } // Get network assignments for interfaces for interface in &mut instance.config.network_interfaces { if let Ok(Some(network)) = self.networks.get(&interface.network_name) { match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) { Ok(Some(assignment)) => { interface.assignment = Some(assignment.to_string()); } Ok(None) => { - // No assignment available + // No existing assignment, try to auto-assign if network has RangeAssigner + // This enables automatic IP allocation for instances with network interfaces + if let Some(ref ranges) = self.networks.ranges { + // Attempt automatic IP assignment using the network's RangeAssigner + if let Err(e) = network.auto_assign_ip(&instance.name, ranges) { + return Err(sled::transaction::ConflictableTransactionError::Abort(e)); + } + + // Retrieve the newly assigned IP address + match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) { + Ok(Some(assignment)) => { + interface.assignment = Some(assignment.to_string()); + } + Ok(None) => { + // Auto-assignment failed silently or range is full + // This is not necessarily an error - some networks may not support auto-assignment + } + Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort(e)), + } + } } Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort(e)), } } } // Store instance in transaction let instance_json = serde_json::to_string(&instance) .map_err(|e| sled::transaction::ConflictableTransactionError::Abort( Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) ))?; instances_tree.insert(instance.name.as_bytes(), instance_json.as_bytes()) .map_err(|e| sled::transaction::ConflictableTransactionError::Abort( Error::StoreError(sled::Error::Unsupported(format!("Transaction insert error: {}", e))) ))?; Ok(instance) } /// Get an instance by name pub fn get(&self, name: &str) -> Result<Instance> { match self.instances.get(name)? { Some(data) => { let json = String::from_utf8(data.to_vec())?; serde_json::from_str(&json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))) } None => Err(Error::StoreError(sled::Error::Unsupported(format!("Instance '{}' not found", name)))), } } /// Update an existing instance pub fn update(&self, instance: Instance) -> Result<Instance> { // Check if instance exists if !self.exists(&instance.name)? { return Err(Error::StoreError(sled::Error::Unsupported(format!("Instance '{}' not found", instance.name)))); } let instance_json = serde_json::to_string(&instance) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))?; self.instances.insert(&instance.name, instance_json.as_bytes())?; Ok(instance) } - /// Delete an instance + /// Delete an instance and clean up IP assignments + /// + /// This method removes the instance and automatically unassigns any IP addresses + /// that were allocated to the instance's network interfaces. This ensures proper + /// cleanup of network resources and allows IPs to be reused by new instances. pub fn delete(&self, name: &str) -> Result<()> { // Get instance to clean up network assignments if let Ok(instance) = self.get(name) { - // TODO: Clean up network assignments when unassign functionality is available - for _interface in &instance.config.network_interfaces { - // network.unassign_ip(&name)?; + // Clean up IP assignments for each network interface + for interface in &instance.config.network_interfaces { + if let Ok(Some(network)) = self.networks.get(&interface.network_name) { + // Only clean up if there's a range store and the network has a RangeAssigner + if let Some(ref ranges) = self.networks.ranges { + if let Err(e) = network.auto_unassign_ip(name, ranges) { + // Log the error but don't fail the deletion + eprintln!("Warning: Failed to clean up IP assignment for {} on network {}: {:?}", + name, interface.network_name, e); + } + } + } } } // Remove from instance store self.instances.remove(name)?; // Remove from namespace self.namespaces.remove(Self::INSTANCE_NAMESPACE, name)?; Ok(()) } /// Check if instance exists pub fn exists(&self, name: &str) -> Result<bool> { Ok(self.instances.contains_key(name)?) } /// List all instances pub fn list(&self) -> Result<Vec<Instance>> { let mut instances = Vec::new(); for item in self.instances.iter() { let (_, value_bytes) = item?; let json = String::from_utf8(value_bytes.to_vec())?; let instance: Instance = serde_json::from_str(&json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))))?; instances.push(instance); } instances.sort_by(|a, b| a.name.cmp(&b.name)); Ok(instances) } /// List instances by provider type pub fn list_by_provider(&self, provider_type: &str) -> Result<Vec<Instance>> { Ok(self.list()? .into_iter() .filter(|instance| instance.provider_type == provider_type) .collect()) } /// Check if an instance name is reserved in the namespace pub fn is_name_reserved(&self, name: &str) -> Result<bool> { self.namespaces.key_exists(Self::INSTANCE_NAMESPACE, name) } } // Example provider implementations /// Jail provider for FreeBSD jails #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JailProvider { pub jail_conf: String, pub dataset: Option<String>, } impl JailProvider { pub fn new() -> Self { Self { jail_conf: String::new(), dataset: None, } } pub fn with_dataset(mut self, dataset: String) -> Self { self.dataset = Some(dataset); self } } impl Provider for JailProvider { fn provider_type(&self) -> &'static str { "jail" } fn to_json(&self) -> Result<String> { serde_json::to_string(self) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))) } fn from_json(json: &str) -> Result<Self> { serde_json::from_str(json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))) } fn start(&self, name: &str, _config: &InstanceConfig) -> Result<()> { // TODO: Implement jail start logic println!("Starting jail: {}", name); Ok(()) } fn stop(&self, name: &str) -> Result<()> { // TODO: Implement jail stop logic println!("Stopping jail: {}", name); Ok(()) } fn is_running(&self, _name: &str) -> Result<bool> { // TODO: Implement jail status check Ok(false) } fn status(&self, _name: &str) -> Result<ProviderStatus> { // TODO: Implement jail status collection Ok(ProviderStatus { running: false, pid: None, uptime: None, memory_usage: None, cpu_usage: None, }) } } /// Container provider for Docker/Podman containers #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContainerProvider { pub runtime: String, // "docker" or "podman" pub image: String, pub registry: Option<String>, } impl ContainerProvider { pub fn docker(image: String) -> Self { Self { runtime: "docker".to_string(), image, registry: None, } } pub fn podman(image: String) -> Self { Self { runtime: "podman".to_string(), image, registry: None, } } } impl Provider for ContainerProvider { fn provider_type(&self) -> &'static str { "container" } fn to_json(&self) -> Result<String> { serde_json::to_string(self) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))) } fn from_json(json: &str) -> Result<Self> { serde_json::from_str(json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))) } fn start(&self, name: &str, _config: &InstanceConfig) -> Result<()> { // TODO: Implement container start logic println!("Starting {} container: {}", self.runtime, name); Ok(()) } fn stop(&self, name: &str) -> Result<()> { // TODO: Implement container stop logic println!("Stopping {} container: {}", self.runtime, name); Ok(()) } fn is_running(&self, _name: &str) -> Result<bool> { // TODO: Implement container status check Ok(false) } fn status(&self, _name: &str) -> Result<ProviderStatus> { // TODO: Implement container status collection Ok(ProviderStatus { running: false, pid: None, uptime: None, memory_usage: None, cpu_usage: None, }) } } #[cfg(test)] mod tests { use super::*; use crate::{NamespaceStore, NetworkStore, RangeStore}; use tempfile::TempDir; fn create_test_stores() -> Result<(TempDir, InstanceStore)> { let temp_dir = TempDir::new().unwrap(); let db = sled::open(temp_dir.path())?; let namespaces = NamespaceStore::open(&db)?; let ranges = RangeStore::open(&db)?; let networks = NetworkStore::open_with_ranges(&db, ranges)?; let instances = InstanceStore::open(&db, namespaces, networks)?; Ok((temp_dir, instances)) } #[test] fn test_create_instance() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance = Instance::new( "test-instance".to_string(), "jail".to_string(), config, ); let created = store.create(instance.clone())?; assert_eq!(created.name, instance.name); assert_eq!(created.provider_type, instance.provider_type); Ok(()) } #[test] fn test_get_instance() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance = Instance::new( "test-instance".to_string(), "jail".to_string(), config, ); store.create(instance.clone())?; let retrieved = store.get(&instance.name)?; assert_eq!(retrieved.name, instance.name); assert_eq!(retrieved.provider_type, instance.provider_type); Ok(()) } #[test] fn test_duplicate_instance_name() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance1 = Instance::new( "test-instance".to_string(), "jail".to_string(), config.clone(), ); let instance2 = Instance::new( "test-instance".to_string(), "container".to_string(), config, ); store.create(instance1)?; // Should fail due to duplicate name assert!(store.create(instance2).is_err()); Ok(()) } #[test] fn test_list_instances() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance1 = Instance::new( "instance1".to_string(), "jail".to_string(), config.clone(), ); let instance2 = Instance::new( "instance2".to_string(), "container".to_string(), config, ); store.create(instance1)?; store.create(instance2)?; let instances = store.list()?; assert_eq!(instances.len(), 2); let jail_instances = store.list_by_provider("jail")?; assert_eq!(jail_instances.len(), 1); assert_eq!(jail_instances[0].name, "instance1"); Ok(()) } #[test] fn test_delete_instance() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance = Instance::new( "test-instance".to_string(), "jail".to_string(), config, ); store.create(instance.clone())?; assert!(store.exists(&instance.name)?); store.delete(&instance.name)?; assert!(!store.exists(&instance.name)?); Ok(()) } #[test] fn test_jail_provider() -> Result<()> { let provider = JailProvider::new().with_dataset("tank/jails".to_string()); let json = provider.to_json()?; let deserialized = JailProvider::from_json(&json)?; assert_eq!(provider.dataset, deserialized.dataset); assert_eq!(provider.provider_type(), "jail"); Ok(()) } #[test] fn test_container_provider() -> Result<()> { let provider = ContainerProvider::docker("nginx:latest".to_string()); let json = provider.to_json()?; let deserialized = ContainerProvider::from_json(&json)?; assert_eq!(provider.runtime, deserialized.runtime); assert_eq!(provider.image, deserialized.image); assert_eq!(provider.provider_type(), "container"); Ok(()) } #[test] fn test_instance_with_range_assigner_network() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network with RangeAssigner use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("test_range"); store.networks.create( "test_network", NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create instance with network interface let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "test_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "test-with-network".to_string(), "jail".to_string(), config, ); let created = store.create(instance)?; // Verify instance was created with network assignment assert_eq!(created.name, "test-with-network"); assert_eq!(created.config.network_interfaces.len(), 1); let interface = &created.config.network_interfaces[0]; assert_eq!(interface.network_name, "test_network"); assert_eq!(interface.interface_name, "eth0"); // Note: IP assignment would be available if range was properly initialized // For this test, we verify the structure is correct Ok(()) } #[test] fn test_multiple_instances_different_ip_assignments() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network with RangeAssigner use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("multi_test_range"); store.networks.create( "multi_network", NetRange::ipv4("10.0.0.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create first instance let config1 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "multi_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance1 = Instance::new( "instance1".to_string(), "jail".to_string(), config1, ); // Create second instance let config2 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "multi_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance2 = Instance::new( "instance2".to_string(), "container".to_string(), config2, ); let created1 = store.create(instance1)?; let created2 = store.create(instance2)?; // Verify both instances have network interfaces assert_eq!(created1.config.network_interfaces.len(), 1); assert_eq!(created2.config.network_interfaces.len(), 1); // Verify they're connected to the same network assert_eq!(created1.config.network_interfaces[0].network_name, "multi_network"); assert_eq!(created2.config.network_interfaces[0].network_name, "multi_network"); // Both instances should exist in the store assert!(store.exists("instance1")?); assert!(store.exists("instance2")?); Ok(()) } #[test] fn test_instance_with_multiple_networks() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up multiple networks use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; // Management network let mgmt_provider = BasicProvider::new(); let mgmt_assigner = RangeAssigner::with_range_name("mgmt_range"); store.networks.create( "management", NetRange::ipv4("10.0.1.0".parse().unwrap(), 24)?, mgmt_provider, Some(mgmt_assigner), )?; // Public network let public_provider = BasicProvider::new(); let public_assigner = RangeAssigner::with_range_name("public_range"); store.networks.create( "public", NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?, public_provider, Some(public_assigner), )?; // Create instance with multiple network interfaces let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "management".to_string(), interface_name: "eth0".to_string(), assignment: None, }, NetworkInterface { network_name: "public".to_string(), interface_name: "eth1".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "multi-network-instance".to_string(), "container".to_string(), config, ); let created = store.create(instance)?; // Verify instance has both network interfaces assert_eq!(created.config.network_interfaces.len(), 2); let interfaces: Vec<&str> = created.config.network_interfaces .iter() .map(|i| i.network_name.as_str()) .collect(); assert!(interfaces.contains(&"management")); assert!(interfaces.contains(&"public")); Ok(()) } #[test] fn test_instance_network_assignment_cleanup_on_delete() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("cleanup_range"); store.networks.create( "cleanup_network", NetRange::ipv4("172.16.0.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create instance let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "cleanup_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "cleanup-test".to_string(), "jail".to_string(), config, ); let created = store.create(instance)?; assert!(store.exists("cleanup-test")?); // Delete the instance store.delete("cleanup-test")?; assert!(!store.exists("cleanup-test")?); // Verify instance is removed from namespace assert!(!store.namespaces.key_exists("instances", "cleanup-test")?); Ok(()) } #[test] fn test_instance_creation_fails_with_nonexistent_network() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Try to create instance with non-existent network let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "nonexistent_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "test-nonexistent-network".to_string(), "jail".to_string(), config, ); // This should succeed because network assignment is not enforced during creation // The instance will be created but without IP assignments let created = store.create(instance)?; assert_eq!(created.config.network_interfaces[0].assignment, None); Ok(()) } #[test] fn test_instance_transaction_rollback_preserves_state() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("tx_test_range"); store.networks.create( "tx_network", NetRange::ipv4("172.20.0.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create first instance successfully let config1 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "tx_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance1 = Instance::new( "tx-test-1".to_string(), "jail".to_string(), config1, ); store.create(instance1)?; assert!(store.exists("tx-test-1")?); // Verify namespace contains the instance assert!(store.namespaces.key_exists("instances", "tx-test-1")?); // Try to create duplicate instance (should fail) let config2 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance2 = Instance::new( "tx-test-1".to_string(), // Same name - should fail "container".to_string(), config2, ); // This should fail due to duplicate name assert!(store.create(instance2).is_err()); // Original instance should still exist assert!(store.exists("tx-test-1")?); assert!(store.namespaces.key_exists("instances", "tx-test-1")?); Ok(()) } } \ No newline at end of file diff --git a/crates/store/src/network.rs b/crates/store/src/network.rs index 2421c27..6c8a4f3 100644 --- a/crates/store/src/network.rs +++ b/crates/store/src/network.rs @@ -1,1535 +1,1664 @@ //! Network management module for storing and managing network configurations. //! //! This module provides functionality to create, read, update, and delete network //! configurations. Each network is defined by a name, network range (IPv4 or IPv6), //! a provider implementation, and an optional assigner implementation. //! //! # Example //! //! ``` //! use store::network::*; //! # use tempfile::TempDir; //! # fn example() -> store::Result<()> { //! # let temp_dir = tempfile::tempdir().unwrap(); //! # let db = sled::open(temp_dir.path())?; //! let store = NetworkStore::open(&db)?; //! //! // Create a network with IPv4 range //! let netrange = NetRange::from_cidr("192.168.1.0/24")?; //! let provider = BasicProvider::new() //! .with_config("type", "aws") //! .with_config("region", "us-west-2"); //! let assigner = Some(BasicAssigner::new("dhcp")); //! //! store.create("production", netrange, provider, assigner)?; //! //! // Retrieve the network //! let network = store.get("production")?.unwrap(); //! assert_eq!(network.name, "production"); //! //! // List all networks //! let networks = store.list()?; //! assert!(networks.contains(&"production".to_string())); //! # Ok(()) //! # } //! ``` use crate::{Result, Error, RangeStore}; use sled::Transactional; use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr, IpAddr}; /// Represents an IPv4 or IPv6 network range #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum NetRange { /// IPv4 network with address and prefix length V4 { addr: Ipv4Addr, prefix: u8 }, /// IPv6 network with address and prefix length V6 { addr: Ipv6Addr, prefix: u8 }, } impl NetRange { /// Create a new IPv4 network range pub fn ipv4(addr: Ipv4Addr, prefix: u8) -> Result<Self> { if prefix > 32 { return Err(Error::StoreError(sled::Error::Unsupported( format!("Invalid IPv4 prefix length: {}", prefix) ))); } Ok(NetRange::V4 { addr, prefix }) } /// Create a new IPv6 network range pub fn ipv6(addr: Ipv6Addr, prefix: u8) -> Result<Self> { if prefix > 128 { return Err(Error::StoreError(sled::Error::Unsupported( format!("Invalid IPv6 prefix length: {}", prefix) ))); } Ok(NetRange::V6 { addr, prefix }) } /// Parse a network range from CIDR notation pub fn from_cidr(cidr: &str) -> Result<Self> { let parts: Vec<&str> = cidr.split('/').collect(); if parts.len() != 2 { return Err(Error::StoreError(sled::Error::Unsupported( "Invalid CIDR format".to_string() ))); } let prefix: u8 = parts[1].parse().map_err(|_| { Error::StoreError(sled::Error::Unsupported("Invalid prefix length".to_string())) })?; if let Ok(ipv4) = parts[0].parse::<Ipv4Addr>() { Self::ipv4(ipv4, prefix) } else if let Ok(ipv6) = parts[0].parse::<Ipv6Addr>() { Self::ipv6(ipv6, prefix) } else { Err(Error::StoreError(sled::Error::Unsupported( "Invalid IP address format".to_string() ))) } } /// Convert to CIDR notation string pub fn to_cidr(&self) -> String { match self { NetRange::V4 { addr, prefix } => format!("{}/{}", addr, prefix), NetRange::V6 { addr, prefix } => format!("{}/{}", addr, prefix), } } } /// Trait for network providers pub trait NetworkProvider: std::fmt::Debug { /// Get the provider type identifier fn provider_type(&self) -> &'static str; /// Serialize provider configuration to JSON fn to_json(&self) -> Result<String>; /// Create provider from JSON fn from_json(json: &str) -> Result<Self> where Self: Sized; } /// Trait for network assigners pub trait NetworkAssigner: std::fmt::Debug { /// Get the assigner type identifier fn assigner_type(&self) -> &'static str; /// Serialize assigner configuration to JSON fn to_json(&self) -> Result<String>; /// Create assigner from JSON fn from_json(json: &str) -> Result<Self> where Self: Sized; /// Initialize the assigner for a network during creation fn initialize_for_network(&self, _network_name: &str, _netrange: &NetRange, _range_store: Option<&RangeStore>) -> Result<()> { // Default implementation does nothing Ok(()) } /// Initialize the assigner for a network within a transaction fn initialize_for_network_in_transaction( &self, _network_name: &str, _netrange: &NetRange, _range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)> ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> { // Default implementation does nothing Ok(()) } /// Cleanup the assigner for a network during deletion fn cleanup_for_network(&self, _network_name: &str, _range_store: Option<&RangeStore>) -> Result<()> { // Default implementation does nothing Ok(()) } /// Cleanup the assigner for a network within a transaction fn cleanup_for_network_in_transaction( &self, _network_name: &str, _range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)> ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> { // Default implementation does nothing Ok(()) } /// Get an IP assignment for the given identifier fn get_assignment(&self, _network: &Network, _identifier: &str, _range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> { // Default implementation returns None (no assignment tracking) Ok(None) } } /// Basic network provider implementation #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BasicProvider { pub config: HashMap<String, String>, } impl BasicProvider { pub fn new() -> Self { Self { config: HashMap::new(), } } pub fn with_config(mut self, key: &str, value: &str) -> Self { self.config.insert(key.to_string(), value.to_string()); self } } impl NetworkProvider for BasicProvider { fn provider_type(&self) -> &'static str { "basic" } fn to_json(&self) -> Result<String> { serde_json::to_string(self).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) }) } fn from_json(json: &str) -> Result<Self> { serde_json::from_str(json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) }) } } /// Basic network assigner implementation #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BasicAssigner { pub strategy: String, pub config: HashMap<String, String>, } impl BasicAssigner { pub fn new(strategy: &str) -> Self { Self { strategy: strategy.to_string(), config: HashMap::new(), } } pub fn with_config(mut self, key: &str, value: &str) -> Self { self.config.insert(key.to_string(), value.to_string()); self } } impl NetworkAssigner for BasicAssigner { fn assigner_type(&self) -> &'static str { "basic" } fn to_json(&self) -> Result<String> { serde_json::to_string(self).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) }) } fn from_json(json: &str) -> Result<Self> { serde_json::from_str(json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) }) } } /// Range-based network assigner that uses RangeStore for IP assignment /// /// The `RangeAssigner` provides IP address assignment functionality by integrating /// with the `RangeStore`. It automatically manages IP address allocation within /// a network range using a bitmap-based approach for efficient tracking. /// /// # Features /// /// - Automatic range initialization when creating networks /// - Sequential IP assignment within the network range /// - Assignment tracking with custom identifiers /// - IP unassignment and reuse /// - Integration with the NetworkStore lifecycle /// /// # Example /// /// ``` /// use store::network::{RangeAssigner, NetRange, BasicProvider, NetworkStore}; /// # use tempfile::TempDir; /// # fn example() -> store::Result<()> { /// # let temp_dir = tempfile::tempdir().unwrap(); /// # let db = sled::open(temp_dir.path())?; /// # let ranges = store::range::RangeStore::open(&db)?; /// # let store = NetworkStore::open_with_ranges(&db, ranges.clone())?; /// /// // Create a RangeAssigner for a network (uses network name as range name) /// let assigner = RangeAssigner::new() /// .with_config("strategy", "sequential"); /// /// // Create a network with the range assigner /// let netrange = NetRange::from_cidr("192.168.1.0/24")?; /// let provider = BasicProvider::new(); /// store.create("my-network", netrange, provider, Some(assigner))?; /// /// // Use the assigner to allocate IPs /// let range_assigner = RangeAssigner::new(); /// let ip_bit = range_assigner.assign_ip("my-network", &ranges, "device1")?; /// # Ok(()) /// # } /// ``` #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RangeAssigner { pub range_name: Option<String>, pub config: HashMap<String, String>, } impl RangeAssigner { /// Create a new RangeAssigner with default settings /// /// The range name will default to the network name when the network is created. /// Use `with_range_name()` to specify a custom range name if needed. pub fn new() -> Self { Self { range_name: None, config: HashMap::new(), } } /// Create a new RangeAssigner with a specific range name /// /// # Arguments /// /// * `range_name` - Custom identifier for the IP range pub fn with_range_name(range_name: &str) -> Self { Self { range_name: Some(range_name.to_string()), config: HashMap::new(), } } /// Add configuration key-value pairs to the assigner /// /// Configuration options can be used to customize the behavior of the /// assigner. Common configuration keys might include: /// - `strategy`: Assignment strategy (e.g., "sequential", "random") /// - `reserve_gateway`: Whether to reserve the first IP for gateway /// - `pool_size`: Maximum number of IPs to manage /// /// # Arguments /// /// * `key` - Configuration key /// * `value` - Configuration value pub fn with_config(mut self, key: &str, value: &str) -> Self { self.config.insert(key.to_string(), value.to_string()); self } /// Initialize the range for this assigner with the given network size /// /// This method calculates the number of available IP addresses in the network /// range and creates a corresponding range in the RangeStore. It's automatically /// called when creating a network with a RangeAssigner. /// /// # Arguments /// /// * `range_name` - Name to use for the range /// * `range_store` - Reference to the RangeStore /// * `network` - Network range (IPv4 or IPv6) to initialize /// /// # Returns /// /// Returns `Ok(())` if the range was successfully initialized, or an error /// if the network parameters are invalid or the range already exists. pub fn initialize_range(&self, range_name: &str, range_store: &RangeStore, network: &NetRange) -> Result<()> { let size = match network { NetRange::V4 { prefix, .. } => { if *prefix >= 32 { return Err(Error::StoreError(sled::Error::Unsupported( "IPv4 prefix must be less than 32".to_string() ))); } 1u64 << (32 - prefix) } NetRange::V6 { prefix, .. } => { if *prefix >= 128 { return Err(Error::StoreError(sled::Error::Unsupported( "IPv6 prefix must be less than 128".to_string() ))); } // For IPv6, we'll limit to a reasonable size to avoid huge ranges let host_bits = 128 - prefix; if host_bits > 32 { 1u64 << 32 // Cap at 2^32 addresses } else { 1u64 << host_bits } } }; range_store.define(range_name, size) } /// Assign an IP address from the range /// /// Assigns the next available IP address in the range to the specified identifier. /// The assignment uses a sequential allocation strategy, finding the first /// available bit position in the range bitmap. /// /// # Arguments /// /// * `range_name` - Name of the range to assign from /// * `range_store` - Reference to the RangeStore /// * `identifier` - Unique identifier for the device/entity receiving the IP /// /// # Returns /// /// Returns the bit position of the assigned IP address, which can be used /// to calculate the actual IP address within the network range. /// /// # Errors /// /// Returns an error if the range is full or doesn't exist. pub fn assign_ip(&self, range_name: &str, range_store: &RangeStore, identifier: &str) -> Result<u64> { range_store.assign(range_name, identifier) } /// Get assigned IP information /// /// Retrieves the identifier associated with a specific bit position in the range. /// This is useful for determining which device or entity is assigned to a /// particular IP address. /// /// # Arguments /// /// * `range_name` - Name of the range to query /// * `range_store` - Reference to the RangeStore /// * `bit_position` - The bit position to query /// /// # Returns /// /// Returns `Some(identifier)` if the bit position is assigned, or `None` /// if it's free or out of range. pub fn get_assignment_by_bit(&self, range_name: &str, range_store: &RangeStore, bit_position: u64) -> Result<Option<String>> { range_store.get(range_name, bit_position) } /// Unassign an IP address /// /// Releases an IP address assignment, making it available for future assignments. /// This is typically called when a device is removed or no longer needs its /// assigned IP address. /// /// # Arguments /// /// * `range_name` - Name of the range to unassign from /// * `range_store` - Reference to the RangeStore /// * `bit_position` - The bit position to unassign /// /// # Returns /// /// Returns `true` if the IP was successfully unassigned, or `false` if it /// was already free or the bit position is out of range. pub fn unassign_ip(&self, range_name: &str, range_store: &RangeStore, bit_position: u64) -> Result<bool> { range_store.unassign_bit(range_name, bit_position) } /// Get the range name for this assigner, using the network name as default /// /// # Arguments /// /// * `network_name` - Network name to use as default if no custom range name is set /// /// # Returns /// /// Returns the range name to use for operations pub fn get_range_name(&self, network_name: &str) -> String { self.range_name.clone().unwrap_or_else(|| network_name.to_string()) } /// Convert a bit position to an IP address within the network range /// /// # Arguments /// /// * `bit_position` - The bit position within the range /// * `netrange` - The network range to calculate the IP within /// /// # Returns /// /// Returns the IP address corresponding to the bit position pub fn bit_position_to_ip(&self, bit_position: u64, netrange: &NetRange) -> Result<IpAddr> { match netrange { NetRange::V4 { addr, prefix: _ } => { let network_addr = u32::from(*addr); let host_addr = network_addr + bit_position as u32; Ok(IpAddr::V4(Ipv4Addr::from(host_addr))) } NetRange::V6 { addr, prefix: _ } => { let network_addr = u128::from(*addr); let host_addr = network_addr + bit_position as u128; Ok(IpAddr::V6(Ipv6Addr::from(host_addr))) } } } /// Find the bit position for a given identifier in the range /// /// # Arguments /// /// * `range_name` - Name of the range to search /// * `range_store` - Reference to the RangeStore /// * `identifier` - The identifier to search for /// /// # Returns /// /// Returns the bit position if found, None otherwise pub fn find_identifier_bit_position(&self, range_name: &str, range_store: &RangeStore, identifier: &str) -> Result<Option<u64>> { // Get range ID first let range_id = match range_store.names.get(range_name)? { Some(data) => { if data.len() != 16 { return Ok(None); } let id_bytes: [u8; 8] = data[0..8].try_into() .map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid range data".to_string())))?; u64::from_be_bytes(id_bytes) } None => return Ok(None), }; // Search for the assignment with this identifier let prefix = range_id.to_be_bytes(); for result in range_store.assign.scan_prefix(&prefix) { let (key, stored_value) = result?; if key.len() == 16 { let stored_value_str = String::from_utf8(stored_value.to_vec())?; if stored_value_str == identifier { let bit_position_bytes: [u8; 8] = key[8..16].try_into() .map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid key format".to_string())))?; return Ok(Some(u64::from_be_bytes(bit_position_bytes))); } } } Ok(None) } } impl NetworkAssigner for RangeAssigner { fn assigner_type(&self) -> &'static str { "range" } fn to_json(&self) -> Result<String> { serde_json::to_string(self).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) }) } fn from_json(json: &str) -> Result<Self> { serde_json::from_str(json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) }) } fn initialize_for_network(&self, network_name: &str, netrange: &NetRange, range_store: Option<&RangeStore>) -> Result<()> { if let Some(store) = range_store { let range_name = self.get_range_name(network_name); self.initialize_range(&range_name, store, netrange)?; } Ok(()) } fn initialize_for_network_in_transaction( &self, network_name: &str, netrange: &NetRange, range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)> ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> { if let Some((range_names, range_map)) = range_trees { let range_name = self.get_range_name(network_name); let size = match netrange { NetRange::V4 { prefix, .. } => { if *prefix >= 32 { return Err(sled::transaction::ConflictableTransactionError::Abort(())); } 1u64 << (32 - prefix) } NetRange::V6 { prefix, .. } => { if *prefix >= 128 { return Err(sled::transaction::ConflictableTransactionError::Abort(())); } // For IPv6, we'll limit to a reasonable size to avoid huge ranges let host_bits = 128 - prefix; if host_bits > 32 { 1u64 << 32 // Cap at 2^32 addresses } else { 1u64 << host_bits } } }; // Define the range within the transaction match range_names.get(range_name.as_bytes())? { Some(_) => Ok(()), // Range already exists None => { let id = range_names.generate_id()?; // Store name -> (id, size) let mut value = Vec::new(); value.extend_from_slice(&id.to_be_bytes()); value.extend_from_slice(&size.to_be_bytes()); range_names.insert(range_name.as_bytes(), value)?; // Initialize bitmap for the range (all zeros) let bitmap_size = (size + 7) / 8; // Round up to nearest byte let bitmap = vec![0u8; bitmap_size as usize]; range_map.insert(&id.to_be_bytes(), bitmap)?; Ok(()) } } } else { Ok(()) } } fn get_assignment(&self, network: &Network, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> { if let Some(store) = range_store { let range_name = self.get_range_name(&network.name); // Find the bit position for this identifier if let Some(bit_position) = self.find_identifier_bit_position(&range_name, store, identifier)? { // Convert bit position to IP address let ip = self.bit_position_to_ip(bit_position, &network.netrange)?; Ok(Some(ip)) } else { Ok(None) } } else { Ok(None) } } fn cleanup_for_network(&self, network_name: &str, range_store: Option<&RangeStore>) -> Result<()> { if let Some(store) = range_store { let range_name = self.get_range_name(network_name); store.delete_range(&range_name)?; } Ok(()) } fn cleanup_for_network_in_transaction( &self, network_name: &str, range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)> ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> { if let Some((range_names, range_map)) = range_trees { let range_name = self.get_range_name(network_name); // Get the range ID from the name if let Some(value) = range_names.get(range_name.as_bytes())? { let id = u64::from_be_bytes([ value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], ]); // Remove the range data range_map.remove(&id.to_be_bytes())?; } // Remove the range name range_names.remove(range_name.as_bytes())?; } Ok(()) } } /// Network configuration #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Network { pub name: String, pub netrange: NetRange, pub provider_type: String, pub provider_config: String, pub assigner_type: Option<String>, pub assigner_config: Option<String>, } impl Network { /// Get an IP assignment for the given identifier /// /// This method calls the appropriate assigner implementation to retrieve /// the assigned IP address for a given identifier. Different assigners /// may return the IP in different formats (bit position or full IP). /// /// # Arguments /// /// * `identifier` - The identifier to look up (e.g., device name, container ID) /// * `range_store` - Optional reference to RangeStore (required for RangeAssigner) /// /// # Returns /// /// Returns the assigned IP address if found, None if no assignment exists /// /// # Example /// /// ``` /// # use store::network::{Network, NetRange, RangeAssigner}; /// # use tempfile::TempDir; /// # fn example() -> store::Result<()> { /// # let temp_dir = tempfile::tempdir().unwrap(); /// # let db = sled::open(temp_dir.path())?; /// # let ranges = store::range::RangeStore::open(&db)?; /// # let store = store::network::NetworkStore::open_with_ranges(&db, ranges.clone())?; /// # let netrange = NetRange::from_cidr("192.168.1.0/24")?; /// # let provider = store::network::BasicProvider::new(); /// # let assigner = RangeAssigner::new(); /// # store.create("test", netrange, provider, Some(assigner))?; /// # let network = store.get("test")?.unwrap(); /// /// let ip = network.get_assignment("device1", Some(&ranges))?; /// if let Some(addr) = ip { /// println!("Device1 is assigned IP: {}", addr); /// } /// # Ok(()) /// # } /// ``` /// Create an assigner instance from the network configuration pub fn create_assigner(&self) -> Result<Box<dyn NetworkAssigner>> { if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) { match assigner_type.as_str() { "range" => { let range_assigner: RangeAssigner = serde_json::from_str(assigner_config) .map_err(|e| Error::StoreError(sled::Error::Unsupported( format!("Failed to deserialize RangeAssigner: {}", e) )))?; Ok(Box::new(range_assigner)) } "basic" => { let basic_assigner: BasicAssigner = serde_json::from_str(assigner_config) .map_err(|e| Error::StoreError(sled::Error::Unsupported( format!("Failed to deserialize BasicAssigner: {}", e) )))?; Ok(Box::new(basic_assigner)) } _ => { Err(Error::StoreError(sled::Error::Unsupported( format!("Unknown assigner type: {}", assigner_type) ))) } } } else { Err(Error::StoreError(sled::Error::Unsupported( "No assigner configured".to_string() ))) } } pub fn get_assignment(&self, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> { if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) { match assigner_type.as_str() { "range" => { let range_assigner: RangeAssigner = serde_json::from_str(assigner_config) .map_err(|e| Error::StoreError(sled::Error::Unsupported( format!("Failed to deserialize RangeAssigner: {}", e) )))?; range_assigner.get_assignment(self, identifier, range_store) } "basic" => { let basic_assigner: BasicAssigner = serde_json::from_str(assigner_config) .map_err(|e| Error::StoreError(sled::Error::Unsupported( format!("Failed to deserialize BasicAssigner: {}", e) )))?; basic_assigner.get_assignment(self, identifier, range_store) } _ => { // Unknown assigner type Ok(None) } } } else { // No assigner configured Ok(None) } } + /// Automatically assign an IP address for the given identifier + /// + /// This method will assign an IP address from the network's range if it has + /// a RangeAssigner configured. This is typically called during instance creation + /// to automatically provision network connectivity. + /// + /// The method performs the following steps: + /// 1. Checks if the network has a RangeAssigner configured + /// 2. If so, calls the assigner's assign_ip method to allocate an IP + /// 3. The assigned IP can then be retrieved using get_assignment() + /// + /// # Arguments + /// + /// * `identifier` - The identifier to assign an IP to (e.g., instance name, container ID) + /// * `range_store` - Reference to the RangeStore for managing IP assignments + /// + /// # Returns + /// + /// * `Ok(())` - Assignment was successful or no assigner is configured + /// * `Err(Error)` - Assignment failed (e.g., range is full, network error) + /// + /// # Example + /// + /// ``` + /// # use store::network::{Network, NetRange, BasicProvider, RangeAssigner}; + /// # use store::RangeStore; + /// # use tempfile::TempDir; + /// # fn example() -> store::Result<()> { + /// # let temp_dir = TempDir::new().unwrap(); + /// # let db = sled::open(temp_dir.path())?; + /// # let ranges = RangeStore::open(&db)?; + /// # let networks = store::NetworkStore::open_with_ranges(&db, ranges.clone())?; + /// # let netrange = NetRange::from_cidr("10.0.1.0/24")?; + /// # let provider = BasicProvider::new(); + /// # let assigner = RangeAssigner::with_range_name("test_range"); + /// # networks.create("test_net", netrange, provider, Some(assigner))?; + /// # let network = networks.get("test_net")?.unwrap(); + /// # let ranges_ref = networks.ranges().unwrap(); + /// + /// // Automatically assign an IP to a jail instance + /// network.auto_assign_ip("my-jail-01", ranges_ref)?; + /// + /// // Retrieve the assigned IP + /// let assigned_ip = network.get_assignment("my-jail-01", Some(ranges_ref))?; + /// println!("Assigned IP: {:?}", assigned_ip); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Note + /// + /// This method is automatically called during instance creation when the instance + /// has network interfaces configured for networks with RangeAssigners. Manual calls + /// are typically not needed unless implementing custom provisioning logic. + pub fn auto_assign_ip(&self, identifier: &str, range_store: &RangeStore) -> Result<()> { + if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) { + match assigner_type.as_str() { + "range" => { + let range_assigner: RangeAssigner = serde_json::from_str(assigner_config) + .map_err(|e| Error::StoreError(sled::Error::Unsupported( + format!("Failed to deserialize RangeAssigner: {}", e) + )))?; + + let range_name = range_assigner.get_range_name(&self.name); + + // Try to assign an IP - ignore the result bit position + range_assigner.assign_ip(&range_name, range_store, identifier) + .map(|_| ()) + } + _ => { + // Other assigner types don't support auto-assignment + Ok(()) + } + } + } else { + // No assigner configured, nothing to do + Ok(()) + } + } + + /// Automatically unassign an IP address for the given identifier + /// + /// This method will unassign an IP address from the network's range if it has + /// a RangeAssigner configured and the identifier has an assignment. This is + /// typically called during instance deletion to clean up network resources. + /// + /// # Arguments + /// + /// * `identifier` - The identifier to unassign an IP from (e.g., instance name) + /// * `range_store` - Reference to the RangeStore for managing IP assignments + /// + /// # Returns + /// + /// * `Ok(())` - Unassignment was successful or no assigner is configured + /// * `Err(Error)` - Unassignment failed due to network error + pub fn auto_unassign_ip(&self, identifier: &str, range_store: &RangeStore) -> Result<()> { + if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) { + match assigner_type.as_str() { + "range" => { + let range_assigner: RangeAssigner = serde_json::from_str(assigner_config) + .map_err(|e| Error::StoreError(sled::Error::Unsupported( + format!("Failed to deserialize RangeAssigner: {}", e) + )))?; + + let range_name = range_assigner.get_range_name(&self.name); + + // Find the bit position for this identifier + if let Some(bit_position) = range_assigner.find_identifier_bit_position(&range_name, range_store, identifier)? { + // Unassign the IP + range_assigner.unassign_ip(&range_name, range_store, bit_position)?; + } + Ok(()) + } + _ => { + // Other assigner types don't support auto-unassignment + Ok(()) + } + } + } else { + // No assigner configured, nothing to do + Ok(()) + } + } + /// Get an IP address from a bit position in the network range /// /// This method converts a bit position to the corresponding IP address /// within this network's range. Useful when you already know the bit /// position from range operations. /// /// # Arguments /// /// * `bit_position` - The bit position within the network range /// /// # Returns /// /// Returns the IP address corresponding to the bit position /// /// # Example /// /// ``` /// # use store::network::{Network, NetRange}; /// # fn example() -> store::Result<()> { /// # let netrange = NetRange::from_cidr("192.168.1.0/24")?; /// let network = Network { /// name: "test".to_string(), /// netrange: netrange, /// provider_type: "basic".to_string(), /// provider_config: "{}".to_string(), /// assigner_type: None, /// assigner_config: None, /// }; /// /// let ip = network.get_assignment_by_bit_position(5)?; /// println!("Bit position 5 corresponds to IP: {}", ip); /// # Ok(()) /// # } /// ``` pub fn get_assignment_by_bit_position(&self, bit_position: u64) -> Result<IpAddr> { match &self.netrange { NetRange::V4 { addr, prefix: _ } => { let network_addr = u32::from(*addr); let host_addr = network_addr + bit_position as u32; Ok(IpAddr::V4(Ipv4Addr::from(host_addr))) } NetRange::V6 { addr, prefix: _ } => { let network_addr = u128::from(*addr); let host_addr = network_addr + bit_position as u128; Ok(IpAddr::V6(Ipv6Addr::from(host_addr))) } } } } /// Network store for managing network configurations #[derive(Debug, Clone)] pub struct NetworkStore { pub(crate) namespaces: crate::namespace::NamespaceStore, pub(crate) networks: sled::Tree, pub(crate) ranges: Option<crate::range::RangeStore>, } impl NetworkStore { /// Open a new network store pub fn open(db: &sled::Db) -> Result<Self> { Ok(NetworkStore { namespaces: crate::namespace::NamespaceStore::open(db)?, networks: db.open_tree("networks/1/data")?, ranges: None, }) } /// Open a new network store with range store pub fn open_with_ranges(db: &sled::Db, ranges: crate::range::RangeStore) -> Result<Self> { Ok(NetworkStore { namespaces: crate::namespace::NamespaceStore::open(db)?, networks: db.open_tree("networks/1/data")?, ranges: Some(ranges), }) } + /// Get reference to the range store + pub fn ranges(&self) -> Option<&crate::range::RangeStore> { + self.ranges.as_ref() + } + /// Create a new network with provider and optional assigner pub fn create<P, A>(&self, name: &str, netrange: NetRange, provider: P, assigner: Option<A>) -> Result<()> where P: NetworkProvider, A: NetworkAssigner, { // Ensure "networks" namespace exists if !self.namespaces.namespace_exists("networks")? { self.namespaces.define("networks")?; } let network = Network { name: name.to_string(), netrange: netrange.clone(), provider_type: provider.provider_type().to_string(), provider_config: provider.to_json()?, assigner_type: assigner.as_ref().map(|a| a.assigner_type().to_string()), assigner_config: assigner.as_ref().map(|a| a.to_json()).transpose()?, }; let network_json = serde_json::to_string(&network).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) })?; // Include range store trees in transaction if needed if let (Some(assigner_ref), Some(range_store)) = (&assigner, &self.ranges) { if assigner_ref.assigner_type() == "range" { // Transaction with both network and range stores (&self.namespaces.names, &self.namespaces.spaces, &self.networks, &range_store.names, &range_store.map).transaction( |(names, spaces, networks, range_names, range_map)| { // Reserve the network name in the "networks" namespace if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? { return Err(sled::transaction::ConflictableTransactionError::Abort(())); } // Initialize the assigner within the transaction assigner_ref.initialize_for_network_in_transaction(name, &netrange, Some((range_names, range_map)))?; // Store the network configuration networks.insert(name.as_bytes(), network_json.as_bytes())?; Ok(()) } ).map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => { Error::NamespaceKeyReserved("networks".to_string(), name.to_string()) } sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; } else { // Transaction without range store (&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction( |(names, spaces, networks)| { // Reserve the network name in the "networks" namespace if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? { return Err(sled::transaction::ConflictableTransactionError::Abort(())); } // Store the network configuration networks.insert(name.as_bytes(), network_json.as_bytes())?; Ok(()) } ).map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => { Error::NamespaceKeyReserved("networks".to_string(), name.to_string()) } sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; } } else { // Transaction without assigner or range store (&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction( |(names, spaces, networks)| { // Reserve the network name in the "networks" namespace if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? { return Err(sled::transaction::ConflictableTransactionError::Abort(())); } // Store the network configuration networks.insert(name.as_bytes(), network_json.as_bytes())?; Ok(()) } ).map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => { Error::NamespaceKeyReserved("networks".to_string(), name.to_string()) } sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; } Ok(()) } /// Get a network by name pub fn get(&self, name: &str) -> Result<Option<Network>> { if let Some(data) = self.networks.get(name.as_bytes())? { let network_json = String::from_utf8(data.to_vec())?; let network: Network = serde_json::from_str(&network_json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) })?; Ok(Some(network)) } else { Ok(None) } } /// Update an existing network pub fn update<P, A>(&self, name: &str, netrange: NetRange, provider: P, assigner: Option<A>) -> Result<()> where P: NetworkProvider, A: NetworkAssigner, { if !self.namespaces.key_exists("networks", name)? { return Err(Error::StoreError(sled::Error::Unsupported( format!("Network '{}' does not exist", name) ))); } let network = Network { name: name.to_string(), netrange, provider_type: provider.provider_type().to_string(), provider_config: provider.to_json()?, assigner_type: assigner.as_ref().map(|a| a.assigner_type().to_string()), assigner_config: assigner.as_ref().map(|a| a.to_json()).transpose()?, }; let network_json = serde_json::to_string(&network).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) })?; self.networks.insert(name.as_bytes(), network_json.as_bytes())?; Ok(()) } /// Delete a network by name pub fn delete(&self, name: &str) -> Result<bool> { let result = if let Some(ranges) = &self.ranges { // Transaction with range cleanup (&self.namespaces.names, &self.namespaces.spaces, &self.networks, &ranges.names, &ranges.map).transaction( |(names, spaces, networks, range_names, range_map)| { // Get network data before deletion to access assigner if let Some(network_data) = networks.get(name.as_bytes())? { let network: Network = serde_json::from_slice(network_data.as_ref()) .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; // Clean up assigner if it exists if network.assigner_type.is_some() { let assigner = network.create_assigner() .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; assigner.cleanup_for_network_in_transaction(name, Some((range_names, range_map)))?; } } // Remove from namespace let removed = self.namespaces.remove_in_transaction(names, spaces, "networks", name)?; if removed { // Remove network data networks.remove(name.as_bytes())?; } Ok(removed) } ) } else { // Transaction without range cleanup (&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction( |(names, spaces, networks)| { // Remove from namespace let removed = self.namespaces.remove_in_transaction(names, spaces, "networks", name)?; if removed { // Remove network data networks.remove(name.as_bytes())?; } Ok(removed) } ) }; let result = result.map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => { Error::StoreError(sled::Error::Unsupported("Transaction aborted".to_string())) } sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; Ok(result) } /// List all network names pub fn list(&self) -> Result<Vec<String>> { self.namespaces.list_keys("networks") } /// Check if a network exists pub fn exists(&self, name: &str) -> Result<bool> { self.namespaces.key_exists("networks", name) } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; fn create_test_store() -> Result<(NetworkStore, TempDir)> { let temp_dir = tempfile::tempdir().unwrap(); let db = sled::open(temp_dir.path())?; let ranges = crate::range::RangeStore::open(&db)?; let store = NetworkStore::open_with_ranges(&db, ranges)?; Ok((store, temp_dir)) } #[test] fn test_netrange_ipv4() -> Result<()> { let netrange = NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?; assert_eq!(netrange.to_cidr(), "192.168.1.0/24"); Ok(()) } #[test] fn test_netrange_ipv6() -> Result<()> { let netrange = NetRange::ipv6("2001:db8::".parse().unwrap(), 64)?; assert_eq!(netrange.to_cidr(), "2001:db8::/64"); Ok(()) } #[test] fn test_netrange_from_cidr() -> Result<()> { let ipv4_range = NetRange::from_cidr("10.0.0.0/8")?; assert_eq!(ipv4_range.to_cidr(), "10.0.0.0/8"); let ipv6_range = NetRange::from_cidr("fe80::/10")?; assert_eq!(ipv6_range.to_cidr(), "fe80::/10"); Ok(()) } #[test] fn test_basic_provider() -> Result<()> { let provider = BasicProvider::new() .with_config("endpoint", "https://api.example.com") .with_config("timeout", "30"); let json = provider.to_json()?; let restored = BasicProvider::from_json(&json)?; assert_eq!(provider.config, restored.config); assert_eq!(provider.provider_type(), "basic"); Ok(()) } #[test] fn test_basic_assigner() -> Result<()> { let assigner = BasicAssigner::new("round_robin") .with_config("pool_size", "100"); let json = assigner.to_json()?; let restored = BasicAssigner::from_json(&json)?; assert_eq!(assigner.strategy, restored.strategy); assert_eq!(assigner.config, restored.config); assert_eq!(assigner.assigner_type(), "basic"); Ok(()) } #[test] fn test_create_and_get_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?; let provider = BasicProvider::new().with_config("type", "test"); let assigner = Some(BasicAssigner::new("sequential")); store.create("test_network", netrange.clone(), provider, assigner)?; let network = store.get("test_network")?.unwrap(); assert_eq!(network.name, "test_network"); assert_eq!(network.netrange, netrange); assert_eq!(network.provider_type, "basic"); assert!(network.assigner_type.is_some()); Ok(()) } #[test] fn test_create_network_without_assigner() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv6("2001:db8::".parse().unwrap(), 64)?; let provider = BasicProvider::new(); let assigner: Option<BasicAssigner> = None; store.create("ipv6_network", netrange.clone(), provider, assigner)?; let network = store.get("ipv6_network")?.unwrap(); assert_eq!(network.name, "ipv6_network"); assert_eq!(network.netrange, netrange); assert!(network.assigner_type.is_none()); assert!(network.assigner_config.is_none()); Ok(()) } #[test] fn test_create_duplicate_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("10.0.0.0".parse().unwrap(), 8)?; let provider = BasicProvider::new(); let assigner: Option<BasicAssigner> = None; // First creation should succeed store.create("duplicate_test", netrange.clone(), provider.clone(), assigner.clone())?; // Second creation should fail let result = store.create("duplicate_test", netrange, provider, assigner); assert!(result.is_err()); Ok(()) } #[test] fn test_update_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create initial network let netrange = NetRange::ipv4("172.16.0.0".parse().unwrap(), 12)?; let provider = BasicProvider::new().with_config("version", "1"); let assigner: Option<BasicAssigner> = None; store.create("update_test", netrange, provider, assigner)?; // Update the network let new_netrange = NetRange::ipv4("172.16.0.0".parse().unwrap(), 16)?; let new_provider = BasicProvider::new().with_config("version", "2"); let new_assigner = Some(BasicAssigner::new("random")); store.update("update_test", new_netrange.clone(), new_provider, new_assigner)?; // Verify the update let network = store.get("update_test")?.unwrap(); assert_eq!(network.netrange, new_netrange); assert!(network.assigner_type.is_some()); Ok(()) } #[test] fn test_delete_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("203.0.113.0".parse().unwrap(), 24)?; let provider = BasicProvider::new(); let assigner: Option<BasicAssigner> = None; store.create("delete_test", netrange, provider, assigner)?; assert!(store.exists("delete_test")?); let deleted = store.delete("delete_test")?; assert!(deleted); assert!(!store.exists("delete_test")?); // Try to delete non-existent network let deleted_again = store.delete("delete_test")?; assert!(!deleted_again); Ok(()) } #[test] fn test_list_networks() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create multiple networks let networks = vec![ ("net1", "10.1.0.0/16"), ("net2", "10.2.0.0/16"), ("net3", "2001:db8:1::/48"), ]; for (name, cidr) in &networks { let netrange = NetRange::from_cidr(cidr)?; let provider = BasicProvider::new(); let assigner: Option<BasicAssigner> = None; store.create(name, netrange, provider, assigner)?; } let mut network_names = store.list()?; network_names.sort(); let mut expected: Vec<String> = networks.iter().map(|(name, _)| name.to_string()).collect(); expected.sort(); assert_eq!(network_names, expected); Ok(()) } #[test] fn test_update_nonexistent_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("198.51.100.0".parse().unwrap(), 24)?; let provider = BasicProvider::new(); let assigner: Option<BasicAssigner> = None; let result = store.update("nonexistent", netrange, provider, assigner); assert!(result.is_err()); Ok(()) } #[test] fn test_invalid_cidr() { let result = NetRange::from_cidr("invalid"); assert!(result.is_err()); let result = NetRange::from_cidr("192.168.1.0"); assert!(result.is_err()); let result = NetRange::from_cidr("192.168.1.0/33"); assert!(result.is_err()); } #[test] fn test_range_assigner() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create a network with RangeAssigner let netrange = NetRange::from_cidr("192.168.1.0/24")?; let provider = BasicProvider::new() .with_config("type", "test"); let assigner = RangeAssigner::new() .with_config("strategy", "sequential"); store.create("test-network", netrange, provider, Some(assigner))?; // Verify the network was created let network = store.get("test-network")?.unwrap(); assert_eq!(network.name, "test-network"); assert_eq!(network.assigner_type, Some("range".to_string())); // Test range assignment functionality if let Some(ref range_store) = store.ranges { let range_assigner = RangeAssigner::new(); let range_name = "test-network"; // Uses network name as range name // Assign some IPs let bit1 = range_assigner.assign_ip(range_name, range_store, "device1")?; let bit2 = range_assigner.assign_ip(range_name, range_store, "device2")?; // Verify assignments assert_eq!(bit1, 0); assert_eq!(bit2, 1); // Get assignment info let assignment1 = range_assigner.get_assignment_by_bit(range_name, range_store, bit1)?.unwrap(); let assignment2 = range_assigner.get_assignment_by_bit(range_name, range_store, bit2)?.unwrap(); assert_eq!(assignment1, "device1"); assert_eq!(assignment2, "device2"); // Test unassignment let unassigned = range_assigner.unassign_ip(range_name, range_store, bit1)?; assert!(unassigned); let assignment1_after = range_assigner.get_assignment_by_bit(range_name, range_store, bit1)?; assert!(assignment1_after.is_none()); } Ok(()) } #[test] fn test_range_assigner_serialization() -> Result<()> { let assigner = RangeAssigner::with_range_name("test-range") .with_config("strategy", "random") .with_config("pool_size", "100"); // Test serialization let json = assigner.to_json()?; assert!(json.contains("test-range")); assert!(json.contains("random")); assert!(json.contains("100")); // Test deserialization let deserialized: RangeAssigner = RangeAssigner::from_json(&json)?; assert_eq!(deserialized.range_name, Some("test-range".to_string())); assert_eq!(deserialized.config.get("strategy"), Some(&"random".to_string())); assert_eq!(deserialized.config.get("pool_size"), Some(&"100".to_string())); Ok(()) } #[test] fn test_network_get_assignment() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create a network with RangeAssigner let netrange = NetRange::from_cidr("192.168.1.0/24")?; let provider = BasicProvider::new() .with_config("type", "test"); let assigner = RangeAssigner::new() .with_config("strategy", "sequential"); store.create("test-network", netrange, provider, Some(assigner))?; // Get the network let network = store.get("test-network")?.unwrap(); // Test that no assignment exists initially let ip = network.get_assignment("device1", store.ranges.as_ref())?; assert!(ip.is_none()); // Assign an IP using the range assigner directly if let Some(ref range_store) = store.ranges { let range_assigner = RangeAssigner::new(); let range_name = "test-network"; // Uses network name as range name // Assign an IP to device1 let bit_position = range_assigner.assign_ip(range_name, range_store, "device1")?; assert_eq!(bit_position, 0); // Now test get_assignment on the network let ip = network.get_assignment("device1", store.ranges.as_ref())?; assert!(ip.is_some()); let assigned_ip = ip.unwrap(); match assigned_ip { std::net::IpAddr::V4(ipv4) => { // Should be 192.168.1.0 + bit_position let expected = std::net::Ipv4Addr::new(192, 168, 1, bit_position as u8); assert_eq!(ipv4, expected); } _ => panic!("Expected IPv4 address"), } // Test assignment for non-existent device let no_ip = network.get_assignment("nonexistent", store.ranges.as_ref())?; assert!(no_ip.is_none()); // Assign another device let bit_position2 = range_assigner.assign_ip(range_name, range_store, "device2")?; assert_eq!(bit_position2, 1); let ip2 = network.get_assignment("device2", store.ranges.as_ref())?; assert!(ip2.is_some()); let assigned_ip2 = ip2.unwrap(); match assigned_ip2 { std::net::IpAddr::V4(ipv4) => { let expected = std::net::Ipv4Addr::new(192, 168, 1, bit_position2 as u8); assert_eq!(ipv4, expected); } _ => panic!("Expected IPv4 address"), } } Ok(()) } #[test] fn test_network_get_assignment_no_assigner() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create a network without an assigner let netrange = NetRange::from_cidr("192.168.1.0/24")?; let provider = BasicProvider::new() .with_config("type", "test"); store.create("no-assigner-network", netrange, provider, None::<RangeAssigner>)?; // Get the network let network = store.get("no-assigner-network")?.unwrap(); // Test that get_assignment returns None when no assigner is configured let ip = network.get_assignment("device1", store.ranges.as_ref())?; assert!(ip.is_none()); Ok(()) } #[test] fn test_delete_network_with_range_assigner() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create a network with a range assigner let netrange = NetRange::from_cidr("192.168.100.0/24")?; let provider = BasicProvider::new() .with_config("type", "test"); let assigner = RangeAssigner::new() .with_config("strategy", "sequential"); store.create("test-range-network", netrange, provider, Some(assigner))?; // Verify the network was created assert!(store.exists("test-range-network")?); let network = store.get("test-range-network")?.unwrap(); // Verify the range was initialized by checking if the range exists if let Some(ranges) = &store.ranges { let range_name = "test-range-network"; // Check if range exists by trying to get range info let range_exists = ranges.names.get(range_name.as_bytes())?.is_some(); assert!(range_exists, "Range should be initialized after network creation"); // Make an assignment using the range assigner directly let range_assigner = RangeAssigner::new(); let _bit_position = range_assigner.assign_ip(range_name, ranges, "device1")?; // Assignment should succeed (bit_position is always >= 0 since it's u64) // Now verify the assignment exists let ip = network.get_assignment("device1", store.ranges.as_ref())?; assert!(ip.is_some(), "Assignment should be retrievable"); // Verify assignment data exists let range_info = ranges.list_range(range_name)?; assert!(!range_info.is_empty(), "Range should have assignment data"); } // Delete the network let deleted = store.delete("test-range-network")?; assert!(deleted); // Verify the network no longer exists assert!(!store.exists("test-range-network")?); assert!(store.get("test-range-network")?.is_none()); // Verify range data was cleaned up if let Some(ranges) = &store.ranges { let range_name = "test-range-network"; let range_exists = ranges.names.get(range_name.as_bytes())?.is_some(); assert!(!range_exists, "Range should be deleted"); let range_info = ranges.list_range(range_name)?; assert!(range_info.is_empty(), "Range assignments should be cleaned up"); } Ok(()) } #[test] fn test_delete_network_without_assigner() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create a network without an assigner let netrange = NetRange::from_cidr("10.10.0.0/16")?; let provider = BasicProvider::new() .with_config("type", "test"); store.create("test-no-assigner", netrange, provider, None::<RangeAssigner>)?; // Verify the network was created assert!(store.exists("test-no-assigner")?); let network = store.get("test-no-assigner")?.unwrap(); assert!(network.assigner_type.is_none()); // Delete the network let deleted = store.delete("test-no-assigner")?; assert!(deleted); // Verify the network no longer exists assert!(!store.exists("test-no-assigner")?); assert!(store.get("test-no-assigner")?.is_none()); Ok(()) } #[test] fn test_network_get_assignment_ipv6() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create an IPv6 network with RangeAssigner let netrange = NetRange::from_cidr("2001:db8::/64")?; let provider = BasicProvider::new() .with_config("type", "test"); let assigner = RangeAssigner::new() .with_config("strategy", "sequential"); store.create("ipv6-network", netrange, provider, Some(assigner))?; // Get the network let network = store.get("ipv6-network")?.unwrap(); // Assign an IP using the range assigner directly if let Some(ref range_store) = store.ranges { let range_assigner = RangeAssigner::new(); let range_name = "ipv6-network"; // Assign an IP to device1 let bit_position = range_assigner.assign_ip(range_name, range_store, "ipv6-device1")?; assert_eq!(bit_position, 0); // Test get_assignment on the network let ip = network.get_assignment("ipv6-device1", store.ranges.as_ref())?; assert!(ip.is_some()); let assigned_ip = ip.unwrap(); match assigned_ip { std::net::IpAddr::V6(ipv6) => { // Should be 2001:db8:: + bit_position let expected = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, bit_position as u16); assert_eq!(ipv6, expected); } _ => panic!("Expected IPv6 address"), } } Ok(()) } } \ No newline at end of file diff --git a/crates/store/tests/jail_range_assignment_test.rs b/crates/store/tests/jail_range_assignment_test.rs new file mode 100644 index 0000000..a88b5f3 --- /dev/null +++ b/crates/store/tests/jail_range_assignment_test.rs @@ -0,0 +1,330 @@ +use store::{ + InstanceStore, Instance, InstanceConfig, NetworkInterface, + NamespaceStore, NetworkStore, RangeStore, + JailProvider, Provider, + network::{NetRange, BasicProvider, RangeAssigner}, +}; +use std::collections::HashMap; +use tempfile::TempDir; + +fn create_test_stores() -> store::Result<(TempDir, sled::Db, InstanceStore)> { + let temp_dir = TempDir::new().unwrap(); + let db = sled::open(temp_dir.path().join("test.db"))?; + + let namespaces = NamespaceStore::open(&db)?; + let ranges = RangeStore::open(&db)?; + let networks = NetworkStore::open_with_ranges(&db, ranges)?; + let instances = InstanceStore::open(&db, namespaces, networks)?; + + Ok((temp_dir, db, instances)) +} + +#[test] +fn test_jail_with_range_assigner_should_get_automatic_ip_assignment() -> store::Result<()> { + let (_temp_dir, _db, instances) = create_test_stores()?; + + // Create a network with RangeAssigner + let provider = BasicProvider::new() + .with_config("type", "bridge") + .with_config("driver", "epair"); + + let assigner = RangeAssigner::with_range_name("jail_network_ips") + .with_config("strategy", "sequential") + .with_config("reserve_gateway", "true"); + + let netrange = NetRange::from_cidr("10.0.10.0/24")?; + + instances.networks().create( + "jail_network", + netrange, + provider, + Some(assigner), + )?; + + // Create a jail instance with network interface + let jail_provider = JailProvider::new() + .with_dataset("tank/jails".to_string()); + + let jail_config = InstanceConfig { + provider_config: jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "jail_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, // Should be automatically assigned + } + ], + metadata: HashMap::new(), + }; + + let jail_instance = Instance { + name: "test-jail".to_string(), + provider_type: "jail".to_string(), + config: jail_config, + created_at: 0, + updated_at: 0, + }; + + // Create the instance - this should automatically assign an IP + let created_instance = instances.create(jail_instance)?; + + // Verify the instance was created with an IP assignment + assert_eq!(created_instance.name, "test-jail"); + assert_eq!(created_instance.config.network_interfaces.len(), 1); + + let interface = &created_instance.config.network_interfaces[0]; + assert_eq!(interface.network_name, "jail_network"); + assert_eq!(interface.interface_name, "em0"); + + // This should pass once the automatic assignment is implemented + assert!(interface.assignment.is_some(), "IP should be automatically assigned"); + + // Verify the assigned IP is valid + if let Some(ref assignment) = interface.assignment { + let ip: std::net::IpAddr = assignment.parse() + .expect("Assignment should be a valid IP address"); + + // Should be in the 10.0.10.0/24 range + match ip { + std::net::IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + assert_eq!(octets[0], 10); + assert_eq!(octets[1], 0); + assert_eq!(octets[2], 10); + // RangeAssigner starts from bit position 0, which is the network address + // This is actually correct behavior - bit 0 = .0, bit 1 = .1, etc. + assert!(octets[3] < 255, "IP should not be broadcast address"); + } + _ => panic!("Expected IPv4 address"), + } + } + + // Test that we can retrieve the instance and it still has the assignment + let retrieved_instance = instances.get("test-jail")?; + assert!(retrieved_instance.config.network_interfaces[0].assignment.is_some()); + + Ok(()) +} + +#[test] +fn test_jail_deletion_cleans_up_ip_assignments() -> store::Result<()> { + let (_temp_dir, _db, instances) = create_test_stores()?; + + // Create a network with RangeAssigner + let provider = BasicProvider::new(); + let assigner = RangeAssigner::with_range_name("deletion_test_ips"); + let netrange = NetRange::from_cidr("10.0.30.0/24")?; + + instances.networks().create("deletion_test_network", netrange, provider, Some(assigner))?; + + // Create a jail instance + let jail_provider = JailProvider::new(); + let jail_config = InstanceConfig { + provider_config: jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "deletion_test_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, + } + ], + metadata: HashMap::new(), + }; + + let jail_instance = Instance { + name: "deletion-test-jail".to_string(), + provider_type: "jail".to_string(), + config: jail_config, + created_at: 0, + updated_at: 0, + }; + + // Create the instance - should get IP assignment + let created = instances.create(jail_instance)?; + assert!(created.config.network_interfaces[0].assignment.is_some()); + let assigned_ip = created.config.network_interfaces[0].assignment.as_ref().unwrap().clone(); + + // Verify the IP is assigned in the range + let network = instances.networks().get("deletion_test_network")?.unwrap(); + let assignment = network.get_assignment("deletion-test-jail", instances.networks().ranges())?; + assert!(assignment.is_some()); + assert_eq!(assignment.unwrap().to_string(), assigned_ip); + + // Delete the instance + instances.delete("deletion-test-jail")?; + + // Verify instance is deleted + assert!(instances.get("deletion-test-jail").is_err()); + + // Verify IP assignment is cleaned up + let assignment_after = network.get_assignment("deletion-test-jail", instances.networks().ranges())?; + assert!(assignment_after.is_none(), "IP assignment should be cleaned up after deletion"); + + // Create a new instance to verify the IP can be reused + let new_jail_config = InstanceConfig { + provider_config: JailProvider::new().to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "deletion_test_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, + } + ], + metadata: HashMap::new(), + }; + + let new_jail = Instance { + name: "new-test-jail".to_string(), + provider_type: "jail".to_string(), + config: new_jail_config, + created_at: 0, + updated_at: 0, + }; + + let new_created = instances.create(new_jail)?; + + // Should get the same IP that was freed + assert!(new_created.config.network_interfaces[0].assignment.is_some()); + let new_assigned_ip = new_created.config.network_interfaces[0].assignment.as_ref().unwrap(); + assert_eq!(*new_assigned_ip, assigned_ip, "IP should be reused after cleanup"); + + Ok(()) +} + +#[test] +fn test_multiple_jails_get_different_ip_assignments() -> store::Result<()> { + let (_temp_dir, _db, instances) = create_test_stores()?; + + // Create a network with RangeAssigner + let provider = BasicProvider::new(); + let assigner = RangeAssigner::with_range_name("multi_jail_ips"); + let netrange = NetRange::from_cidr("192.168.100.0/24")?; + + instances.networks().create("multi_jail_network", netrange, provider, Some(assigner))?; + + // Create multiple jail instances + for i in 1..=3 { + let jail_provider = JailProvider::new(); + let jail_config = InstanceConfig { + provider_config: jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "multi_jail_network".to_string(), + interface_name: "em0".to_string(), + assignment: None, + } + ], + metadata: HashMap::new(), + }; + + let jail_instance = Instance { + name: format!("jail-{}", i), + provider_type: "jail".to_string(), + config: jail_config, + created_at: 0, + updated_at: 0, + }; + + instances.create(jail_instance)?; + } + + // Verify all instances have different IP assignments + let mut assigned_ips = std::collections::HashSet::new(); + + for i in 1..=3 { + let instance = instances.get(&format!("jail-{}", i))?; + let interface = &instance.config.network_interfaces[0]; + + assert!(interface.assignment.is_some(), "Instance jail-{} should have IP assignment", i); + let ip = interface.assignment.as_ref().unwrap(); + + assert!(assigned_ips.insert(ip.clone()), + "Instance jail-{} got duplicate IP: {}", i, ip); + } + + // Should have 3 unique IP assignments + assert_eq!(assigned_ips.len(), 3); + + Ok(()) +} + +#[test] +fn test_jail_with_multiple_network_interfaces() -> store::Result<()> { + let (_temp_dir, _db, instances) = create_test_stores()?; + + // Create two networks with RangeAssigners + let mgmt_provider = BasicProvider::new(); + let mgmt_assigner = RangeAssigner::with_range_name("mgmt_ips"); + instances.networks().create( + "management", + NetRange::from_cidr("10.0.1.0/24")?, + mgmt_provider, + Some(mgmt_assigner), + )?; + + let data_provider = BasicProvider::new(); + let data_assigner = RangeAssigner::with_range_name("data_ips"); + instances.networks().create( + "data", + NetRange::from_cidr("10.0.2.0/24")?, + data_provider, + Some(data_assigner), + )?; + + // Create jail with multiple interfaces + let jail_provider = JailProvider::new(); + let jail_config = InstanceConfig { + provider_config: jail_provider.to_json()?, + network_interfaces: vec![ + NetworkInterface { + network_name: "management".to_string(), + interface_name: "em0".to_string(), + assignment: None, + }, + NetworkInterface { + network_name: "data".to_string(), + interface_name: "em1".to_string(), + assignment: None, + } + ], + metadata: HashMap::new(), + }; + + let jail_instance = Instance { + name: "multi-interface-jail".to_string(), + provider_type: "jail".to_string(), + config: jail_config, + created_at: 0, + updated_at: 0, + }; + + let created_instance = instances.create(jail_instance)?; + + // Verify both interfaces got IP assignments + assert_eq!(created_instance.config.network_interfaces.len(), 2); + + for (i, interface) in created_instance.config.network_interfaces.iter().enumerate() { + assert!(interface.assignment.is_some(), + "Interface {} should have IP assignment", i); + + let ip: std::net::IpAddr = interface.assignment.as_ref().unwrap().parse() + .expect("Assignment should be valid IP"); + + match ip { + std::net::IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + assert_eq!(octets[0], 10); + assert_eq!(octets[1], 0); + + if interface.network_name == "management" { + assert_eq!(octets[2], 1); + } else if interface.network_name == "data" { + assert_eq!(octets[2], 2); + } + } + _ => panic!("Expected IPv4 address"), + } + } + + Ok(()) +} \ No newline at end of file