Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F73613
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
89 KB
Subscribers
None
View Options
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
@@ -133,6 +133,12 @@ dependencies = [
"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"
@@ -244,6 +250,12 @@ dependencies = [
"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"
@@ -276,6 +288,12 @@ 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"
@@ -294,6 +312,15 @@ 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"
@@ -326,6 +353,46 @@ dependencies = [
"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"
@@ -340,6 +407,19 @@ dependencies = [
"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"
@@ -356,6 +436,22 @@ dependencies = [
"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"
@@ -412,6 +508,15 @@ 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"
@@ -435,6 +540,12 @@ dependencies = [
"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"
@@ -457,6 +568,21 @@ 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"
@@ -497,6 +623,12 @@ 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"
@@ -527,6 +659,17 @@ 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"
@@ -551,6 +694,37 @@ 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"
@@ -606,6 +780,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
+ "h2",
"http",
"http-body",
"httparse",
@@ -617,12 +792,45 @@ dependencies = [
"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",
@@ -630,12 +838,16 @@ dependencies = [
"http",
"http-body",
"hyper",
+ "ipnet",
"libc",
+ "percent-encoding",
"pin-project-lite",
"socket2",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
@@ -756,6 +968,16 @@ dependencies = [
"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"
@@ -765,6 +987,22 @@ 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"
@@ -810,6 +1048,16 @@ dependencies = [
"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"
@@ -936,6 +1184,23 @@ dependencies = [
"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"
@@ -1008,6 +1273,50 @@ 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"
@@ -1080,6 +1389,12 @@ 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"
@@ -1189,7 +1504,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
- "getrandom",
+ "getrandom 0.3.3",
]
[[package]]
@@ -1254,6 +1569,48 @@ 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"
@@ -1263,6 +1620,20 @@ 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"
@@ -1303,6 +1674,39 @@ dependencies = [
"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"
@@ -1315,12 +1719,44 @@ 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"
@@ -1458,6 +1894,18 @@ dependencies = [
"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"
@@ -1474,6 +1922,9 @@ 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"
@@ -1486,6 +1937,27 @@ dependencies = [
"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"
@@ -1493,7 +1965,7 @@ 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",
@@ -1599,6 +2071,26 @@ dependencies = [
"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"
@@ -1623,6 +2115,19 @@ dependencies = [
"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"
@@ -1647,9 +2152,12 @@ checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
dependencies = [
"bitflags 2.9.1",
"bytes",
+ "futures-util",
"http",
"http-body",
+ "iri-string",
"pin-project-lite",
+ "tower",
"tower-layer",
"tower-service",
"tracing",
@@ -1741,6 +2249,12 @@ 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"
@@ -1770,6 +2284,12 @@ 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"
@@ -1800,6 +2320,87 @@ 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"
@@ -1822,6 +2423,41 @@ 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"
@@ -2054,6 +2690,12 @@ dependencies = [
"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"
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
@@ -120,7 +120,26 @@ impl InstanceStore {
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)?;
@@ -133,7 +152,32 @@ impl InstanceStore {
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
@@ -182,7 +226,26 @@ impl InstanceStore {
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)),
}
@@ -229,13 +292,26 @@ impl InstanceStore {
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);
+ }
+ }
+ }
}
}
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
@@ -753,6 +753,130 @@ impl Network {
}
}
+ /// 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
@@ -830,6 +954,11 @@ impl NetworkStore {
})
}
+ /// 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
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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Jun 7, 6:54 PM (53 m, 43 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47542
Default Alt Text
(89 KB)
Attached To
rCOLLAR collar
Event Timeline
Log In to Comment