From 6fa172eab198a43fe75d702e5c420f96e00a61c7 Mon Sep 17 00:00:00 2001 From: centra Date: Fri, 27 Mar 2026 12:14:12 +0900 Subject: [PATCH] Implement host lifecycle orchestration and distributed storage restructuring --- apigateway/Cargo.lock | 552 +++-- chainfire/Cargo.lock | 434 ++++ chainfire/chainfire-client/src/client.rs | 494 ++++- .../chainfire-api/src/cluster_service.rs | 31 +- chainfire/crates/chainfire-server/Cargo.toml | 1 + chainfire/crates/chainfire-server/src/rest.rs | 296 ++- .../crates/chainfire-server/src/server.rs | 60 +- coronafs/Cargo.lock | 1114 +++++++++- coronafs/Cargo.toml | 1 + coronafs/crates/coronafs-server/Cargo.toml | 4 + coronafs/crates/coronafs-server/src/config.rs | 45 +- coronafs/crates/coronafs-server/src/main.rs | 1756 ++++++++++++++- coronafs/scripts/benchmark-local-export.sh | 231 ++ creditservice/Cargo.lock | 561 +++-- deployer/Cargo.lock | 525 +++-- deployer/crates/deployer-ctl/Cargo.toml | 3 + deployer/crates/deployer-ctl/src/chainfire.rs | 540 ++++- deployer/crates/deployer-ctl/src/main.rs | 367 +++ deployer/crates/deployer-ctl/src/power.rs | 372 ++++ deployer/crates/deployer-server/Cargo.toml | 1 + .../crates/deployer-server/src/phone_home.rs | 29 +- deployer/crates/deployer-types/src/lib.rs | 229 ++ deployer/crates/fleet-scheduler/src/main.rs | 6 + deployer/crates/nix-agent/src/main.rs | 264 ++- .../crates/plasmacloud-reconciler/Cargo.toml | 3 + .../plasmacloud-reconciler/src/hosts.rs | 823 +++++++ .../crates/plasmacloud-reconciler/src/main.rs | 10 +- .../scripts/verify-deployer-bootstrap-e2e.sh | 30 +- .../scripts/verify-fleet-scheduler-e2e.sh | 40 +- deployer/scripts/verify-host-lifecycle-e2e.sh | 431 ++++ docs/storage-benchmarks.md | 114 +- fiberlb/Cargo.lock | 574 +++-- flake.lock | 17 +- flake.nix | 467 +++- flaredb/crates/flaredb-client/src/client.rs | 279 ++- flaredb/crates/flaredb-client/src/main.rs | 6 +- flaredb/crates/flaredb-pd/src/cluster.rs | 13 +- flaredb/crates/flaredb-pd/src/pd_service.rs | 3 +- flaredb/crates/flaredb-proto/src/pdpb.proto | 1 + .../crates/flaredb-server/src/heartbeat.rs | 27 +- flaredb/crates/flaredb-server/src/main.rs | 141 +- flaredb/crates/flaredb-server/src/rest.rs | 176 +- flaredb/flake.nix | 2 +- flaredb/scripts/verify-core.sh | 56 +- flaredb/scripts/verify-multiraft.sh | 17 +- flaredb/scripts/verify-raft.sh | 17 +- flaredb/scripts/verify-sharding.sh | 97 +- flashdns/Cargo.lock | 607 +++-- iam/Cargo.lock | 621 ++++-- iam/crates/iam-api/Cargo.toml | 3 + iam/crates/iam-api/src/credential_service.rs | 51 +- iam/crates/iam-api/src/lib.rs | 6 +- iam/crates/iam-client/src/client.rs | 338 ++- iam/crates/iam-server/src/main.rs | 30 +- iam/crates/iam-service-auth/Cargo.toml | 1 + iam/crates/iam-service-auth/src/lib.rs | 83 +- iam/crates/iam-store/src/credential_store.rs | 42 +- iam/crates/iam-store/src/lib.rs | 2 + iam/crates/iam-types/src/credential.rs | 5 + iam/crates/iam-types/src/lib.rs | 2 + iam/proto/iam.proto | 65 + k8shost/Cargo.lock | 796 ++++--- lightningstor/Cargo.lock | 588 +++-- .../src/backends/erasure_coded.rs | 432 +++- .../src/backends/replicated.rs | 567 ++++- .../src/chunk/mod.rs | 73 +- .../lightningstor-distributed/src/lib.rs | 2 + .../lightningstor-distributed/src/repair.rs | 58 + .../crates/lightningstor-node/src/storage.rs | 68 +- .../crates/lightningstor-server/Cargo.toml | 1 + .../crates/lightningstor-server/src/lib.rs | 3 + .../crates/lightningstor-server/src/main.rs | 66 +- .../lightningstor-server/src/metadata.rs | 446 +++- .../src/object_service.rs | 59 +- .../crates/lightningstor-server/src/repair.rs | 182 ++ .../lightningstor-server/src/s3/auth.rs | 456 +++- .../crates/lightningstor-server/src/s3/mod.rs | 2 +- .../lightningstor-server/src/s3/router.rs | 1864 ++++++++++++++-- .../crates/lightningstor-server/src/s3/xml.rs | 6 + .../crates/lightningstor-server/src/tenant.rs | 2 +- nix/ci/flake.lock | 146 +- nix/ci/flake.nix | 10 +- nix/images/deployer-vm-smoke-target.nix | 67 + nix/modules/cluster-config-lib.nix | 212 +- nix/modules/coronafs.nix | 139 +- nix/modules/deployer.nix | 21 +- nix/modules/first-boot-automation.nix | 2 +- nix/modules/lightningstor.nix | 32 + nix/modules/plasmacloud-cluster.nix | 17 + nix/modules/plasmavmc.nix | 114 +- nix/nodes/vm-cluster/cluster.nix | 13 + nix/nodes/vm-cluster/node01/configuration.nix | 4 +- nix/nodes/vm-cluster/node02/configuration.nix | 4 +- nix/nodes/vm-cluster/node03/configuration.nix | 4 +- nix/test-cluster/README.md | 5 +- nix/test-cluster/common.nix | 45 + nix/test-cluster/flake.nix | 5 + nix/test-cluster/node01.nix | 31 +- nix/test-cluster/node02.nix | 5 +- nix/test-cluster/node03.nix | 5 +- nix/test-cluster/node04.nix | 23 +- nix/test-cluster/node05.nix | 23 +- nix/test-cluster/node06.nix | 12 +- nix/test-cluster/run-cluster.sh | 1232 +++++++++-- nix/test-cluster/storage-node01.nix | 17 +- nix/test-cluster/storage-node02.nix | 5 +- nix/test-cluster/storage-node03.nix | 5 +- nix/test-cluster/storage-node04.nix | 23 +- nix/test-cluster/storage-node05.nix | 23 +- nix/test-cluster/vm-bench-guest-image.nix | 2 +- nix/tests/deployer-vm-smoke.nix | 403 ++++ plasmavmc/Cargo.lock | 764 ++++--- plasmavmc/crates/plasmavmc-kvm/src/env.rs | 35 + plasmavmc/crates/plasmavmc-kvm/src/lib.rs | 93 +- .../plasmavmc-server/src/artifact_store.rs | 195 +- plasmavmc/crates/plasmavmc-server/src/main.rs | 10 +- plasmavmc/crates/plasmavmc-server/src/rest.rs | 2 +- .../crates/plasmavmc-server/src/storage.rs | 136 ++ .../crates/plasmavmc-server/src/vm_service.rs | 486 +++- .../plasmavmc-server/src/volume_manager.rs | 1967 ++++++++++++++--- plasmavmc/crates/plasmavmc-types/src/vm.rs | 15 +- plasmavmc/proto/plasmavmc.proto | 3 + prismnet/Cargo.lock | 590 +++-- scripts/refresh-iam-workspace-locks.sh | 26 + 124 files changed, 21742 insertions(+), 4016 deletions(-) create mode 100755 coronafs/scripts/benchmark-local-export.sh create mode 100644 deployer/crates/deployer-ctl/src/power.rs create mode 100644 deployer/crates/plasmacloud-reconciler/src/hosts.rs create mode 100644 deployer/scripts/verify-host-lifecycle-e2e.sh create mode 100644 lightningstor/crates/lightningstor-distributed/src/repair.rs create mode 100644 lightningstor/crates/lightningstor-server/src/repair.rs create mode 100644 nix/images/deployer-vm-smoke-target.nix create mode 100644 nix/tests/deployer-vm-smoke.nix create mode 100755 scripts/refresh-iam-workspace-locks.sh diff --git a/apigateway/Cargo.lock b/apigateway/Cargo.lock index 923d3d5..481badc 100644 --- a/apigateway/Cargo.lock +++ b/apigateway/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -39,9 +74,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -54,15 +89,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -89,9 +124,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -134,6 +169,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -159,7 +206,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -170,7 +217,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -261,6 +308,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -269,9 +322,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -285,6 +338,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -296,32 +358,33 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecheck" @@ -353,15 +416,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -419,9 +482,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -432,10 +495,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.54" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -443,9 +516,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -455,27 +528,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -606,9 +679,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -625,9 +708,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -651,7 +734,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -724,9 +807,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -797,9 +880,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -812,9 +895,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -822,15 +905,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -850,38 +933,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -891,7 +974,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -932,6 +1014,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -1193,12 +1285,12 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1216,14 +1308,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1232,7 +1323,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1242,7 +1333,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64 0.22.1", "iam-audit", @@ -1252,6 +1345,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1336,6 +1430,8 @@ dependencies = [ "http 1.4.0", "iam-client", "iam-types", + "serde_json", + "tokio", "tonic", "tracing", ] @@ -1371,9 +1467,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1516,10 +1612,19 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1557,15 +1662,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1594,9 +1699,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" @@ -1604,7 +1709,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "plain", "redox_syscall 0.7.3", @@ -1623,9 +1728,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1681,9 +1786,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1753,9 +1858,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1763,6 +1868,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1798,6 +1909,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1834,29 +1956,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1876,6 +1998,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1907,7 +2041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1954,7 +2088,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.114", + "syn 2.0.117", "tempfile", ] @@ -1968,7 +2102,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2076,8 +2210,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2086,9 +2220,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2096,7 +2230,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -2114,16 +2248,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2205,7 +2339,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2214,14 +2348,14 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2231,9 +2365,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2242,9 +2376,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -2317,7 +2451,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -2334,7 +2468,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2404,11 +2538,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2429,15 +2563,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -2494,9 +2628,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -2511,15 +2645,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2548,11 +2682,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2561,9 +2695,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2586,7 +2720,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2688,9 +2822,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2713,12 +2847,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2765,7 +2899,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.36", + "rustls 0.23.37", "serde", "serde_json", "sha2", @@ -2788,7 +2922,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2810,7 +2944,7 @@ dependencies = [ "sqlx-core", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.114", + "syn 2.0.117", "tokio", "url", ] @@ -2823,7 +2957,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -2918,9 +3052,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2950,7 +3084,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2982,9 +3116,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -3019,7 +3153,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3030,7 +3164,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3085,9 +3219,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3100,9 +3234,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3110,20 +3244,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3142,7 +3276,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -3211,7 +3345,7 @@ dependencies = [ "serde_spanned", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3223,16 +3357,16 @@ dependencies = [ "indexmap 2.13.0", "toml_datetime 0.7.0", "toml_parser", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -3285,7 +3419,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3343,7 +3477,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", @@ -3387,7 +3521,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3413,9 +3547,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3449,9 +3583,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3468,6 +3602,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3545,9 +3689,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] @@ -3560,9 +3704,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3573,9 +3717,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -3587,9 +3731,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3597,22 +3741,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -3632,9 +3776,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3662,14 +3806,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3705,7 +3849,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3716,7 +3860,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3967,13 +4111,19 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.50.0" @@ -3986,9 +4136,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4024,28 +4174,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4065,7 +4215,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -4105,5 +4255,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] diff --git a/chainfire/Cargo.lock b/chainfire/Cargo.lock index 3f4be67..dfd5a17 100644 --- a/chainfire/Cargo.lock +++ b/chainfire/Cargo.lock @@ -342,6 +342,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chainfire-api" version = "0.1.0" @@ -471,6 +477,7 @@ dependencies = [ "http-body-util", "metrics", "metrics-exporter-prometheus", + "reqwest", "serde", "serde_json", "tempfile", @@ -786,6 +793,17 @@ dependencies = [ "crypto-common", ] +[[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 = "dlv-list" version = "0.3.0" @@ -978,8 +996,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -989,9 +1009,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1150,6 +1172,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -1171,6 +1194,7 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1178,7 +1202,9 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.1", "tokio", @@ -1210,6 +1236,108 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +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 = "indexmap" version = "1.9.3" @@ -1236,6 +1364,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1367,6 +1505,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -1382,6 +1526,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lz4-sys" version = "1.11.1+lz4-1.10.0" @@ -1730,6 +1880,15 @@ dependencies = [ "serde", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1889,6 +2048,61 @@ dependencies = [ "winapi", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -2030,6 +2244,44 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "ring" version = "0.17.14" @@ -2137,6 +2389,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ + "web-time", "zeroize", ] @@ -2359,6 +2612,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -2387,6 +2646,20 @@ 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 = "tempfile" @@ -2450,6 +2723,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2460,6 +2743,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -2676,9 +2974,12 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", "bytes", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -2788,6 +3089,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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" @@ -2871,6 +3190,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -2913,6 +3245,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3174,6 +3525,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -3183,6 +3540,29 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -3203,12 +3583,66 @@ dependencies = [ "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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" diff --git a/chainfire/chainfire-client/src/client.rs b/chainfire/chainfire-client/src/client.rs index 189305a..9a645f2 100644 --- a/chainfire/chainfire-client/src/client.rs +++ b/chainfire/chainfire-client/src/client.rs @@ -18,11 +18,17 @@ use chainfire_proto::proto::{ StatusRequest, TxnRequest, }; +use std::time::Duration; +use tonic::Code; use tonic::transport::Channel; -use tracing::debug; +use tracing::{debug, warn}; /// Chainfire client pub struct Client { + /// Configured client endpoints + endpoints: Vec, + /// Preferred endpoint index + current_endpoint: usize, /// gRPC channel channel: Channel, /// KV client @@ -34,36 +40,187 @@ pub struct Client { impl Client { /// Connect to a Chainfire server pub async fn connect(addr: impl AsRef) -> Result { - let addr = addr.as_ref().to_string(); - debug!(addr = %addr, "Connecting to Chainfire"); + let endpoints = parse_endpoints(addr.as_ref())?; + let mut last_error = None; - let channel = Channel::from_shared(addr) - .map_err(|e| ClientError::Connection(e.to_string()))? - .connect() - .await?; + for (index, endpoint) in endpoints.iter().enumerate() { + match connect_endpoint(endpoint).await { + Ok((channel, kv, cluster)) => { + debug!(endpoint = %endpoint, "Connected to Chainfire"); + let mut client = Self { + endpoints: endpoints.clone(), + current_endpoint: index, + channel, + kv, + cluster, + }; + client.promote_leader_endpoint().await?; + return Ok(client); + } + Err(error) => { + warn!(endpoint = %endpoint, error = %error, "Chainfire endpoint connect failed"); + last_error = Some(error); + } + } + } - let kv = KvClient::new(channel.clone()); - let cluster = ClusterClient::new(channel.clone()); + Err(last_error.unwrap_or_else(|| ClientError::Connection("no Chainfire endpoints configured".to_string()))) + } - Ok(Self { - channel, - kv, - cluster, - }) + async fn with_kv_retry(&mut self, mut op: F) -> Result + where + F: FnMut(KvClient) -> Fut, + Fut: std::future::Future>, + { + let max_attempts = self.endpoints.len().max(1) * 3; + let mut last_status = None; + for attempt in 0..max_attempts { + let client = self.kv.clone(); + match op(client).await { + Ok(value) => return Ok(value), + Err(status) if attempt + 1 < max_attempts && is_retryable_status(&status) => { + warn!( + endpoint = %self.endpoints[self.current_endpoint], + code = ?status.code(), + message = %status.message(), + attempt = attempt + 1, + max_attempts, + "retrying Chainfire KV RPC on alternate endpoint" + ); + last_status = Some(status); + self.recover_after_status(last_status.as_ref().unwrap()).await?; + tokio::time::sleep(retry_delay(attempt)).await; + } + Err(status) => return Err(status.into()), + } + } + + Err(last_status.unwrap_or_else(|| tonic::Status::unavailable("Chainfire KV retry exhausted")).into()) + } + + async fn with_cluster_retry(&mut self, mut op: F) -> Result + where + F: FnMut(ClusterClient) -> Fut, + Fut: std::future::Future>, + { + let max_attempts = self.endpoints.len().max(1) * 3; + let mut last_status = None; + for attempt in 0..max_attempts { + let client = self.cluster.clone(); + match op(client).await { + Ok(value) => return Ok(value), + Err(status) if attempt + 1 < max_attempts && is_retryable_status(&status) => { + warn!( + endpoint = %self.endpoints[self.current_endpoint], + code = ?status.code(), + message = %status.message(), + attempt = attempt + 1, + max_attempts, + "retrying Chainfire cluster RPC on alternate endpoint" + ); + last_status = Some(status); + self.recover_after_status(last_status.as_ref().unwrap()).await?; + tokio::time::sleep(retry_delay(attempt)).await; + } + Err(status) => return Err(status.into()), + } + } + + Err(last_status.unwrap_or_else(|| tonic::Status::unavailable("Chainfire cluster retry exhausted")).into()) + } + + async fn recover_after_status(&mut self, status: &tonic::Status) -> Result<()> { + if let Some(leader_idx) = self.discover_leader_endpoint().await? { + if leader_idx != self.current_endpoint { + return self.reconnect_to_index(leader_idx).await; + } + } + + if self.endpoints.len() > 1 { + let next = (self.current_endpoint + 1) % self.endpoints.len(); + if next != self.current_endpoint { + return self.reconnect_to_index(next).await; + } + } + + Err(ClientError::Rpc(status.clone())) + } + + async fn reconnect_to_index(&mut self, index: usize) -> Result<()> { + let endpoint = self + .endpoints + .get(index) + .ok_or_else(|| ClientError::Connection(format!("invalid Chainfire endpoint index {index}")))? + .clone(); + let (channel, kv, cluster) = connect_endpoint(&endpoint).await?; + self.current_endpoint = index; + self.channel = channel; + self.kv = kv; + self.cluster = cluster; + Ok(()) + } + + async fn promote_leader_endpoint(&mut self) -> Result<()> { + if let Some(index) = self.discover_leader_endpoint().await? { + if index != self.current_endpoint { + self.reconnect_to_index(index).await?; + } + } + Ok(()) + } + + async fn discover_leader_endpoint(&self) -> Result> { + for (index, endpoint) in self.endpoints.iter().enumerate() { + let mut cluster = match ClusterClient::connect(endpoint.clone()).await { + Ok(client) => client, + Err(error) => { + warn!(endpoint = %endpoint, error = %error, "failed to connect while probing Chainfire leader"); + continue; + } + }; + + match cluster.status(StatusRequest {}).await { + Ok(response) => { + let status = response.into_inner(); + let member_id = status.header.as_ref().map(|header| header.member_id).unwrap_or(0); + if status.leader != 0 && status.leader == member_id { + return Ok(Some(index)); + } + } + Err(status) => { + warn!( + endpoint = %endpoint, + code = ?status.code(), + message = %status.message(), + "failed to query Chainfire leader status" + ); + } + } + } + + Ok(None) } /// Put a key-value pair pub async fn put(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result { + let key = key.as_ref().to_vec(); + let value = value.as_ref().to_vec(); let resp = self - .kv - .put(PutRequest { - key: key.as_ref().to_vec(), - value: value.as_ref().to_vec(), - lease: 0, - prev_kv: false, + .with_kv_retry(|mut kv| { + let key = key.clone(); + let value = value.clone(); + async move { + kv.put(PutRequest { + key, + value, + lease: 0, + prev_kv: false, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; Ok(resp.header.map(|h| h.revision as u64).unwrap_or(0)) } @@ -86,19 +243,25 @@ impl Client { &mut self, key: impl AsRef<[u8]>, ) -> Result, u64)>> { + let key = key.as_ref().to_vec(); let resp = self - .kv - .range(RangeRequest { - key: key.as_ref().to_vec(), - range_end: vec![], - limit: 1, - revision: 0, - keys_only: false, - count_only: false, - serializable: false, // default: linearizable read + .with_kv_retry(|mut kv| { + let key = key.clone(); + async move { + kv.range(RangeRequest { + key, + range_end: vec![], + limit: 1, + revision: 0, + keys_only: false, + count_only: false, + serializable: false, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; Ok(resp.kvs.into_iter().next().map(|kv| (kv.value, kv.mod_revision as u64))) } @@ -132,14 +295,20 @@ impl Client { })), }; - self.kv - .txn(TxnRequest { - compare: vec![compare], - success: vec![put_op], - failure: vec![], - }) - .await? - .into_inner(); + self.with_kv_retry(|mut kv| { + let compare = compare.clone(); + let put_op = put_op.clone(); + async move { + kv.txn(TxnRequest { + compare: vec![compare], + success: vec![put_op], + failure: vec![], + }) + .await + .map(|resp| resp.into_inner()) + } + }) + .await?; Ok(()) } @@ -152,15 +321,21 @@ impl Client { /// Delete a key pub async fn delete(&mut self, key: impl AsRef<[u8]>) -> Result { + let key = key.as_ref().to_vec(); let resp = self - .kv - .delete(DeleteRangeRequest { - key: key.as_ref().to_vec(), - range_end: vec![], - prev_kv: false, + .with_kv_retry(|mut kv| { + let key = key.clone(); + async move { + kv.delete(DeleteRangeRequest { + key, + range_end: vec![], + prev_kv: false, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; Ok(resp.deleted > 0) } @@ -171,18 +346,24 @@ impl Client { let range_end = prefix_end(prefix); let resp = self - .kv - .range(RangeRequest { - key: prefix.to_vec(), - range_end, - limit: 0, - revision: 0, - keys_only: false, - count_only: false, - serializable: false, + .with_kv_retry(|mut kv| { + let key = prefix.to_vec(); + let range_end = range_end.clone(); + async move { + kv.range(RangeRequest { + key, + range_end, + limit: 0, + revision: 0, + keys_only: false, + count_only: false, + serializable: false, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; Ok(resp.kvs.into_iter().map(|kv| (kv.key, kv.value)).collect()) } @@ -197,18 +378,24 @@ impl Client { let range_end = prefix_end(prefix); let resp = self - .kv - .range(RangeRequest { - key: prefix.to_vec(), - range_end, - limit, - revision: 0, - keys_only: false, - count_only: false, - serializable: false, + .with_kv_retry(|mut kv| { + let key = prefix.to_vec(); + let range_end = range_end.clone(); + async move { + kv.range(RangeRequest { + key, + range_end, + limit, + revision: 0, + keys_only: false, + count_only: false, + serializable: false, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; let more = resp.more; let kvs: Vec<(Vec, Vec, u64)> = resp @@ -238,18 +425,24 @@ impl Client { limit: i64, ) -> Result<(Vec<(Vec, Vec, u64)>, Option>)> { let resp = self - .kv - .range(RangeRequest { - key: start.as_ref().to_vec(), - range_end: end.as_ref().to_vec(), - limit, - revision: 0, - keys_only: false, - count_only: false, - serializable: false, + .with_kv_retry(|mut kv| { + let key = start.as_ref().to_vec(); + let range_end = end.as_ref().to_vec(); + async move { + kv.range(RangeRequest { + key, + range_end, + limit, + revision: 0, + keys_only: false, + count_only: false, + serializable: false, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; let more = resp.more; let kvs: Vec<(Vec, Vec, u64)> = resp @@ -309,14 +502,21 @@ impl Client { }; let resp = self - .kv - .txn(TxnRequest { - compare: vec![compare], - success: vec![put_op], - failure: vec![read_on_fail], + .with_kv_retry(|mut kv| { + let compare = compare.clone(); + let put_op = put_op.clone(); + let read_on_fail = read_on_fail.clone(); + async move { + kv.txn(TxnRequest { + compare: vec![compare], + success: vec![put_op], + failure: vec![read_on_fail], + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; if resp.succeeded { let new_version = resp @@ -371,10 +571,13 @@ impl Client { /// Get cluster status pub async fn status(&mut self) -> Result { let resp = self - .cluster - .status(StatusRequest {}) - .await? - .into_inner(); + .with_cluster_retry(|mut cluster| async move { + cluster + .status(StatusRequest {}) + .await + .map(|resp| resp.into_inner()) + }) + .await?; Ok(ClusterStatus { version: resp.version, @@ -392,15 +595,22 @@ impl Client { /// # Returns /// The node ID of the added member pub async fn member_add(&mut self, node_id: u64, peer_url: impl AsRef, is_learner: bool) -> Result { + let peer_url = peer_url.as_ref().to_string(); let resp = self - .cluster - .member_add(MemberAddRequest { - node_id, - peer_urls: vec![peer_url.as_ref().to_string()], - is_learner, + .with_cluster_retry(|mut cluster| { + let peer_url = peer_url.clone(); + async move { + cluster + .member_add(MemberAddRequest { + node_id, + peer_urls: vec![peer_url], + is_learner, + }) + .await + .map(|resp| resp.into_inner()) + } }) - .await? - .into_inner(); + .await?; // Extract the member ID from the response let member_id = resp @@ -410,7 +620,7 @@ impl Client { debug!( member_id = member_id, - peer_url = peer_url.as_ref(), + peer_url = peer_url.as_str(), is_learner = is_learner, "Added member to cluster" ); @@ -441,6 +651,64 @@ pub struct CasOutcome { pub new_version: u64, } +fn parse_endpoints(input: &str) -> Result> { + let endpoints: Vec = input + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(normalize_endpoint) + .collect(); + + if endpoints.is_empty() { + return Err(ClientError::Connection("no Chainfire endpoints configured".to_string())); + } + + Ok(endpoints) +} + +fn normalize_endpoint(endpoint: &str) -> String { + if endpoint.contains("://") { + endpoint.to_string() + } else { + format!("http://{endpoint}") + } +} + +async fn connect_endpoint(endpoint: &str) -> Result<(Channel, KvClient, ClusterClient)> { + let channel = Channel::from_shared(endpoint.to_string()) + .map_err(|e| ClientError::Connection(e.to_string()))? + .connect() + .await?; + + let kv = KvClient::new(channel.clone()); + let cluster = ClusterClient::new(channel.clone()); + Ok((channel, kv, cluster)) +} + +fn retry_delay(attempt: usize) -> Duration { + let multiplier = 1u64 << attempt.min(3); + Duration::from_millis((200 * multiplier).min(1_000)) +} + +fn is_retryable_status(status: &tonic::Status) -> bool { + matches!( + status.code(), + Code::Unavailable | Code::DeadlineExceeded | Code::Internal | Code::Aborted | Code::FailedPrecondition + ) || retryable_message(status.message()) +} + +fn retryable_message(message: &str) -> bool { + let lowercase = message.to_ascii_lowercase(); + lowercase.contains("not leader") + || lowercase.contains("leader_id") + || lowercase.contains("transport error") + || lowercase.contains("connection was not ready") + || lowercase.contains("deadline has elapsed") + || lowercase.contains("broken pipe") + || lowercase.contains("connection reset") + || lowercase.contains("connection refused") +} + /// Calculate prefix end for range queries fn prefix_end(prefix: &[u8]) -> Vec { let mut end = prefix.to_vec(); @@ -463,4 +731,30 @@ mod tests { assert_eq!(prefix_end(b"abc"), b"abd"); assert_eq!(prefix_end(b"/nodes/"), b"/nodes0"); } + + #[test] + fn normalize_endpoint_adds_http_scheme() { + assert_eq!(normalize_endpoint("127.0.0.1:2379"), "http://127.0.0.1:2379"); + assert_eq!(normalize_endpoint("http://127.0.0.1:2379"), "http://127.0.0.1:2379"); + } + + #[test] + fn parse_endpoints_accepts_comma_separated_values() { + let endpoints = parse_endpoints("127.0.0.1:2379, http://127.0.0.2:2379").unwrap(); + assert_eq!( + endpoints, + vec![ + "http://127.0.0.1:2379".to_string(), + "http://127.0.0.2:2379".to_string() + ] + ); + } + + #[test] + fn retryable_message_covers_not_leader_and_transport() { + assert!(retryable_message("NotLeader { leader_id: Some(1) }")); + assert!(retryable_message("transport error")); + assert!(retryable_message("connection was not ready")); + assert!(!retryable_message("permission denied")); + } } diff --git a/chainfire/crates/chainfire-api/src/cluster_service.rs b/chainfire/crates/chainfire-api/src/cluster_service.rs index 674fa90..9f83685 100644 --- a/chainfire/crates/chainfire-api/src/cluster_service.rs +++ b/chainfire/crates/chainfire-api/src/cluster_service.rs @@ -27,17 +27,25 @@ pub struct ClusterServiceImpl { rpc_client: Arc, /// Cluster ID cluster_id: u64, + /// Configured members with client and peer URLs + members: Vec, /// Server version version: String, } impl ClusterServiceImpl { /// Create a new cluster service - pub fn new(raft: Arc, rpc_client: Arc, cluster_id: u64) -> Self { + pub fn new( + raft: Arc, + rpc_client: Arc, + cluster_id: u64, + members: Vec, + ) -> Self { Self { raft, rpc_client, cluster_id, + members, version: env!("CARGO_PKG_VERSION").to_string(), } } @@ -47,16 +55,19 @@ impl ClusterServiceImpl { } /// Get current members as proto Member list - /// NOTE: Custom RaftCore doesn't track membership dynamically yet + /// NOTE: Custom RaftCore doesn't track membership dynamically yet, so this returns + /// the configured static membership that the server was booted with. async fn get_member_list(&self) -> Vec { - // For now, return only the current node - vec![Member { - id: self.raft.node_id(), - name: format!("node-{}", self.raft.node_id()), - peer_urls: vec![], - client_urls: vec![], - is_learner: false, - }] + if self.members.is_empty() { + return vec![Member { + id: self.raft.node_id(), + name: format!("node-{}", self.raft.node_id()), + peer_urls: vec![], + client_urls: vec![], + is_learner: false, + }]; + } + self.members.clone() } } diff --git a/chainfire/crates/chainfire-server/Cargo.toml b/chainfire/crates/chainfire-server/Cargo.toml index 0b7b06d..8acd055 100644 --- a/chainfire/crates/chainfire-server/Cargo.toml +++ b/chainfire/crates/chainfire-server/Cargo.toml @@ -42,6 +42,7 @@ http-body-util = { workspace = true } uuid = { version = "1.11", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } serde_json = "1.0" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } # Configuration clap.workspace = true diff --git a/chainfire/crates/chainfire-server/src/rest.rs b/chainfire/crates/chainfire-server/src/rest.rs index bd06595..3b615a2 100644 --- a/chainfire/crates/chainfire-server/src/rest.rs +++ b/chainfire/crates/chainfire-server/src/rest.rs @@ -11,13 +11,14 @@ use axum::{ extract::{Path, Query, State}, http::StatusCode, - routing::{delete, get, post, put}, + routing::{get, post}, Json, Router, }; use chainfire_api::GrpcRaftClient; -use chainfire_raft::RaftCore; +use chainfire_raft::{core::RaftError, RaftCore}; use chainfire_types::command::RaftCommand; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::sync::Arc; /// REST API state @@ -26,16 +27,18 @@ pub struct RestApiState { pub raft: Arc, pub cluster_id: u64, pub rpc_client: Option>, + pub http_client: reqwest::Client, + pub peer_http_addrs: Arc>, } /// Standard REST error response -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ErrorResponse { pub error: ErrorDetail, pub meta: ResponseMeta, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ErrorDetail { pub code: String, pub message: String, @@ -43,7 +46,7 @@ pub struct ErrorDetail { pub details: Option, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ResponseMeta { pub request_id: String, pub timestamp: String, @@ -59,7 +62,7 @@ impl ResponseMeta { } /// Standard REST success response -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct SuccessResponse { pub data: T, pub meta: ResponseMeta, @@ -75,25 +78,25 @@ impl SuccessResponse { } /// KV Put request body -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct PutRequest { pub value: String, } /// KV Get response -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct GetResponse { pub key: String, pub value: String, } /// KV List response -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ListResponse { pub items: Vec, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct KvItem { pub key: String, pub value: String, @@ -129,6 +132,13 @@ pub struct AddMemberRequestLegacy { #[derive(Debug, Deserialize)] pub struct PrefixQuery { pub prefix: Option, + pub consistency: Option, +} + +/// Query parameters for key reads +#[derive(Debug, Default, Deserialize)] +pub struct ReadQuery { + pub consistency: Option, } /// Build the REST API router @@ -153,80 +163,11 @@ async fn health_check() -> (StatusCode, Json> ) } -/// GET /api/v1/kv/{key} - Get value -async fn get_kv( - State(state): State, - Path(key): Path, -) -> Result>, (StatusCode, Json)> { - let sm = state.raft.state_machine(); - let key_bytes = key.as_bytes().to_vec(); - - let results = sm.kv() - .get(&key_bytes) - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; - - let value = results - .into_iter() - .next() - .ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Key not found"))?; - - Ok(Json(SuccessResponse::new(GetResponse { - key, - value: String::from_utf8_lossy(&value.value).to_string(), - }))) -} - -/// PUT /api/v1/kv/{key} - Put value -async fn put_kv( - State(state): State, - Path(key): Path, - Json(req): Json, -) -> Result<(StatusCode, Json>), (StatusCode, Json)> { - let command = RaftCommand::Put { - key: key.as_bytes().to_vec(), - value: req.value.as_bytes().to_vec(), - lease_id: None, - prev_kv: false, - }; - - state - .raft - .client_write(command) - .await - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; - - Ok(( - StatusCode::OK, - Json(SuccessResponse::new(serde_json::json!({ "key": key, "success": true }))), - )) -} - -/// DELETE /api/v1/kv/{key} - Delete key -async fn delete_kv( - State(state): State, - Path(key): Path, -) -> Result<(StatusCode, Json>), (StatusCode, Json)> { - let command = RaftCommand::Delete { - key: key.as_bytes().to_vec(), - prev_kv: false, - }; - - state - .raft - .client_write(command) - .await - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; - - Ok(( - StatusCode::OK, - Json(SuccessResponse::new(serde_json::json!({ "key": key, "success": true }))), - )) -} - /// GET /api/v1/kv/*key - Get value (wildcard for all keys) async fn get_kv_wildcard( State(state): State, Path(key): Path, + Query(query): Query, ) -> Result>, (StatusCode, Json)> { // Use key as-is for simple keys, prepend / for namespaced keys // Keys like "testkey" stay as "testkey", keys like "flaredb/stores/1" become "/flaredb/stores/1" @@ -235,6 +176,14 @@ async fn get_kv_wildcard( } else { key.clone() }; + if should_proxy_read(query.consistency.as_deref(), &state).await { + return proxy_read_to_leader( + &state, + &format!("/api/v1/kv/{}", full_key.trim_start_matches('/')), + None, + ) + .await; + } let sm = state.raft.state_machine(); let key_bytes = full_key.as_bytes().to_vec(); @@ -272,11 +221,7 @@ async fn put_kv_wildcard( prev_kv: false, }; - state - .raft - .client_write(command) - .await - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; + submit_rest_write(&state, command, Some(&req), &full_key, reqwest::Method::PUT).await?; Ok(( StatusCode::OK, @@ -300,11 +245,7 @@ async fn delete_kv_wildcard( prev_kv: false, }; - state - .raft - .client_write(command) - .await - .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; + submit_rest_write(&state, command, None, &full_key, reqwest::Method::DELETE).await?; Ok(( StatusCode::OK, @@ -317,6 +258,13 @@ async fn list_kv( State(state): State, Query(params): Query, ) -> Result>, (StatusCode, Json)> { + if should_proxy_read(params.consistency.as_deref(), &state).await { + let query = params + .prefix + .as_ref() + .map(|prefix| vec![("prefix", prefix.as_str())]); + return proxy_read_to_leader(&state, "/api/v1/kv", query.as_deref()).await; + } let prefix = params.prefix.unwrap_or_default(); let sm = state.raft.state_machine(); @@ -445,4 +393,170 @@ fn error_response( meta: ResponseMeta::new(), }), ) -} \ No newline at end of file +} + +async fn submit_rest_write( + state: &RestApiState, + command: RaftCommand, + body: Option<&PutRequest>, + key: &str, + method: reqwest::Method, +) -> Result<(), (StatusCode, Json)> { + match state.raft.client_write(command).await { + Ok(()) => Ok(()), + Err(RaftError::NotLeader { leader_id }) => { + let resolved_leader = match leader_id { + Some(leader_id) => Some(leader_id), + None => state.raft.leader().await, + }; + proxy_write_to_leader(state, resolved_leader, key, method, body).await + } + Err(err) => Err(error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "INTERNAL_ERROR", + &err.to_string(), + )), + } +} + +async fn proxy_write_to_leader( + state: &RestApiState, + leader_id: Option, + key: &str, + method: reqwest::Method, + body: Option<&PutRequest>, +) -> Result<(), (StatusCode, Json)> { + let leader_id = leader_id.ok_or_else(|| { + error_response( + StatusCode::SERVICE_UNAVAILABLE, + "NOT_LEADER", + "current node is not the leader and no leader is known yet", + ) + })?; + let leader_http_addr = state.peer_http_addrs.get(&leader_id).ok_or_else(|| { + error_response( + StatusCode::SERVICE_UNAVAILABLE, + "NOT_LEADER", + &format!("leader {leader_id} is known but has no HTTP endpoint mapping"), + ) + })?; + let url = format!( + "{}/api/v1/kv/{}", + leader_http_addr.trim_end_matches('/'), + key.trim_start_matches('/') + ); + let mut request = state.http_client.request(method, &url); + if let Some(body) = body { + request = request.json(body); + } + let response = request.send().await.map_err(|err| { + error_response( + StatusCode::BAD_GATEWAY, + "LEADER_PROXY_FAILED", + &format!("failed to forward write to leader {leader_id}: {err}"), + ) + })?; + if response.status().is_success() { + return Ok(()); + } + let status = StatusCode::from_u16(response.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); + let payload = response.json::().await.unwrap_or_else(|err| ErrorResponse { + error: ErrorDetail { + code: "LEADER_PROXY_FAILED".to_string(), + message: format!("leader {leader_id} returned {status}: {err}"), + details: None, + }, + meta: ResponseMeta::new(), + }); + Err((status, Json(payload))) +} + +async fn should_proxy_read(consistency: Option<&str>, state: &RestApiState) -> bool { + let node_id = state.raft.node_id(); + let leader_id = state.raft.leader().await; + read_requires_leader_proxy(consistency, node_id, leader_id) +} + +fn read_requires_leader_proxy( + consistency: Option<&str>, + node_id: u64, + leader_id: Option, +) -> bool { + if matches!(consistency, Some(mode) if mode.eq_ignore_ascii_case("local")) { + return false; + } + matches!(leader_id, Some(leader_id) if leader_id != node_id) +} + +async fn proxy_read_to_leader( + state: &RestApiState, + path: &str, + query: Option<&[(&str, &str)]>, +) -> Result>, (StatusCode, Json)> +where + T: for<'de> Deserialize<'de>, +{ + let leader_id = state.raft.leader().await.ok_or_else(|| { + error_response( + StatusCode::SERVICE_UNAVAILABLE, + "NOT_LEADER", + "current node is not the leader and no leader is known yet", + ) + })?; + let leader_http_addr = state.peer_http_addrs.get(&leader_id).ok_or_else(|| { + error_response( + StatusCode::SERVICE_UNAVAILABLE, + "NOT_LEADER", + &format!("leader {leader_id} is known but has no HTTP endpoint mapping"), + ) + })?; + let url = format!( + "{}{}", + leader_http_addr.trim_end_matches('/'), + path + ); + let mut request = state.http_client.get(&url); + if let Some(query) = query { + request = request.query(query); + } + let response = request.send().await.map_err(|err| { + error_response( + StatusCode::BAD_GATEWAY, + "LEADER_PROXY_FAILED", + &format!("failed to forward read to leader {leader_id}: {err}"), + ) + })?; + if response.status().is_success() { + let payload = response.json::>().await.map_err(|err| { + error_response( + StatusCode::BAD_GATEWAY, + "LEADER_PROXY_FAILED", + &format!("failed to decode leader {leader_id} response: {err}"), + ) + })?; + return Ok(Json(payload)); + } + let status = StatusCode::from_u16(response.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); + let payload = response.json::().await.unwrap_or_else(|err| ErrorResponse { + error: ErrorDetail { + code: "LEADER_PROXY_FAILED".to_string(), + message: format!("leader {leader_id} returned {status}: {err}"), + details: None, + }, + meta: ResponseMeta::new(), + }); + Err((status, Json(payload))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_requires_leader_proxy_defaults_to_leader_consistency() { + assert!(read_requires_leader_proxy(None, 2, Some(1))); + assert!(!read_requires_leader_proxy(Some("local"), 2, Some(1))); + assert!(!read_requires_leader_proxy(None, 2, Some(2))); + assert!(!read_requires_leader_proxy(None, 2, None)); + } +} diff --git a/chainfire/crates/chainfire-server/src/server.rs b/chainfire/crates/chainfire-server/src/server.rs index 6b4f98b..354bb62 100644 --- a/chainfire/crates/chainfire-server/src/server.rs +++ b/chainfire/crates/chainfire-server/src/server.rs @@ -11,10 +11,11 @@ use crate::rest::{build_router, RestApiState}; use anyhow::Result; use chainfire_api::internal_proto::raft_service_server::RaftServiceServer; use chainfire_api::proto::{ - cluster_server::ClusterServer, kv_server::KvServer, watch_server::WatchServer, + cluster_server::ClusterServer, kv_server::KvServer, watch_server::WatchServer, Member, }; use chainfire_api::{ClusterServiceImpl, KvServiceImpl, RaftServiceImpl, WatchServiceImpl}; use chainfire_types::RaftRole; +use std::collections::HashMap; use std::sync::Arc; use tokio::signal; use tonic::transport::{Certificate, Identity, Server as TonicServer, ServerTlsConfig}; @@ -109,6 +110,7 @@ impl Server { Arc::clone(&raft), rpc_client, self.node.cluster_id(), + configured_members(&self.config), ); // Internal Raft service for inter-node communication @@ -166,10 +168,24 @@ impl Server { // HTTP REST API server let http_addr = self.config.network.http_addr; + let http_port = self.config.network.http_addr.port(); + let peer_http_addrs = Arc::new( + self.config + .cluster + .initial_members + .iter() + .filter_map(|member| { + http_endpoint_from_raft_addr(&member.raft_addr, http_port) + .map(|http_addr| (member.id, http_addr)) + }) + .collect::>(), + ); let rest_state = RestApiState { raft: Arc::clone(&raft), cluster_id: self.node.cluster_id(), rpc_client: self.node.rpc_client().cloned(), + http_client: reqwest::Client::new(), + peer_http_addrs, }; let rest_app = build_router(rest_state); let http_listener = tokio::net::TcpListener::bind(&http_addr).await?; @@ -286,3 +302,45 @@ impl Server { Ok(()) } } + +fn http_endpoint_from_raft_addr(raft_addr: &str, http_port: u16) -> Option { + if let Ok(addr) = raft_addr.parse::() { + return Some(format!("http://{}:{}", addr.ip(), http_port)); + } + let (host, _) = raft_addr.rsplit_once(':')?; + Some(format!("http://{}:{}", host, http_port)) +} + +fn grpc_endpoint_from_raft_addr(raft_addr: &str, api_port: u16) -> Option { + if let Ok(addr) = raft_addr.parse::() { + return Some(format!("http://{}:{}", addr.ip(), api_port)); + } + let (host, _) = raft_addr.rsplit_once(':')?; + Some(format!("http://{}:{}", host, api_port)) +} + +fn normalize_peer_url(raft_addr: &str) -> String { + if raft_addr.contains("://") { + raft_addr.to_string() + } else { + format!("http://{raft_addr}") + } +} + +fn configured_members(config: &ServerConfig) -> Vec { + let api_port = config.network.api_addr.port(); + config + .cluster + .initial_members + .iter() + .map(|member| Member { + id: member.id, + name: format!("node-{}", member.id), + peer_urls: vec![normalize_peer_url(&member.raft_addr)], + client_urls: grpc_endpoint_from_raft_addr(&member.raft_addr, api_port) + .into_iter() + .collect(), + is_learner: false, + }) + .collect() +} diff --git a/coronafs/Cargo.lock b/coronafs/Cargo.lock index c52df03..9a845ff 100644 --- a/coronafs/Cargo.lock +++ b/coronafs/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -140,6 +140,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.11.0" @@ -174,6 +180,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -247,15 +259,28 @@ dependencies = [ "chrono", "clap", "futures-util", + "reqwest", "serde", "serde_json", - "thiserror", + "tempfile", + "thiserror 1.0.69", "tokio", "toml", "tracing", "tracing-subscriber", ] +[[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 = "equivalent" version = "1.0.2" @@ -269,15 +294,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -332,6 +369,55 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -408,6 +494,24 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", ] [[package]] @@ -416,13 +520,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -449,6 +561,114 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +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 = "indexmap" version = "2.13.0" @@ -456,7 +676,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", ] [[package]] @@ -487,12 +725,30 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -508,6 +764,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -543,7 +805,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -552,7 +814,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -617,6 +879,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -626,6 +916,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -635,6 +980,47 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -661,6 +1047,112 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[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.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[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.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -679,6 +1171,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -798,15 +1296,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[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.117" @@ -823,6 +1333,33 @@ 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 = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] [[package]] name = "thiserror" @@ -830,7 +1367,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -844,6 +1390,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -853,6 +1410,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -867,7 +1449,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -881,6 +1463,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -938,6 +1530,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1012,12 +1622,48 @@ dependencies = [ "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.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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" @@ -1030,12 +1676,39 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[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.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -1049,6 +1722,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -1081,6 +1768,69 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1140,6 +1890,24 @@ 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.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1149,6 +1917,135 @@ dependencies = [ "windows-link", ] +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -1158,6 +2055,203 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/coronafs/Cargo.toml b/coronafs/Cargo.toml index 217013f..ae89bdd 100644 --- a/coronafs/Cargo.toml +++ b/coronafs/Cargo.toml @@ -24,6 +24,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = "1.0" thiserror = "1.0" chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } [workspace.lints.rust] unsafe_code = "deny" diff --git a/coronafs/crates/coronafs-server/Cargo.toml b/coronafs/crates/coronafs-server/Cargo.toml index e4aeefb..b2117ed 100644 --- a/coronafs/crates/coronafs-server/Cargo.toml +++ b/coronafs/crates/coronafs-server/Cargo.toml @@ -21,7 +21,11 @@ tracing-subscriber = { workspace = true } anyhow = { workspace = true } thiserror = { workspace = true } chrono = { workspace = true } +reqwest = { workspace = true } futures-util = "0.3" +[dev-dependencies] +tempfile = "3" + [lints] workspace = true diff --git a/coronafs/crates/coronafs-server/src/config.rs b/coronafs/crates/coronafs-server/src/config.rs index 712c788..fb98da7 100644 --- a/coronafs/crates/coronafs-server/src/config.rs +++ b/coronafs/crates/coronafs-server/src/config.rs @@ -2,9 +2,40 @@ use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::PathBuf; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ServerMode { + Combined, + Controller, + Node, +} + +impl Default for ServerMode { + fn default() -> Self { + Self::Combined + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum MetadataBackend { + Filesystem, + Chainfire, +} + +impl Default for MetadataBackend { + fn default() -> Self { + Self::Filesystem + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct ServerConfig { + pub mode: ServerMode, + pub metadata_backend: MetadataBackend, + pub chainfire_api_url: Option, + pub chainfire_key_prefix: String, pub listen_addr: SocketAddr, pub advertise_host: String, pub data_dir: PathBuf, @@ -26,6 +57,10 @@ pub struct ServerConfig { impl Default for ServerConfig { fn default() -> Self { Self { + mode: ServerMode::Combined, + metadata_backend: MetadataBackend::Filesystem, + chainfire_api_url: None, + chainfire_key_prefix: "/coronafs/volumes".to_string(), listen_addr: "0.0.0.0:50088".parse().expect("valid listen addr"), advertise_host: "127.0.0.1".to_string(), data_dir: PathBuf::from("/var/lib/coronafs"), @@ -34,7 +69,7 @@ impl Default for ServerConfig { export_port_count: 512, export_shared_clients: 32, export_cache_mode: "none".to_string(), - export_aio_mode: "io_uring".to_string(), + export_aio_mode: "threads".to_string(), export_discard_mode: "unmap".to_string(), export_detect_zeroes_mode: "unmap".to_string(), preallocate: true, @@ -47,6 +82,14 @@ impl Default for ServerConfig { } impl ServerConfig { + pub fn supports_controller_api(&self) -> bool { + matches!(self.mode, ServerMode::Combined | ServerMode::Controller) + } + + pub fn supports_node_api(&self) -> bool { + matches!(self.mode, ServerMode::Combined | ServerMode::Node) + } + pub fn volume_dir(&self) -> PathBuf { self.data_dir.join("volumes") } diff --git a/coronafs/crates/coronafs-server/src/main.rs b/coronafs/crates/coronafs-server/src/main.rs index 5efb10e..d819866 100644 --- a/coronafs/crates/coronafs-server/src/main.rs +++ b/coronafs/crates/coronafs-server/src/main.rs @@ -8,12 +8,12 @@ use axum::response::{IntoResponse, Response}; use axum::routing::{get, post, put}; use axum::{Json, Router}; use clap::Parser; -use config::ServerConfig; +use config::{MetadataBackend, ServerConfig, ServerMode}; use futures_util::StreamExt; use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use std::collections::{HashMap, HashSet}; use std::path::{Path as FsPath, PathBuf}; use std::sync::Arc; use tokio::fs; @@ -33,8 +33,16 @@ struct Args { struct VolumeMetadata { id: String, size_bytes: u64, + #[serde(default)] + format: VolumeFileFormat, + #[serde(default)] + node_local: bool, + #[serde(default)] + materialized_from: Option, port: Option, export_pid: Option, + #[serde(default)] + export_read_only: Option, created_at: String, updated_at: String, } @@ -43,20 +51,42 @@ struct VolumeMetadata { struct VolumeResponse { id: String, size_bytes: u64, + format: String, + node_local: bool, + materialized_from: Option, path: String, export: Option, } +#[derive(Debug, Serialize)] +struct VolumeListResponse { + items: Vec, +} + #[derive(Debug, Serialize)] struct ExportResponse { uri: String, port: u16, pid: Option, + read_only: bool, +} + +#[derive(Debug, Serialize)] +struct CapabilitiesResponse { + mode: ServerMode, + supports_controller_api: bool, + supports_node_api: bool, } #[derive(Debug, Deserialize)] struct CreateVolumeRequest { size_bytes: u64, + #[serde(default)] + format: Option, + #[serde(default)] + backing_file: Option, + #[serde(default)] + backing_format: Option, } #[derive(Debug, Deserialize)] @@ -74,19 +104,339 @@ struct ExportQuery { read_only: Option, } +#[derive(Debug, Deserialize)] +struct MaterializeVolumeRequest { + source_uri: String, + #[serde(default)] + size_bytes: Option, + #[serde(default)] + format: Option, + #[serde(default)] + lazy: bool, +} + #[derive(Clone)] struct AppState { config: Arc, + metadata_store: MetadataStore, volume_guards: Arc>>>>, reserved_ports: Arc>>, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum VolumeFileFormat { + Raw, + Qcow2, +} + +impl Default for VolumeFileFormat { + fn default() -> Self { + Self::Raw + } +} + +impl VolumeFileFormat { + fn as_qemu_arg(self) -> &'static str { + match self { + Self::Raw => "raw", + Self::Qcow2 => "qcow2", + } + } +} + +#[derive(Clone)] +enum MetadataStore { + Filesystem, + Chainfire(ChainfireMetadataStore), +} + +#[derive(Clone)] +struct ChainfireMetadataStore { + client: reqwest::Client, + base_urls: Vec, + key_prefix: String, +} + +#[derive(Debug, Deserialize)] +struct ChainfireGetEnvelope { + data: ChainfireValueEntry, +} + +#[derive(Debug, Deserialize)] +struct ChainfireValueEntry { + value: String, +} + +#[derive(Debug, Deserialize)] +struct ChainfireListEnvelope { + data: ChainfireListData, +} + +#[derive(Debug, Deserialize)] +struct ChainfireListData { + items: Vec, +} + +#[derive(Debug, Deserialize)] +struct ChainfireKvItem { + key: String, + value: String, +} + +impl MetadataStore { + fn from_config(config: &ServerConfig) -> Result { + match config.metadata_backend { + MetadataBackend::Filesystem => Ok(Self::Filesystem), + MetadataBackend::Chainfire => { + let base_url = config.chainfire_api_url.clone().ok_or_else(|| { + anyhow!("chainfire_api_url must be set when metadata_backend = chainfire") + })?; + Ok(Self::Chainfire(ChainfireMetadataStore { + client: reqwest::Client::new(), + base_urls: parse_chainfire_base_urls(&base_url)?, + key_prefix: config.chainfire_key_prefix.clone(), + })) + } + } + } + + async fn load(&self, config: &ServerConfig, id: &str) -> Result> { + match self { + Self::Filesystem => load_metadata_file(&metadata_path(config, id)).await, + Self::Chainfire(store) => store.load(id).await, + } + } + + async fn save(&self, config: &ServerConfig, meta: &VolumeMetadata) -> Result<()> { + match self { + Self::Filesystem => save_metadata_file(&metadata_path(config, &meta.id), meta).await, + Self::Chainfire(store) => store.save(meta).await, + } + } + + async fn delete(&self, config: &ServerConfig, id: &str) -> Result<()> { + match self { + Self::Filesystem => delete_metadata_file(&metadata_path(config, id)).await, + Self::Chainfire(store) => store.delete(id).await, + } + } + + async fn list(&self, config: &ServerConfig) -> Result> { + match self { + Self::Filesystem => list_metadata_files(config).await, + Self::Chainfire(store) => store.list().await, + } + } +} + +impl ChainfireMetadataStore { + fn key_for(&self, id: &str) -> String { + format!( + "{}/{}", + self.key_prefix.trim_end_matches('/'), + id.trim_start_matches('/') + ) + } + + fn kv_url(&self, base_url: &str, key: &str) -> String { + format!( + "{}/api/v1/kv/{}", + base_url.trim_end_matches('/'), + key.trim_start_matches('/') + ) + } + + async fn load(&self, id: &str) -> Result> { + let key = self.key_for(id); + let mut last_error: Option = None; + for base_url in &self.base_urls { + let response = match self + .client + .get(self.kv_url(base_url, &key)) + .query(&[("consistency", "leader")]) + .send() + .await + { + Ok(response) => response, + Err(error) => { + last_error = Some(anyhow!( + "failed to read CoronaFS metadata from {}: {error}", + base_url + )); + continue; + } + }; + if response.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(None); + } + let response = match response.error_for_status() { + Ok(response) => response, + Err(error) if retryable_chainfire_status(error.status()) => { + last_error = Some(anyhow!( + "failed to read CoronaFS metadata from {}: {}", + base_url, + error + )); + continue; + } + Err(error) => return Err(error.into()), + }; + let payload = response.json::().await?; + return Ok(Some(serde_json::from_str(&payload.data.value)?)); + } + Err(last_error.unwrap_or_else(|| anyhow!("no ChainFire endpoints configured"))) + } + + async fn save(&self, meta: &VolumeMetadata) -> Result<()> { + let body = serde_json::json!({ + "value": serde_json::to_string(meta)?, + }); + let key = self.key_for(&meta.id); + let mut last_error: Option = None; + for base_url in &self.base_urls { + let response = match self + .client + .put(self.kv_url(base_url, &key)) + .json(&body) + .send() + .await + { + Ok(response) => response, + Err(error) => { + last_error = Some(anyhow!( + "failed to store CoronaFS metadata to {}: {error}", + base_url + )); + continue; + } + }; + match response.error_for_status() { + Ok(_) => return Ok(()), + Err(error) if retryable_chainfire_status(error.status()) => { + last_error = Some(anyhow!( + "failed to store CoronaFS metadata to {}: {}", + base_url, + error + )); + } + Err(error) => return Err(error.into()), + } + } + Err(last_error.unwrap_or_else(|| anyhow!("no ChainFire endpoints configured"))) + } + + async fn delete(&self, id: &str) -> Result<()> { + let key = self.key_for(id); + let mut last_error: Option = None; + for base_url in &self.base_urls { + let response = match self.client.delete(self.kv_url(base_url, &key)).send().await { + Ok(response) => response, + Err(error) => { + last_error = Some(anyhow!( + "failed to delete CoronaFS metadata from {}: {error}", + base_url + )); + continue; + } + }; + if response.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(()); + } + match response.error_for_status() { + Ok(_) => return Ok(()), + Err(error) if retryable_chainfire_status(error.status()) => { + last_error = Some(anyhow!( + "failed to delete CoronaFS metadata from {}: {}", + base_url, + error + )); + } + Err(error) => return Err(error.into()), + } + } + Err(last_error.unwrap_or_else(|| anyhow!("no ChainFire endpoints configured"))) + } + + async fn list(&self) -> Result> { + let mut last_error: Option = None; + for base_url in &self.base_urls { + let response = match self + .client + .get(format!("{}/api/v1/kv", base_url.trim_end_matches('/'))) + .query(&[ + ("prefix", self.key_prefix.as_str()), + ("consistency", "leader"), + ]) + .send() + .await + { + Ok(response) => response, + Err(error) => { + last_error = Some(anyhow!( + "failed to list CoronaFS metadata from {}: {error}", + base_url + )); + continue; + } + }; + let response = match response.error_for_status() { + Ok(response) => response, + Err(error) if retryable_chainfire_status(error.status()) => { + last_error = Some(anyhow!( + "failed to list CoronaFS metadata from {}: {}", + base_url, + error + )); + continue; + } + Err(error) => return Err(error.into()), + }; + let payload = response.json::().await?; + let mut items = Vec::with_capacity(payload.data.items.len()); + for item in payload.data.items { + items.push( + serde_json::from_str::(&item.value).with_context(|| { + format!("failed to decode CoronaFS metadata for key {}", item.key) + })?, + ); + } + return Ok(items); + } + Err(last_error.unwrap_or_else(|| anyhow!("no ChainFire endpoints configured"))) + } +} + +fn parse_chainfire_base_urls(value: &str) -> Result> { + let urls: Vec = value + .split(',') + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(|item| item.trim_end_matches('/').to_string()) + .collect(); + if urls.is_empty() { + return Err(anyhow!("at least one ChainFire API URL must be configured")); + } + Ok(urls) +} + +fn retryable_chainfire_status(status: Option) -> bool { + matches!( + status, + Some(reqwest::StatusCode::BAD_GATEWAY) + | Some(reqwest::StatusCode::SERVICE_UNAVAILABLE) + | Some(reqwest::StatusCode::GATEWAY_TIMEOUT) + ) +} + impl AppState { async fn new(config: ServerConfig) -> Result { prepare_dirs(&config).await?; - let reserved_ports = collect_reserved_ports(&config).await?; + let metadata_store = MetadataStore::from_config(&config)?; + let reserved_ports = collect_reserved_ports(&config, &metadata_store).await?; Ok(Self { config: Arc::new(config), + metadata_store, volume_guards: Arc::new(Mutex::new(HashMap::new())), reserved_ports: Arc::new(Mutex::new(reserved_ports)), }) @@ -99,6 +449,18 @@ impl AppState { .or_insert_with(|| Arc::new(Mutex::new(()))) .clone() } + + async fn load_metadata(&self, id: &str) -> Result> { + self.metadata_store.load(&self.config, id).await + } + + async fn save_metadata(&self, meta: &VolumeMetadata) -> Result<()> { + self.metadata_store.save(&self.config, meta).await + } + + async fn delete_metadata(&self, id: &str) -> Result<()> { + self.metadata_store.delete(&self.config, id).await + } } #[derive(Debug)] @@ -126,6 +488,11 @@ impl ApiError { impl IntoResponse for ApiError { fn into_response(self) -> Response { + if self.status.is_server_error() { + tracing::error!(status = %self.status, error = %self.message, "CoronaFS API request failed"); + } else if self.status == StatusCode::NOT_FOUND { + tracing::debug!(status = %self.status, error = %self.message, "CoronaFS API request returned not found"); + } ( self.status, Json(serde_json::json!({ @@ -162,10 +529,21 @@ async fn main() -> Result<()> { let app = Router::new() .route("/healthz", get(healthz)) - .route("/v1/volumes/{id}", put(create_blank_volume).get(get_volume).delete(delete_volume)) + .route("/v1/capabilities", get(capabilities)) + .route("/v1/volumes", get(list_volumes)) + .route( + "/v1/volumes/{id}", + put(create_blank_volume) + .get(get_volume) + .delete(delete_volume), + ) .route("/v1/volumes/{id}/import", put(import_volume)) + .route("/v1/volumes/{id}/materialize", post(materialize_volume)) .route("/v1/volumes/{id}/resize", post(resize_volume)) - .route("/v1/volumes/{id}/export", post(ensure_export)) + .route( + "/v1/volumes/{id}/export", + post(ensure_export).delete(release_export), + ) .with_state(state); tracing::info!(%listen_addr, "starting CoronaFS server"); @@ -178,14 +556,53 @@ async fn healthz() -> Json { Json(serde_json::json!({"status": "ok"})) } +async fn capabilities(State(state): State) -> Json { + Json(CapabilitiesResponse { + mode: state.config.mode, + supports_controller_api: state.config.supports_controller_api(), + supports_node_api: state.config.supports_node_api(), + }) +} + +fn ensure_controller_api(state: &AppState, operation: &str) -> Result<()> { + if state.config.supports_controller_api() { + return Ok(()); + } + Err(anyhow!( + "{operation} is not available when CoronaFS runs in {:?} mode", + state.config.mode + )) +} + +fn ensure_node_api(state: &AppState, operation: &str) -> Result<()> { + if state.config.supports_node_api() { + return Ok(()); + } + Err(anyhow!( + "{operation} is not available when CoronaFS runs in {:?} mode", + state.config.mode + )) +} + +fn ensure_volume_create_api(state: &AppState, operation: &str) -> Result<()> { + if state.config.supports_controller_api() || state.config.supports_node_api() { + return Ok(()); + } + Err(anyhow!( + "{operation} is not available when CoronaFS runs in {:?} mode", + state.config.mode + )) +} + async fn create_blank_volume( State(state): State, Path(id): Path, Json(req): Json, ) -> ApiResult { + ensure_volume_create_api(&state, "volume create").map_err(ApiError::internal)?; let volume_guard = state.volume_guard(&id).await; let _guard = volume_guard.lock().await; - create_blank_impl(&state, &id, req.size_bytes) + create_blank_impl(&state, &id, req) .await .map(Json) .map_err(ApiError::internal) @@ -197,6 +614,7 @@ async fn import_volume( Query(query): Query, body: Body, ) -> ApiResult { + ensure_controller_api(&state, "volume import").map_err(ApiError::internal)?; let volume_guard = state.volume_guard(&id).await; let _guard = volume_guard.lock().await; import_impl(&state, &id, query.size_bytes, body) @@ -215,11 +633,19 @@ async fn get_volume( .map(Json) } +async fn list_volumes(State(state): State) -> ApiResult { + let items = list_volume_responses(&state) + .await + .map_err(ApiError::internal)?; + Ok(Json(VolumeListResponse { items })) +} + async fn ensure_export( State(state): State, Path(id): Path, Query(query): Query, ) -> ApiResult { + ensure_node_api(&state, "volume export").map_err(ApiError::internal)?; let volume_guard = state.volume_guard(&id).await; let _guard = volume_guard.lock().await; ensure_export_impl(&state, &id, query.read_only.unwrap_or(false)) @@ -228,11 +654,39 @@ async fn ensure_export( .map_err(ApiError::internal) } +async fn release_export( + State(state): State, + Path(id): Path, +) -> Result { + ensure_node_api(&state, "volume export release").map_err(ApiError::internal)?; + let volume_guard = state.volume_guard(&id).await; + let _guard = volume_guard.lock().await; + release_export_impl(&state, &id) + .await + .map(|_| StatusCode::NO_CONTENT) + .map_err(ApiError::internal) +} + +async fn materialize_volume( + State(state): State, + Path(id): Path, + Json(req): Json, +) -> ApiResult { + ensure_node_api(&state, "volume materialize").map_err(ApiError::internal)?; + let volume_guard = state.volume_guard(&id).await; + let _guard = volume_guard.lock().await; + materialize_impl(&state, &id, req) + .await + .map(Json) + .map_err(ApiError::internal) +} + async fn resize_volume( State(state): State, Path(id): Path, Json(req): Json, ) -> ApiResult { + ensure_controller_api(&state, "volume resize").map_err(ApiError::internal)?; let volume_guard = state.volume_guard(&id).await; let _guard = volume_guard.lock().await; resize_impl(&state, &id, req.size_bytes) @@ -257,44 +711,64 @@ async fn prepare_dirs(config: &ServerConfig) -> Result<()> { fs::create_dir_all(config.volume_dir()).await?; fs::create_dir_all(config.metadata_dir()).await?; fs::create_dir_all(config.pid_dir()).await?; + ensure_volume_dir_permissions(&config.volume_dir()).await?; Ok(()) } -async fn create_blank_impl(state: &AppState, id: &str, size_bytes: u64) -> Result { +async fn create_blank_impl( + state: &AppState, + id: &str, + req: CreateVolumeRequest, +) -> Result { let path = volume_path(&state.config, id); - let meta_path = metadata_path(&state.config, id); - if fs::try_exists(&meta_path).await.unwrap_or(false) { + if state.load_metadata(id).await?.is_some() { return load_response_required(state, id).await; } - if state.config.preallocate { - let status = Command::new("fallocate") - .args(["-l", &size_bytes.to_string(), path.to_string_lossy().as_ref()]) - .status() - .await; - match status { - Ok(status) if status.success() => {} - _ => { - let file = fs::File::create(&path).await?; - file.set_len(size_bytes).await?; + let format = req.format.unwrap_or_default(); + let backing_file = req.backing_file.clone(); + let backing_format = req.backing_format.unwrap_or(VolumeFileFormat::Raw); + let temp_path = temp_create_path(&state.config, id); + if fs::try_exists(&temp_path).await.unwrap_or(false) { + let _ = fs::remove_file(&temp_path).await; + } + + match format { + VolumeFileFormat::Raw => { + if backing_file.is_some() { + return Err(anyhow!("raw CoronaFS volumes do not support backing files")); } + create_or_preallocate_file(&temp_path, req.size_bytes, state.config.preallocate) + .await?; + } + VolumeFileFormat::Qcow2 => { + create_qcow2_volume_file( + &state.config, + &temp_path, + req.size_bytes, + backing_file.as_deref(), + backing_format, + ) + .await?; } - } else { - let file = fs::File::create(&path).await?; - file.set_len(size_bytes).await?; } let meta = VolumeMetadata { id: id.to_string(), - size_bytes, + size_bytes: req.size_bytes, + format, + node_local: matches!(state.config.mode, ServerMode::Node) || backing_file.is_some(), + materialized_from: backing_file, port: None, export_pid: None, + export_read_only: None, created_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(), }; + fs::rename(&temp_path, &path).await?; ensure_volume_file_permissions(&path).await?; - save_metadata(&meta_path, &meta).await?; - load_response_required(state, id).await + state.save_metadata(&meta).await?; + Ok(response_from_metadata(state, meta)) } async fn import_impl( @@ -304,7 +778,6 @@ async fn import_impl( body: Body, ) -> Result { let path = volume_path(&state.config, id); - let meta_path = metadata_path(&state.config, id); let tmp_path = temp_import_path(&state.config, id); if let Some(size_bytes) = size_bytes { create_or_preallocate_file(&tmp_path, size_bytes, state.config.preallocate).await?; @@ -338,25 +811,29 @@ async fn import_impl( let meta = VolumeMetadata { id: id.to_string(), size_bytes: size_bytes.unwrap_or(actual_size), + format: VolumeFileFormat::Raw, + node_local: false, + materialized_from: None, port: None, export_pid: None, + export_read_only: None, created_at: chrono::Utc::now().to_rfc3339(), updated_at: chrono::Utc::now().to_rfc3339(), }; - save_metadata(&meta_path, &meta).await?; + state.save_metadata(&meta).await?; tracing::info!( volume_id = id, bytes_written, volume_size = actual_size, "Imported raw volume into CoronaFS" ); - load_response_required(state, id).await + Ok(response_from_metadata(state, meta)) } async fn resize_impl(state: &AppState, id: &str, size_bytes: u64) -> Result { - let meta_path = metadata_path(&state.config, id); let path = volume_path(&state.config, id); - let mut meta = load_metadata(&meta_path) + let mut meta = state + .load_metadata(id) .await? .ok_or_else(|| anyhow!("volume {id} not found"))?; @@ -368,7 +845,7 @@ async fn resize_impl(state: &AppState, id: &str, size_bytes: u64) -> Result Result Result { + let path = volume_path(&state.config, id); + if state.load_metadata(id).await?.is_some() { + return load_response_required(state, id).await; + } + + let format = req.format.unwrap_or_else(|| { + if req.lazy { + VolumeFileFormat::Qcow2 + } else { + VolumeFileFormat::Raw + } + }); + let temp_path = temp_create_path(&state.config, id); + if fs::try_exists(&temp_path).await.unwrap_or(false) { + let _ = fs::remove_file(&temp_path).await; + } + + if req.lazy { + if format != VolumeFileFormat::Qcow2 { + return Err(anyhow!( + "lazy CoronaFS materialization for volume {id} requires qcow2 output format" + )); + } + let size_bytes = req.size_bytes.ok_or_else(|| { + anyhow!("lazy CoronaFS materialization for volume {id} requires size_bytes") + })?; + create_qcow2_volume_file( + &state.config, + &temp_path, + size_bytes, + Some(req.source_uri.as_str()), + VolumeFileFormat::Raw, + ) + .await + .with_context(|| { + format!( + "failed to create lazy CoronaFS materialization for volume {id} from {}", + req.source_uri + ) + })?; + } else { + let status = Command::new(&state.config.qemu_img_path) + .args([ + "convert", + "-t", + "none", + "-T", + "none", + "-O", + format.as_qemu_arg(), + req.source_uri.as_str(), + temp_path.to_string_lossy().as_ref(), + ]) + .status() + .await + .context("failed to spawn qemu-img convert for CoronaFS materialization")?; + if !status.success() { + return Err(anyhow!( + "qemu-img convert failed while materializing volume {id} from {}", + req.source_uri + )); + } + } + + if let Some(size_bytes) = req.size_bytes { + let resize = Command::new(&state.config.qemu_img_path) + .args([ + "resize", + "-f", + format.as_qemu_arg(), + temp_path.to_string_lossy().as_ref(), + &size_bytes.to_string(), + ]) + .status() + .await + .context("failed to resize materialized CoronaFS volume")?; + if !resize.success() { + return Err(anyhow!( + "qemu-img resize failed while materializing volume {id}" + )); + } + } + + fs::rename(&temp_path, &path).await?; + ensure_volume_file_permissions(&path).await?; + let actual_size = fs::metadata(&path).await?.len(); + let meta = VolumeMetadata { + id: id.to_string(), + size_bytes: req.size_bytes.unwrap_or(actual_size), + format, + node_local: true, + materialized_from: Some(req.source_uri.clone()), + port: None, + export_pid: None, + export_read_only: None, + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + }; + state.save_metadata(&meta).await?; + tracing::info!( + volume_id = id, + source_uri = req.source_uri, + size_bytes = meta.size_bytes, + format = meta.format.as_qemu_arg(), + "Materialized CoronaFS volume from remote export" + ); + Ok(response_from_metadata(state, meta)) } async fn ensure_export_impl(state: &AppState, id: &str, read_only: bool) -> Result { - let meta_path = metadata_path(&state.config, id); - let mut meta = load_metadata(&meta_path) + let mut meta = state + .load_metadata(id) .await? .ok_or_else(|| anyhow!("volume {id} not found"))?; + let mut preferred_port = meta.port; if let Some(pid) = meta.export_pid { - if process_running(pid).await { + if process_running(pid).await + && meta.port.is_some() + && meta.export_read_only == Some(read_only) + { if let Some(port) = meta.port { mark_port_reserved(state, port).await; } - return load_response_required(state, id).await; + return Ok(response_from_metadata(state, meta)); } + stop_export_if_running(&state.config, id, &mut meta).await?; + release_export_port(state, meta.port).await; + meta.port = None; } - let port = reserve_export_port(state, meta.port).await?; + let port = reserve_export_port(state, preferred_port.take()).await?; let pid_path = pid_path(&state.config, id); let path = volume_path(&state.config, id); - let effective_aio_mode = export_aio_mode(&state.config.export_cache_mode, &state.config.export_aio_mode); + let effective_aio_mode = export_aio_mode( + &state.config.export_cache_mode, + &state.config.export_aio_mode, + ); let mut command = Command::new(&state.config.qemu_nbd_path); command.args([ "--fork", @@ -418,7 +1019,7 @@ async fn ensure_export_impl(state: &AppState, id: &str, read_only: bool) -> Resu "--detect-zeroes", &state.config.export_detect_zeroes_mode, "--format", - "raw", + meta.format.as_qemu_arg(), "--bind", &state.config.export_bind_addr, "--port", @@ -428,13 +1029,12 @@ async fn ensure_export_impl(state: &AppState, id: &str, read_only: bool) -> Resu command.arg("--read-only"); } command.arg(path.to_string_lossy().as_ref()); - let status = command - .status() - .await - .context("failed to spawn qemu-nbd")?; + let status = command.status().await.context("failed to spawn qemu-nbd")?; if !status.success() { release_export_port(state, Some(port)).await; - return Err(anyhow!("qemu-nbd failed to export volume {id} on port {port}")); + return Err(anyhow!( + "qemu-nbd failed to export volume {id} on port {port}" + )); } let pid = match read_pid_file(&pid_path).await { Ok(pid) => pid, @@ -445,14 +1045,27 @@ async fn ensure_export_impl(state: &AppState, id: &str, read_only: bool) -> Resu }; meta.port = Some(port); meta.export_pid = Some(pid); + meta.export_read_only = Some(read_only); meta.updated_at = chrono::Utc::now().to_rfc3339(); - save_metadata(&meta_path, &meta).await?; + state.save_metadata(&meta).await?; if let Err(err) = wait_for_tcp_listen(export_probe_host(&state.config), port).await { let _ = stop_export_if_running(&state.config, id, &mut meta).await; release_export_port(state, Some(port)).await; return Err(err); } - load_response_required(state, id).await + Ok(response_from_metadata(state, meta)) +} + +async fn release_export_impl(state: &AppState, id: &str) -> Result<()> { + let Some(mut meta) = state.load_metadata(id).await? else { + return Ok(()); + }; + let reserved_port = meta.port; + stop_export_if_running(&state.config, id, &mut meta).await?; + release_export_port(state, reserved_port).await; + meta.updated_at = chrono::Utc::now().to_rfc3339(); + state.save_metadata(&meta).await?; + Ok(()) } fn export_aio_mode<'a>(cache_mode: &str, aio_mode: &'a str) -> &'a str { @@ -470,20 +1083,19 @@ fn export_aio_mode<'a>(cache_mode: &str, aio_mode: &'a str) -> &'a str { } async fn delete_impl(state: &AppState, id: &str) -> Result<()> { - let meta_path = metadata_path(&state.config, id); - if let Some(mut meta) = load_metadata(&meta_path).await? { + let pid_path = pid_path(&state.config, id); + if let Some(mut meta) = state.load_metadata(id).await? { let reserved_port = meta.port; stop_export_if_running(&state.config, id, &mut meta).await?; release_export_port(state, reserved_port).await; + } else { + stop_export_for_pid_file(&pid_path).await?; } let path = volume_path(&state.config, id); if fs::try_exists(&path).await.unwrap_or(false) { fs::remove_file(&path).await?; } - if fs::try_exists(&meta_path).await.unwrap_or(false) { - fs::remove_file(&meta_path).await?; - } - let pid_path = pid_path(&state.config, id); + state.delete_metadata(id).await?; if fs::try_exists(&pid_path).await.unwrap_or(false) { fs::remove_file(pid_path).await?; } @@ -497,27 +1109,48 @@ async fn load_response(state: &AppState, id: &str) -> Option { } } -async fn load_response_required(state: &AppState, id: &str) -> Result { - let meta = load_metadata(&metadata_path(&state.config, id)) - .await? - .ok_or_else(|| anyhow!("volume {id} not found"))?; - let export = match (meta.port, meta.export_pid) { - (Some(port), pid) if pid.map(process_running_sync).unwrap_or(false) => Some(ExportResponse { - uri: format!("nbd://{}:{}", state.config.advertise_host, port), - port, - pid, - }), - _ => None, - }; - Ok(VolumeResponse { - id: meta.id, - size_bytes: meta.size_bytes, - path: volume_path(&state.config, id).display().to_string(), - export, - }) +async fn list_volume_responses(state: &AppState) -> Result> { + let mut items = Vec::new(); + for meta in state.metadata_store.list(&state.config).await? { + items.push(response_from_metadata(state, meta)); + } + items.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id)); + Ok(items) } -async fn load_metadata(path: &FsPath) -> Result> { +async fn load_response_required(state: &AppState, id: &str) -> Result { + let meta = state + .load_metadata(id) + .await? + .ok_or_else(|| anyhow!("volume {id} not found"))?; + Ok(response_from_metadata(state, meta)) +} + +fn response_from_metadata(state: &AppState, meta: VolumeMetadata) -> VolumeResponse { + let id = meta.id; + let export = match (meta.port, meta.export_pid) { + (Some(port), pid) if pid.map(process_running_sync).unwrap_or(false) => { + Some(ExportResponse { + uri: format!("nbd://{}:{}", state.config.advertise_host, port), + port, + pid, + read_only: meta.export_read_only.unwrap_or(false), + }) + } + _ => None, + }; + VolumeResponse { + id: id.clone(), + size_bytes: meta.size_bytes, + format: meta.format.as_qemu_arg().to_string(), + node_local: meta.node_local, + materialized_from: meta.materialized_from, + path: volume_path(&state.config, &id).display().to_string(), + export, + } +} + +async fn load_metadata_file(path: &FsPath) -> Result> { if !fs::try_exists(path).await.unwrap_or(false) { return Ok(None); } @@ -525,7 +1158,7 @@ async fn load_metadata(path: &FsPath) -> Result> { Ok(Some(serde_json::from_slice(&bytes)?)) } -async fn save_metadata(path: &FsPath, meta: &VolumeMetadata) -> Result<()> { +async fn save_metadata_file(path: &FsPath, meta: &VolumeMetadata) -> Result<()> { let bytes = serde_json::to_vec_pretty(meta)?; let tmp_path = path.with_extension("json.tmp"); fs::write(&tmp_path, bytes).await?; @@ -533,40 +1166,85 @@ async fn save_metadata(path: &FsPath, meta: &VolumeMetadata) -> Result<()> { Ok(()) } -async fn stop_export_if_running(config: &ServerConfig, id: &str, meta: &mut VolumeMetadata) -> Result<()> { - if let Some(pid) = meta.export_pid { - if process_running(pid).await { - let status = Command::new("kill") - .args(["-TERM", &pid.to_string()]) - .status() - .await - .context("failed to terminate qemu-nbd export")?; - if !status.success() { - return Err(anyhow!("failed to stop qemu-nbd export pid {pid}")); - } - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); - while process_running(pid).await { - if std::time::Instant::now() >= deadline { - let _ = Command::new("kill") - .args(["-KILL", &pid.to_string()]) - .status() - .await; - break; - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - } +async fn delete_metadata_file(path: &FsPath) -> Result<()> { + if fs::try_exists(path).await.unwrap_or(false) { + fs::remove_file(path).await?; + } + Ok(()) +} + +async fn list_metadata_files(config: &ServerConfig) -> Result> { + let mut items = Vec::new(); + let mut entries = fs::read_dir(config.metadata_dir()).await?; + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + let Some(meta) = load_metadata_file(&path).await? else { + continue; + }; + items.push(meta); + } + Ok(items) +} + +async fn stop_export_if_running( + config: &ServerConfig, + id: &str, + meta: &mut VolumeMetadata, +) -> Result<()> { + let pid_path = pid_path(config, id); + if let Some(pid) = meta + .export_pid + .or(read_pid_file_if_present(&pid_path).await?) + { + stop_export_process(pid).await?; } meta.export_pid = None; - let pid_path = pid_path(config, id); + meta.export_read_only = None; if fs::try_exists(&pid_path).await.unwrap_or(false) { fs::remove_file(pid_path).await?; } Ok(()) } +async fn stop_export_for_pid_file(pid_path: &FsPath) -> Result<()> { + if let Some(pid) = read_pid_file_if_present(pid_path).await? { + stop_export_process(pid).await?; + } + if fs::try_exists(pid_path).await.unwrap_or(false) { + fs::remove_file(pid_path).await?; + } + Ok(()) +} + +async fn stop_export_process(pid: u32) -> Result<()> { + if process_running(pid).await { + let status = Command::new("kill") + .args(["-TERM", &pid.to_string()]) + .status() + .await + .context("failed to terminate qemu-nbd export")?; + if !status.success() { + return Err(anyhow!("failed to stop qemu-nbd export pid {pid}")); + } + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + while process_running(pid).await { + if std::time::Instant::now() >= deadline { + let _ = Command::new("kill") + .args(["-KILL", &pid.to_string()]) + .status() + .await; + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + } + Ok(()) +} + async fn process_running(pid: u32) -> bool { - fs::try_exists(format!("/proc/{pid}")).await.unwrap_or(false) + fs::try_exists(format!("/proc/{pid}")) + .await + .unwrap_or(false) } fn process_running_sync(pid: u32) -> bool { @@ -590,6 +1268,24 @@ async fn read_pid_file(path: &FsPath) -> Result { } } +async fn read_pid_file_if_present(path: &FsPath) -> Result> { + if !fs::try_exists(path).await.unwrap_or(false) { + return Ok(None); + } + match read_pid_file(path).await { + Ok(pid) => Ok(Some(pid)), + Err(error) => { + tracing::warn!( + pid_file = %path.display(), + error = %error, + "failed to read CoronaFS export pid file; removing stale pid file" + ); + let _ = fs::remove_file(path).await; + Ok(None) + } + } +} + async fn wait_for_tcp_listen(host: &str, port: u16) -> Result<()> { let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); loop { @@ -603,14 +1299,12 @@ async fn wait_for_tcp_listen(host: &str, port: u16) -> Result<()> { } } -async fn collect_reserved_ports(config: &ServerConfig) -> Result> { +async fn collect_reserved_ports( + config: &ServerConfig, + metadata_store: &MetadataStore, +) -> Result> { let mut reserved = HashSet::new(); - let mut entries = fs::read_dir(config.metadata_dir()).await?; - while let Some(entry) = entries.next_entry().await? { - let path = entry.path(); - let Some(mut meta) = load_metadata(&path).await? else { - continue; - }; + for mut meta in metadata_store.list(config).await? { match (meta.port, meta.export_pid) { (Some(port), Some(pid)) if process_running(pid).await => { reserved.insert(port); @@ -618,12 +1312,32 @@ async fn collect_reserved_ports(config: &ServerConfig) -> Result> { (Some(_), _) | (_, Some(_)) => { meta.port = None; meta.export_pid = None; + meta.export_read_only = None; meta.updated_at = chrono::Utc::now().to_rfc3339(); - save_metadata(&path, &meta).await?; + metadata_store.save(config, &meta).await?; } _ => {} } } + + let mut pid_entries = fs::read_dir(config.pid_dir()).await?; + while let Some(entry) = pid_entries.next_entry().await? { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("pid") { + continue; + } + let Some(id) = path.file_stem().and_then(|stem| stem.to_str()) else { + continue; + }; + if fs::try_exists(&metadata_path(config, id)) + .await + .unwrap_or(false) + { + continue; + } + stop_export_for_pid_file(&path).await?; + } + Ok(reserved) } @@ -683,10 +1397,18 @@ fn export_probe_host(config: &ServerConfig) -> &str { } } -async fn create_or_preallocate_file(path: &FsPath, size_bytes: u64, preallocate: bool) -> Result<()> { +async fn create_or_preallocate_file( + path: &FsPath, + size_bytes: u64, + preallocate: bool, +) -> Result<()> { if preallocate { let status = Command::new("fallocate") - .args(["-l", &size_bytes.to_string(), path.to_string_lossy().as_ref()]) + .args([ + "-l", + &size_bytes.to_string(), + path.to_string_lossy().as_ref(), + ]) .status() .await; if matches!(status, Ok(status) if status.success()) { @@ -709,6 +1431,17 @@ async fn ensure_volume_file_permissions(path: &FsPath) -> Result<()> { Ok(()) } +async fn ensure_volume_dir_permissions(path: &FsPath) -> Result<()> { + #[cfg(unix)] + { + // Keep CoronaFS volumes group-writable and setgid so PlasmaVMC clones + // inherit the shared `coronafs` group when they populate volumes locally. + let permissions = std::fs::Permissions::from_mode(0o2770); + fs::set_permissions(path, permissions).await?; + } + Ok(()) +} + fn volume_path(config: &ServerConfig, id: &str) -> PathBuf { config.volume_dir().join(format!("{id}.raw")) } @@ -725,9 +1458,54 @@ fn temp_import_path(config: &ServerConfig, id: &str) -> PathBuf { config.data_dir.join(format!("{id}.import.tmp")) } +fn temp_create_path(config: &ServerConfig, id: &str) -> PathBuf { + config.data_dir.join(format!("{id}.create.tmp")) +} + +async fn create_qcow2_volume_file( + config: &ServerConfig, + path: &FsPath, + size_bytes: u64, + backing_file: Option<&str>, + backing_format: VolumeFileFormat, +) -> Result<()> { + let mut command = Command::new(&config.qemu_img_path); + command.args(["create", "-f", "qcow2"]); + if let Some(backing_file) = backing_file { + command.args(["-F", backing_format.as_qemu_arg(), "-b", backing_file]); + } + command.args([path.to_string_lossy().as_ref(), &size_bytes.to_string()]); + let status = command + .status() + .await + .context("failed to spawn qemu-img create")?; + if !status.success() { + return Err(anyhow!( + "qemu-img create failed for {} with status {status}", + path.display() + )); + } + ensure_volume_file_permissions(path).await?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; + use axum::{ + extract::{Path as AxumPath, Query as AxumQuery, State as AxumState}, + routing::get as axum_get, + Json as AxumJson, Router as AxumRouter, + }; + use serde::Deserialize; + use std::sync::{ + atomic::{AtomicU16, Ordering}, + Arc, + }; + use tempfile::tempdir; + use tokio::sync::Mutex as TokioMutex; + + static NEXT_TEST_EXPORT_BASE_PORT: AtomicU16 = AtomicU16::new(18000); #[test] fn export_aio_mode_falls_back_for_cached_exports() { @@ -745,4 +1523,766 @@ mod tests { config.export_bind_addr = "10.100.0.11".to_string(); assert_eq!(export_probe_host(&config), "10.100.0.11"); } + + #[test] + fn server_mode_capabilities_match_intended_split() { + let mut config = ServerConfig::default(); + assert!(config.supports_controller_api()); + assert!(config.supports_node_api()); + + config.mode = ServerMode::Controller; + assert!(config.supports_controller_api()); + assert!(!config.supports_node_api()); + + config.mode = ServerMode::Node; + assert!(!config.supports_controller_api()); + assert!(config.supports_node_api()); + } + + fn test_config() -> (tempfile::TempDir, ServerConfig) { + let tempdir = tempdir().unwrap(); + let mut config = ServerConfig::default(); + config.data_dir = tempdir.path().join("coronafs"); + config.preallocate = false; + config.export_port_count = 4; + config.export_base_port = find_free_export_base_port(config.export_port_count); + (tempdir, config) + } + + fn find_free_export_base_port(port_count: u16) -> u16 { + let step = port_count.max(1); + for _ in 0..4096 { + let start = NEXT_TEST_EXPORT_BASE_PORT.fetch_add(step, Ordering::Relaxed); + let Some(end_port) = start.checked_add(step.saturating_sub(1)) else { + continue; + }; + if end_port >= 24000 { + continue; + } + let mut listeners = Vec::with_capacity(port_count as usize); + let mut available = true; + for offset in 0..port_count { + match std::net::TcpListener::bind(("127.0.0.1", start + offset)) { + Ok(listener) => listeners.push(listener), + Err(_) => { + available = false; + break; + } + } + } + if available { + drop(listeners); + return start; + } + } + panic!("failed to find a free CoronaFS export port range for tests"); + } + + #[tokio::test] + async fn collect_reserved_ports_clears_stale_exports() { + let (_tempdir, config) = test_config(); + prepare_dirs(&config).await.unwrap(); + + let meta_path = metadata_path(&config, "stale"); + save_metadata_file( + &meta_path, + &VolumeMetadata { + id: "stale".to_string(), + size_bytes: 4096, + format: VolumeFileFormat::Raw, + node_local: false, + materialized_from: None, + port: Some(config.export_base_port), + export_pid: Some(u32::MAX), + export_read_only: Some(false), + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + }, + ) + .await + .unwrap(); + + let reserved = collect_reserved_ports(&config, &MetadataStore::Filesystem) + .await + .unwrap(); + assert!(reserved.is_empty()); + + let meta = load_metadata_file(&meta_path).await.unwrap().unwrap(); + assert_eq!(meta.port, None); + assert_eq!(meta.export_pid, None); + } + + #[tokio::test] + async fn collect_reserved_ports_stops_orphan_pid_files_without_metadata() { + let (_tempdir, config) = test_config(); + prepare_dirs(&config).await.unwrap(); + + let mut child = Command::new("sleep").arg("60").spawn().unwrap(); + let pid = child.id().unwrap(); + let orphan_pid_path = pid_path(&config, "orphan"); + fs::write(&orphan_pid_path, format!("{pid}\n")) + .await + .unwrap(); + + let reserved = collect_reserved_ports(&config, &MetadataStore::Filesystem) + .await + .unwrap(); + assert!(reserved.is_empty()); + assert!(!fs::try_exists(&orphan_pid_path).await.unwrap()); + + tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()) + .await + .expect("timed out waiting for orphan qemu-nbd placeholder process to exit") + .unwrap(); + assert!(!process_running(pid).await); + } + + #[tokio::test] + async fn reserve_export_port_prefers_requested_slot() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config).await.unwrap(); + + let preferred = state.config.export_base_port + 1; + let port = reserve_export_port(&state, Some(preferred)).await.unwrap(); + assert_eq!(port, preferred); + + let next = reserve_export_port(&state, Some(preferred)).await.unwrap(); + assert_ne!(next, preferred); + + release_export_port(&state, Some(port)).await; + let reused = reserve_export_port(&state, Some(preferred)).await.unwrap(); + assert_eq!(reused, preferred); + } + + #[tokio::test] + async fn import_impl_writes_volume_and_metadata() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config).await.unwrap(); + + let response = import_impl(&state, "vol-1", None, Body::from("volume-data")) + .await + .unwrap(); + assert_eq!(response.size_bytes, 11); + assert!(!response.node_local); + assert!(response.materialized_from.is_none()); + + let on_disk = fs::read(volume_path(&state.config, "vol-1")).await.unwrap(); + assert_eq!(on_disk, b"volume-data"); + + let meta = load_metadata_file(&metadata_path(&state.config, "vol-1")) + .await + .unwrap() + .unwrap(); + assert_eq!(meta.size_bytes, 11); + assert_eq!(meta.format, VolumeFileFormat::Raw); + assert!(!meta.node_local); + assert!(meta.materialized_from.is_none()); + assert_eq!(meta.port, None); + assert_eq!(meta.export_pid, None); + assert_eq!(meta.export_read_only, None); + assert_eq!(response.format, "raw"); + } + + #[tokio::test] + async fn create_blank_impl_supports_qcow2_backing_files() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config.clone()).await.unwrap(); + let backing = config.data_dir.join("backing.raw"); + create_or_preallocate_file(&backing, 8 * 1024 * 1024, false) + .await + .unwrap(); + + let response = create_blank_impl( + &state, + "overlay", + CreateVolumeRequest { + size_bytes: 16 * 1024 * 1024, + format: Some(VolumeFileFormat::Qcow2), + backing_file: Some(backing.display().to_string()), + backing_format: Some(VolumeFileFormat::Raw), + }, + ) + .await + .unwrap(); + + assert_eq!(response.format, "qcow2"); + assert!(response.node_local); + assert_eq!( + response.materialized_from.as_deref(), + Some(backing.to_string_lossy().as_ref()) + ); + let meta = load_metadata_file(&metadata_path(&state.config, "overlay")) + .await + .unwrap() + .unwrap(); + assert_eq!(meta.format, VolumeFileFormat::Qcow2); + assert!(meta.node_local); + assert_eq!( + meta.materialized_from.as_deref(), + Some(backing.to_string_lossy().as_ref()) + ); + + let info = Command::new(&config.qemu_img_path) + .args([ + "info", + "--output", + "json", + volume_path(&state.config, "overlay") + .to_string_lossy() + .as_ref(), + ]) + .output() + .await + .unwrap(); + assert!(info.status.success()); + let info: serde_json::Value = serde_json::from_slice(&info.stdout).unwrap(); + assert_eq!(info["format"], "qcow2"); + } + + #[tokio::test] + async fn list_volume_responses_returns_sorted_items() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config).await.unwrap(); + + create_blank_impl( + &state, + "vol-b", + CreateVolumeRequest { + size_bytes: 2 * 1024 * 1024, + format: Some(VolumeFileFormat::Raw), + backing_file: None, + backing_format: None, + }, + ) + .await + .unwrap(); + create_blank_impl( + &state, + "vol-a", + CreateVolumeRequest { + size_bytes: 1 * 1024 * 1024, + format: Some(VolumeFileFormat::Raw), + backing_file: None, + backing_format: None, + }, + ) + .await + .unwrap(); + + let items = list_volume_responses(&state).await.unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(items[0].id, "vol-a"); + assert_eq!(items[1].id, "vol-b"); + assert_eq!(items[0].format, "raw"); + assert!(items.iter().all(|item| !item.node_local)); + assert!(items.iter().all(|item| item.materialized_from.is_none())); + assert!(items.iter().all(|item| item.export.is_none())); + } + + #[tokio::test] + async fn materialize_impl_marks_node_local_origin() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config.clone()).await.unwrap(); + let source_path = config.data_dir.join("source.raw"); + create_or_preallocate_file(&source_path, 4 * 1024 * 1024, false) + .await + .unwrap(); + + let response = materialize_impl( + &state, + "materialized", + MaterializeVolumeRequest { + source_uri: source_path.display().to_string(), + size_bytes: Some(4 * 1024 * 1024), + format: Some(VolumeFileFormat::Raw), + lazy: false, + }, + ) + .await + .unwrap(); + + assert!(response.node_local); + assert_eq!( + response.materialized_from.as_deref(), + Some(source_path.to_string_lossy().as_ref()) + ); + + let meta = load_metadata_file(&metadata_path(&state.config, "materialized")) + .await + .unwrap() + .unwrap(); + assert!(meta.node_local); + assert_eq!( + meta.materialized_from.as_deref(), + Some(source_path.to_string_lossy().as_ref()) + ); + assert_eq!(meta.export_read_only, None); + } + + #[tokio::test] + async fn materialize_impl_supports_lazy_qcow2_overlay() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config.clone()).await.unwrap(); + let source_path = config.data_dir.join("source.raw"); + create_or_preallocate_file(&source_path, 4 * 1024 * 1024, false) + .await + .unwrap(); + + let response = materialize_impl( + &state, + "lazy-materialized", + MaterializeVolumeRequest { + source_uri: source_path.display().to_string(), + size_bytes: Some(8 * 1024 * 1024), + format: Some(VolumeFileFormat::Qcow2), + lazy: true, + }, + ) + .await + .unwrap(); + + assert_eq!(response.format, "qcow2"); + assert!(response.node_local); + assert_eq!( + response.materialized_from.as_deref(), + Some(source_path.to_string_lossy().as_ref()) + ); + + let info = Command::new(&config.qemu_img_path) + .args([ + "info", + "--output", + "json", + volume_path(&state.config, "lazy-materialized") + .to_string_lossy() + .as_ref(), + ]) + .output() + .await + .unwrap(); + assert!(info.status.success()); + let info: serde_json::Value = serde_json::from_slice(&info.stdout).unwrap(); + assert_eq!(info["format"], "qcow2"); + assert_eq!(info["backing-filename"], source_path.display().to_string()); + assert_eq!(info["virtual-size"], 8 * 1024 * 1024); + } + + #[tokio::test] + async fn response_from_metadata_preserves_export_mode() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config).await.unwrap(); + let mut child = Command::new("sleep").arg("60").spawn().unwrap(); + let pid = child.id().unwrap(); + + let response = response_from_metadata( + &state, + VolumeMetadata { + id: "export-mode".to_string(), + size_bytes: 4096, + format: VolumeFileFormat::Raw, + node_local: true, + materialized_from: Some("nbd://127.0.0.1:11000".to_string()), + port: Some(state.config.export_base_port), + export_pid: Some(pid), + export_read_only: Some(true), + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + }, + ); + + assert!(response.node_local); + assert_eq!( + response.materialized_from.as_deref(), + Some("nbd://127.0.0.1:11000") + ); + let export = response.export.expect("expected export"); + assert!(export.read_only); + + let _ = child.kill().await; + let _ = child.wait().await; + } + + #[cfg(unix)] + #[tokio::test] + async fn prepare_dirs_makes_volume_dir_group_writable_and_setgid() { + let (_tempdir, config) = test_config(); + prepare_dirs(&config).await.unwrap(); + + let mode = std::fs::metadata(config.volume_dir()) + .unwrap() + .permissions() + .mode() + & 0o7777; + assert_eq!(mode, 0o2770); + } + + #[tokio::test] + async fn delete_impl_releases_reserved_port_and_removes_files() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config).await.unwrap(); + let port = state.config.export_base_port + 2; + + import_impl(&state, "vol-delete", None, Body::from("delete-me")) + .await + .unwrap(); + mark_port_reserved(&state, port).await; + + let meta_path = metadata_path(&state.config, "vol-delete"); + save_metadata_file( + &meta_path, + &VolumeMetadata { + id: "vol-delete".to_string(), + size_bytes: 9, + format: VolumeFileFormat::Raw, + node_local: false, + materialized_from: None, + port: Some(port), + export_pid: None, + export_read_only: None, + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + }, + ) + .await + .unwrap(); + fs::write(pid_path(&state.config, "vol-delete"), b"1234") + .await + .unwrap(); + + delete_impl(&state, "vol-delete").await.unwrap(); + + assert!(!fs::try_exists(volume_path(&state.config, "vol-delete")) + .await + .unwrap()); + assert!(!fs::try_exists(meta_path).await.unwrap()); + assert!(!fs::try_exists(pid_path(&state.config, "vol-delete")) + .await + .unwrap()); + + let reused = reserve_export_port(&state, Some(port)).await.unwrap(); + assert_eq!(reused, port); + } + + #[tokio::test] + async fn release_export_impl_stops_export_and_frees_port() { + let (_tempdir, config) = test_config(); + let state = AppState::new(config).await.unwrap(); + let port = state.config.export_base_port + 2; + + import_impl(&state, "vol-release", None, Body::from("release-me")) + .await + .unwrap(); + mark_port_reserved(&state, port).await; + + let mut child = Command::new("sleep").arg("60").spawn().unwrap(); + let pid = child.id().unwrap(); + let meta_path = metadata_path(&state.config, "vol-release"); + save_metadata_file( + &meta_path, + &VolumeMetadata { + id: "vol-release".to_string(), + size_bytes: 10, + format: VolumeFileFormat::Raw, + node_local: true, + materialized_from: Some("nbd://127.0.0.1:18000".to_string()), + port: Some(port), + export_pid: Some(pid), + export_read_only: Some(false), + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + }, + ) + .await + .unwrap(); + fs::write(pid_path(&state.config, "vol-release"), format!("{pid}\n")) + .await + .unwrap(); + + release_export_impl(&state, "vol-release").await.unwrap(); + + let meta = load_metadata_file(&meta_path).await.unwrap().unwrap(); + assert_eq!(meta.port, Some(port)); + assert_eq!(meta.export_pid, None); + assert_eq!(meta.export_read_only, None); + assert!(!fs::try_exists(pid_path(&state.config, "vol-release")) + .await + .unwrap()); + + tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()) + .await + .expect("timed out waiting for released export placeholder process to exit") + .unwrap(); + assert!(!process_running(pid).await); + + let reused = reserve_export_port(&state, Some(port)).await.unwrap(); + assert_eq!(reused, port); + } + + #[derive(Clone)] + struct MockChainfireState { + values: Arc>>, + stale_once_keys: Arc>>, + } + + #[derive(Deserialize)] + struct MockPutRequest { + value: String, + } + + #[derive(Deserialize)] + struct MockPrefixQuery { + prefix: Option, + } + + async fn mock_chainfire_get( + AxumState(state): AxumState, + AxumPath(key): AxumPath, + ) -> Result, StatusCode> { + let full_key = if key.contains('/') { + format!("/{}", key) + } else { + key + }; + let mut stale_once = state.stale_once_keys.lock().await; + if stale_once.remove(&full_key) { + return Err(StatusCode::NOT_FOUND); + } + drop(stale_once); + let values = state.values.lock().await; + let Some(value) = values.get(&full_key).cloned() else { + return Err(StatusCode::NOT_FOUND); + }; + Ok(AxumJson(serde_json::json!({ + "data": { + "key": full_key, + "value": value, + }, + "meta": { + "request_id": "test", + "timestamp": chrono::Utc::now().to_rfc3339(), + } + }))) + } + + async fn mock_chainfire_put( + AxumState(state): AxumState, + AxumPath(key): AxumPath, + AxumJson(req): AxumJson, + ) -> (StatusCode, AxumJson) { + let full_key = if key.contains('/') { + format!("/{}", key) + } else { + key + }; + state + .values + .lock() + .await + .insert(full_key.clone(), req.value); + ( + StatusCode::OK, + AxumJson(serde_json::json!({ + "data": { + "key": full_key, + "success": true, + }, + "meta": { + "request_id": "test", + "timestamp": chrono::Utc::now().to_rfc3339(), + } + })), + ) + } + + async fn mock_chainfire_delete( + AxumState(state): AxumState, + AxumPath(key): AxumPath, + ) -> (StatusCode, AxumJson) { + let full_key = if key.contains('/') { + format!("/{}", key) + } else { + key + }; + state.values.lock().await.remove(&full_key); + ( + StatusCode::OK, + AxumJson(serde_json::json!({ + "data": { + "key": full_key, + "success": true, + }, + "meta": { + "request_id": "test", + "timestamp": chrono::Utc::now().to_rfc3339(), + } + })), + ) + } + + async fn mock_chainfire_list( + AxumState(state): AxumState, + AxumQuery(query): AxumQuery, + ) -> AxumJson { + let prefix = query.prefix.unwrap_or_default(); + let values = state.values.lock().await; + let mut items = values + .iter() + .filter(|(key, _)| key.starts_with(&prefix)) + .map(|(key, value)| { + serde_json::json!({ + "key": key, + "value": value, + }) + }) + .collect::>(); + items.sort_by(|left, right| left["key"].as_str().cmp(&right["key"].as_str())); + AxumJson(serde_json::json!({ + "data": { + "items": items, + }, + "meta": { + "request_id": "test", + "timestamp": chrono::Utc::now().to_rfc3339(), + } + })) + } + + #[tokio::test] + async fn chainfire_metadata_store_round_trips_volume_metadata() { + let (_tempdir, mut config) = test_config(); + prepare_dirs(&config).await.unwrap(); + + let state = MockChainfireState { + values: Arc::new(TokioMutex::new(HashMap::new())), + stale_once_keys: Arc::new(TokioMutex::new(HashSet::new())), + }; + let app = AxumRouter::new() + .route( + "/api/v1/kv/{*key}", + axum_get(mock_chainfire_get) + .put(mock_chainfire_put) + .delete(mock_chainfire_delete), + ) + .route("/api/v1/kv", axum_get(mock_chainfire_list)) + .with_state(state); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + config.metadata_backend = MetadataBackend::Chainfire; + config.chainfire_api_url = Some(format!("http://{}", addr)); + config.chainfire_key_prefix = "/coronafs/test".to_string(); + let store = MetadataStore::from_config(&config).unwrap(); + + let meta = VolumeMetadata { + id: "vol-chainfire".to_string(), + size_bytes: 1024, + format: VolumeFileFormat::Raw, + node_local: false, + materialized_from: None, + port: Some(18001), + export_pid: Some(1234), + export_read_only: Some(false), + created_at: chrono::Utc::now().to_rfc3339(), + updated_at: chrono::Utc::now().to_rfc3339(), + }; + + store.save(&config, &meta).await.unwrap(); + let loaded = store.load(&config, "vol-chainfire").await.unwrap().unwrap(); + assert_eq!(loaded.id, meta.id); + assert_eq!(loaded.size_bytes, meta.size_bytes); + assert_eq!(loaded.port, meta.port); + + let listed = store.list(&config).await.unwrap(); + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].id, meta.id); + + store.delete(&config, "vol-chainfire").await.unwrap(); + assert!(store + .load(&config, "vol-chainfire") + .await + .unwrap() + .is_none()); + + server.abort(); + } + + #[tokio::test] + async fn create_blank_impl_does_not_require_immediate_chainfire_read_after_write() { + let (_tempdir, mut config) = test_config(); + prepare_dirs(&config).await.unwrap(); + + let state = MockChainfireState { + values: Arc::new(TokioMutex::new(HashMap::new())), + stale_once_keys: Arc::new(TokioMutex::new(HashSet::new())), + }; + let full_key = "/coronafs/test/vol-stale".to_string(); + state.stale_once_keys.lock().await.insert(full_key.clone()); + let app = AxumRouter::new() + .route( + "/api/v1/kv/{*key}", + axum_get(mock_chainfire_get) + .put(mock_chainfire_put) + .delete(mock_chainfire_delete), + ) + .route("/api/v1/kv", axum_get(mock_chainfire_list)) + .with_state(state.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + config.metadata_backend = MetadataBackend::Chainfire; + config.chainfire_api_url = Some(format!("http://{}", addr)); + config.chainfire_key_prefix = "/coronafs/test".to_string(); + let app_state = AppState::new(config).await.unwrap(); + + let response = create_blank_impl( + &app_state, + "vol-stale", + CreateVolumeRequest { + size_bytes: 1024 * 1024, + format: Some(VolumeFileFormat::Raw), + backing_file: None, + backing_format: None, + }, + ) + .await + .unwrap(); + + assert_eq!(response.id, "vol-stale"); + assert_eq!(response.size_bytes, 1024 * 1024); + assert!(state.values.lock().await.contains_key(&full_key)); + + server.abort(); + } + + #[test] + fn parse_chainfire_base_urls_supports_comma_separated_endpoints() { + let urls = + parse_chainfire_base_urls("http://127.0.0.1:2379, http://10.0.0.2:2379/").unwrap(); + assert_eq!( + urls, + vec![ + "http://127.0.0.1:2379".to_string(), + "http://10.0.0.2:2379".to_string() + ] + ); + } + + #[test] + fn volume_create_api_is_available_in_node_mode() { + let mut config = ServerConfig::default(); + config.mode = ServerMode::Node; + let state = AppState { + config: Arc::new(config), + metadata_store: MetadataStore::Filesystem, + volume_guards: Arc::new(Mutex::new(HashMap::new())), + reserved_ports: Arc::new(Mutex::new(HashSet::new())), + }; + assert!(ensure_volume_create_api(&state, "volume create").is_ok()); + } } diff --git a/coronafs/scripts/benchmark-local-export.sh b/coronafs/scripts/benchmark-local-export.sh new file mode 100755 index 0000000..bac5d30 --- /dev/null +++ b/coronafs/scripts/benchmark-local-export.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "missing required command: $1" >&2 + exit 1 + } +} + +for cmd in curl qemu-io; do + require_cmd "${cmd}" +done + +if ! command -v jq >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then + echo "missing required command: jq or python3" >&2 + exit 1 +fi + +json_get() { + local query="$1" + if command -v jq >/dev/null 2>&1; then + jq -r "${query}" + else + python3 -c 'import json,sys +data=json.load(sys.stdin) +value=data +for part in sys.argv[1].split("."): + if not part: + continue + value=value.get(part) if isinstance(value, dict) else None + if value is None: + break +print("" if value is None else value) +' "${query}" + fi +} + +RUN_ID="${CORONAFS_BENCH_RUN_ID:-$$}" +LISTEN_PORT="${CORONAFS_BENCH_PORT:-$((25088 + (RUN_ID % 1000)))}" +EXPORT_BASE_PORT="${CORONAFS_BENCH_EXPORT_BASE_PORT:-$((26100 + (RUN_ID % 1000)))}" +VOLUME_ID="${CORONAFS_BENCH_VOLUME_ID:-local-bench-${RUN_ID}}" +SIZE_MIB="${CORONAFS_BENCH_SIZE_MIB:-${CORONAFS_BENCH_SIZE_MB:-512}}" +SIZE_BYTES="${CORONAFS_BENCH_SIZE_BYTES:-$((SIZE_MIB * 1024 * 1024))}" +WORKLOAD_MIB="${CORONAFS_BENCH_WORKLOAD_MIB:-${CORONAFS_BENCH_WORKLOAD_MB:-256}}" +EXPORT_CACHE_MODE="${CORONAFS_BENCH_EXPORT_CACHE_MODE:-none}" +EXPORT_AIO_MODE="${CORONAFS_BENCH_EXPORT_AIO_MODE:-threads}" +EXPORT_DISCARD_MODE="${CORONAFS_BENCH_EXPORT_DISCARD_MODE:-ignore}" +EXPORT_DETECT_ZEROES_MODE="${CORONAFS_BENCH_EXPORT_DETECT_ZEROES_MODE:-off}" +SERVER_BIN="${CORONAFS_SERVER_BIN:-}" + +if (( WORKLOAD_MIB > SIZE_MIB )); then + echo "workload ${WORKLOAD_MIB} MiB exceeds volume size ${SIZE_MIB} MiB" >&2 + exit 1 +fi + +if [[ -z "${SERVER_BIN}" ]]; then + SERVER_CMD=( + cargo run + --manifest-path "${REPO_ROOT}/coronafs/Cargo.toml" + -p coronafs-server + -- + ) +else + SERVER_CMD=("${SERVER_BIN}") +fi + +TMP_DIR="$(mktemp -d)" +CONFIG_PATH="${TMP_DIR}/coronafs.toml" +SERVER_LOG="${TMP_DIR}/coronafs.log" +SERVER_PID="" + +show_server_log() { + if [[ -f "${SERVER_LOG}" ]]; then + echo "--- coronafs server log ---" >&2 + tail -n 200 "${SERVER_LOG}" >&2 || true + echo "--- end coronafs server log ---" >&2 + fi +} + +delete_volume_if_present() { + curl -fsS -X DELETE "http://127.0.0.1:${LISTEN_PORT}/v1/volumes/${VOLUME_ID}" >/dev/null 2>&1 || true +} + +cleanup() { + delete_volume_if_present + local pid_file="${TMP_DIR}/data/pids/${VOLUME_ID}.pid" + if [[ -f "${pid_file}" ]]; then + local export_pid="" + export_pid="$(tr -d '\n' <"${pid_file}" 2>/dev/null || true)" + if [[ -n "${export_pid}" ]] && kill -0 "${export_pid}" 2>/dev/null; then + kill "${export_pid}" >/dev/null 2>&1 || true + wait "${export_pid}" >/dev/null 2>&1 || true + fi + rm -f "${pid_file}" + fi + if [[ -n "${SERVER_PID}" ]] && kill -0 "${SERVER_PID}" 2>/dev/null; then + kill "${SERVER_PID}" >/dev/null 2>&1 || true + wait "${SERVER_PID}" >/dev/null 2>&1 || true + fi + rm -rf "${TMP_DIR}" +} +trap cleanup EXIT + +cat >"${CONFIG_PATH}" <"${SERVER_LOG}" 2>&1 & +SERVER_PID="$!" + +deadline=$((SECONDS + 60)) +until curl -fsS "http://127.0.0.1:${LISTEN_PORT}/healthz" >/dev/null 2>&1; do + if (( SECONDS >= deadline )); then + echo "timed out waiting for coronafs local bench server" >&2 + tail -n 200 "${SERVER_LOG}" >&2 || true + exit 1 + fi + sleep 1 +done + +create_response_file="${TMP_DIR}/create-response.txt" +create_status="$( + curl -sS \ + -o "${create_response_file}" \ + -w '%{http_code}' \ + -X PUT \ + -H 'content-type: application/json' \ + -d "{\"size_bytes\":${SIZE_BYTES}}" \ + "http://127.0.0.1:${LISTEN_PORT}/v1/volumes/${VOLUME_ID}" +)" +if [[ "${create_status}" -lt 200 || "${create_status}" -ge 300 ]]; then + echo "failed to create CoronaFS benchmark volume: HTTP ${create_status}" >&2 + cat "${create_response_file}" >&2 || true + show_server_log + exit 1 +fi + +export_response_file="${TMP_DIR}/export-response.txt" +export_status="$( + curl -sS \ + -o "${export_response_file}" \ + -w '%{http_code}' \ + -X POST \ + "http://127.0.0.1:${LISTEN_PORT}/v1/volumes/${VOLUME_ID}/export" +)" +if [[ "${export_status}" -lt 200 || "${export_status}" -ge 300 ]]; then + echo "failed to export CoronaFS benchmark volume: HTTP ${export_status}" >&2 + cat "${export_response_file}" >&2 || true + show_server_log + exit 1 +fi +EXPORT_JSON="$(cat "${export_response_file}")" +EXPORT_URI="$(printf '%s' "${EXPORT_JSON}" | json_get '.export.uri')" +[[ -n "${EXPORT_URI}" && "${EXPORT_URI}" != "null" ]] || { + echo "failed to obtain CoronaFS export URI" >&2 + printf '%s\n' "${EXPORT_JSON}" >&2 + show_server_log + exit 1 +} + +run_qemu_io() { + local extra=() + local start_ns end_ns elapsed_ns + local args=("$@") + local cmd=() + local qemu_cmd="" + + if [[ "${#args[@]}" -eq 0 ]]; then + echo "run_qemu_io requires at least one qemu-io command" >&2 + exit 1 + fi + + while [[ "${#args[@]}" -gt 0 && "${args[0]}" == --* ]]; do + extra+=("${args[0]}") + args=("${args[@]:1}") + done + + cmd=(qemu-io -f raw "${extra[@]}") + for qemu_cmd in "${args[@]}"; do + cmd+=(-c "${qemu_cmd}") + done + cmd+=("${EXPORT_URI}") + + start_ns="$(date +%s%N)" + "${cmd[@]}" >/dev/null + end_ns="$(date +%s%N)" + elapsed_ns="$((end_ns - start_ns))" + printf '%s\n' "${elapsed_ns}" +} + +calc_mib_per_s() { + local bytes="$1" + local elapsed_ns="$2" + awk -v bytes="${bytes}" -v elapsed_ns="${elapsed_ns}" ' + BEGIN { + if (elapsed_ns <= 0) { + print "0.00" + } else { + printf "%.2f", (bytes / 1048576.0) / (elapsed_ns / 1000000000.0) + } + } + ' +} + +BYTES="$((WORKLOAD_MIB * 1024 * 1024))" +WRITE_NS="$(run_qemu_io "write -P 0x5a 0 ${WORKLOAD_MIB}M" "flush")" +READ_NS="$(run_qemu_io "read -P 0x5a 0 ${WORKLOAD_MIB}M")" +WRITE_MIBPS="$(calc_mib_per_s "${BYTES}" "${WRITE_NS}")" +READ_MIBPS="$(calc_mib_per_s "${BYTES}" "${READ_NS}")" + +printf 'CoronaFS local export bench: uri=%s cache=%s aio=%s write=%s MiB/s read=%s MiB/s size=%s MiB\n' \ + "${EXPORT_URI}" "${EXPORT_CACHE_MODE}" "${EXPORT_AIO_MODE}" "${WRITE_MIBPS}" "${READ_MIBPS}" "${WORKLOAD_MIB}" + +printf '%s\t%s\t%s\t%s\t%s\n' "${EXPORT_URI}" "${EXPORT_CACHE_MODE}" "${EXPORT_AIO_MODE}" "${WRITE_MIBPS}" "${READ_MIBPS}" diff --git a/creditservice/Cargo.lock b/creditservice/Cargo.lock index b2cdb97..ed1fe48 100644 --- a/creditservice/Cargo.lock +++ b/creditservice/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -39,9 +74,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -54,15 +89,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -89,9 +124,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -104,6 +139,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -129,7 +176,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -140,7 +187,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -276,6 +323,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -284,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -300,6 +353,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -311,32 +373,33 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecheck" @@ -368,15 +431,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -434,9 +497,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -447,10 +510,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.54" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -458,9 +531,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -470,27 +543,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -658,9 +731,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -677,9 +760,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -703,7 +786,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -776,9 +859,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -849,9 +932,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -864,9 +947,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -874,15 +957,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -902,38 +985,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -943,7 +1026,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -984,6 +1066,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -1245,12 +1337,12 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1268,14 +1360,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1284,7 +1375,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1294,7 +1385,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64 0.22.1", "iam-audit", @@ -1304,6 +1397,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1388,6 +1482,8 @@ dependencies = [ "http 1.4.0", "iam-client", "iam-types", + "serde_json", + "tokio", "tonic", "tracing", ] @@ -1423,9 +1519,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1568,10 +1664,19 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1609,15 +1714,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1646,19 +1751,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1674,9 +1780,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1738,9 +1844,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1810,9 +1916,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1820,6 +1926,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1855,6 +1967,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1916,29 +2039,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1952,6 +2075,24 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1983,7 +2124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2030,7 +2171,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.114", + "syn 2.0.117", "tempfile", ] @@ -2044,7 +2185,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2152,8 +2293,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2162,9 +2303,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2172,7 +2313,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -2190,16 +2331,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2281,23 +2422,23 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2307,9 +2448,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2318,9 +2459,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -2392,7 +2533,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -2407,7 +2548,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2477,11 +2618,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2502,15 +2643,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -2567,9 +2708,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -2584,15 +2725,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2621,11 +2762,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2634,9 +2775,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2659,7 +2800,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2761,9 +2902,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2786,12 +2927,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2838,7 +2979,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.36", + "rustls 0.23.37", "serde", "serde_json", "sha2", @@ -2861,7 +3002,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2883,7 +3024,7 @@ dependencies = [ "sqlx-core", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.114", + "syn 2.0.117", "tokio", "url", ] @@ -2896,7 +3037,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -2991,9 +3132,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3023,7 +3164,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3055,9 +3196,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -3092,7 +3233,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3103,7 +3244,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3158,9 +3299,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3173,9 +3314,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3183,20 +3324,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3215,7 +3356,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls 0.23.37", "tokio", ] @@ -3284,7 +3425,7 @@ dependencies = [ "serde_spanned", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3296,16 +3437,16 @@ dependencies = [ "indexmap 2.13.0", "toml_datetime 0.7.0", "toml_parser", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -3358,7 +3499,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3416,7 +3557,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", @@ -3460,7 +3601,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3486,9 +3627,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3522,9 +3663,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3541,6 +3682,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3633,9 +3784,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3646,9 +3797,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -3660,9 +3811,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3670,31 +3821,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3722,14 +3873,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3765,7 +3916,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3776,7 +3927,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4027,13 +4178,19 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.50.0" @@ -4084,28 +4241,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4125,7 +4282,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -4165,5 +4322,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] diff --git a/deployer/Cargo.lock b/deployer/Cargo.lock index 0ff219b..ed8efc1 100644 --- a/deployer/Cargo.lock +++ b/deployer/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -28,9 +63,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -43,15 +78,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -78,9 +113,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -93,6 +128,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -243,7 +290,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -288,10 +335,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "2.10.0" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -304,9 +366,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -316,15 +378,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -346,7 +408,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "x509-parser 0.18.0", + "x509-parser 0.18.1", ] [[package]] @@ -401,9 +463,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -414,10 +476,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.54" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -425,9 +497,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -437,9 +509,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -449,15 +521,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -530,9 +602,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -558,7 +640,9 @@ name = "deployer-ctl" version = "0.1.0" dependencies = [ "anyhow", + "axum", "chainfire-client", + "chrono", "clap", "deployer-types", "reqwest", @@ -585,9 +669,10 @@ dependencies = [ "rcgen", "serde", "serde_json", + "sha2", "thiserror 1.0.69", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tracing", "tracing-subscriber", ] @@ -631,9 +716,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -732,9 +817,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -844,9 +929,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -859,9 +944,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -869,15 +954,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -897,15 +982,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -914,21 +999,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -938,7 +1023,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -954,9 +1038,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -979,6 +1063,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -987,9 +1081,9 @@ checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -997,7 +1091,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1163,7 +1257,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1181,14 +1275,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1197,7 +1290,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1207,7 +1300,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64", "iam-audit", @@ -1217,6 +1312,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1325,9 +1421,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1461,19 +1557,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1511,15 +1616,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1548,9 +1653,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" @@ -1577,9 +1682,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1635,9 +1740,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1774,9 +1879,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1785,10 +1890,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking" @@ -1819,6 +1930,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1842,7 +1964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] @@ -1864,18 +1986,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1884,9 +2006,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1911,7 +2033,10 @@ name = "plasmacloud-reconciler" version = "0.1.0" dependencies = [ "anyhow", + "chainfire-client", + "chrono", "clap", + "deployer-types", "fiberlb-api", "flashdns-api", "serde", @@ -1922,6 +2047,18 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1958,9 +2095,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2094,7 +2231,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2103,9 +2240,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2131,16 +2268,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2198,7 +2335,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2244,9 +2381,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2256,9 +2393,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2267,9 +2404,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2299,14 +2436,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2317,7 +2454,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2340,9 +2477,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -2353,9 +2490,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -2368,9 +2505,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2389,9 +2526,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2399,9 +2536,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -2416,15 +2553,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2437,9 +2574,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2450,9 +2587,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2527,7 +2664,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "itoa", "ryu", "serde", @@ -2562,10 +2699,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2583,9 +2721,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2608,12 +2746,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2655,7 +2793,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -2802,9 +2940,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2833,9 +2971,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2936,9 +3074,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2951,9 +3089,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2961,16 +3099,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2989,9 +3127,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3000,9 +3138,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3038,7 +3176,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -3121,9 +3259,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3148,7 +3286,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -3167,9 +3305,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3190,9 +3328,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3221,9 +3359,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3260,9 +3398,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3279,6 +3417,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3377,9 +3525,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3390,11 +3538,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3403,9 +3552,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3413,9 +3562,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3426,18 +3575,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3459,14 +3608,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3803,9 +3952,9 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs 0.7.1", "data-encoding", @@ -3852,18 +4001,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/deployer/crates/deployer-ctl/Cargo.toml b/deployer/crates/deployer-ctl/Cargo.toml index 9ac0cd0..ef0158c 100644 --- a/deployer/crates/deployer-ctl/Cargo.toml +++ b/deployer/crates/deployer-ctl/Cargo.toml @@ -12,8 +12,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" +chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } chainfire-client = { path = "../../../chainfire/chainfire-client" } deployer-types = { path = "../deployer-types" } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +[dev-dependencies] +axum = { version = "0.7", features = ["macros"] } diff --git a/deployer/crates/deployer-ctl/src/chainfire.rs b/deployer/crates/deployer-ctl/src/chainfire.rs index e09d5c7..7126737 100644 --- a/deployer/crates/deployer-ctl/src/chainfire.rs +++ b/deployer/crates/deployer-ctl/src/chainfire.rs @@ -4,7 +4,12 @@ use std::path::Path; use anyhow::{Context, Result}; use chainfire_client::{Client, ClientError}; -use deployer_types::{ClusterStateSpec, DesiredSystemSpec, InstallPlan, NodeConfig, NodeSpec}; +use chrono::Utc; +use deployer_types::{ + ClusterNodeRecord, ClusterStateSpec, CommissionState, DesiredSystemSpec, HostDeploymentSpec, + HostDeploymentStatus, InstallPlan, InstallState, NodeConfig, NodeSpec, ObservedSystemState, + PowerState, +}; use serde::de::DeserializeOwned; use serde_json::{json, Value}; use tokio::fs; @@ -49,6 +54,56 @@ fn key_desired_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) .into_bytes() } +fn key_observed_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec { + format!( + "{}nodes/{}/observed-system", + cluster_prefix(cluster_namespace, cluster_id), + node_id + ) + .into_bytes() +} + +fn key_host_deployment_spec( + cluster_namespace: &str, + cluster_id: &str, + deployment_name: &str, +) -> Vec { + format!( + "{}deployments/hosts/{}/spec", + cluster_prefix(cluster_namespace, cluster_id), + deployment_name + ) + .into_bytes() +} + +fn key_host_deployment_status( + cluster_namespace: &str, + cluster_id: &str, + deployment_name: &str, +) -> Vec { + format!( + "{}deployments/hosts/{}/status", + cluster_prefix(cluster_namespace, cluster_id), + deployment_name + ) + .into_bytes() +} + +fn parse_commission_state(value: &str) -> Result { + serde_json::from_str(&format!("\"{value}\"")) + .with_context(|| format!("invalid commission state {value}")) +} + +fn parse_install_state(value: &str) -> Result { + serde_json::from_str(&format!("\"{value}\"")) + .with_context(|| format!("invalid install state {value}")) +} + +fn parse_power_state(value: &str) -> Result { + serde_json::from_str(&format!("\"{value}\"")) + .with_context(|| format!("invalid power state {value}")) +} + fn key_node_class(cluster_namespace: &str, cluster_id: &str, node_class: &str) -> Vec { format!( "{}node-classes/{}", @@ -178,6 +233,9 @@ fn desired_system_from_spec(node: &NodeSpec) -> Option { if desired.rollback_on_failure.is_none() { desired.rollback_on_failure = Some(true); } + if desired.drain_before_apply.is_none() { + desired.drain_before_apply = Some(false); + } if desired.nixos_configuration.is_some() { Some(desired) } else { @@ -322,6 +380,30 @@ async fn merge_existing_node_observed_fields( if merged.state.is_none() { merged.state = existing_node.state; } + if merged.machine_id.is_none() { + merged.machine_id = existing_node.machine_id; + } + if merged.hardware_facts.is_none() { + merged.hardware_facts = existing_node.hardware_facts; + } + if merged.commission_state.is_none() { + merged.commission_state = existing_node.commission_state; + } + if merged.install_state.is_none() { + merged.install_state = existing_node.install_state; + } + if merged.commissioned_at.is_none() { + merged.commissioned_at = existing_node.commissioned_at; + } + if merged.last_inventory_hash.is_none() { + merged.last_inventory_hash = existing_node.last_inventory_hash; + } + if merged.power_state.is_none() { + merged.power_state = existing_node.power_state; + } + if merged.bmc_ref.is_none() { + merged.bmc_ref = existing_node.bmc_ref; + } if merged.last_heartbeat.is_none() { merged.last_heartbeat = existing_node.last_heartbeat; } @@ -521,6 +603,13 @@ pub async fn bootstrap_cluster( info!(enrollment_rule = %rule.name, "upserted enrollment rule"); } + for deployment in &spec.host_deployments { + let key = key_host_deployment_spec(cluster_namespace, cluster_id, &deployment.name); + let value = serde_json::to_vec(deployment)?; + client.put(&key, &value).await?; + info!(deployment = %deployment.name, "upserted host deployment"); + } + // 3. Service / Instance (必要であれば) for svc in &spec.services { let key = key_service(cluster_namespace, cluster_id, &svc.name); @@ -627,6 +716,11 @@ pub async fn apply_cluster_state( let value = serde_json::to_vec(rule)?; client.put(&key, &value).await?; } + for deployment in &spec.host_deployments { + let key = key_host_deployment_spec(cluster_namespace, cluster_id, &deployment.name); + let value = serde_json::to_vec(deployment)?; + client.put(&key, &value).await?; + } for svc in &spec.services { let key = key_service(cluster_namespace, cluster_id, &svc.name); let value = serde_json::to_vec(svc)?; @@ -706,6 +800,421 @@ pub async fn dump_prefix(endpoint: &str, prefix: &str, json_output: bool) -> Res .await } +async fn get_json_key(client: &mut Client, key: &[u8]) -> Result> { + client + .get(key) + .await? + .map(|bytes| serde_json::from_slice::(&bytes)) + .transpose() + .with_context(|| format!("failed to decode key {}", String::from_utf8_lossy(key))) +} + +pub async fn inspect_node( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + node_id: &str, + include_desired_system: bool, + include_observed_system: bool, + json_output: bool, +) -> Result<()> { + let endpoints = chainfire_endpoints(endpoint); + with_chainfire_endpoint_failover(&endpoints, "inspect node", |endpoint| { + let endpoint = endpoint.to_string(); + let cluster_namespace = cluster_namespace.to_string(); + let cluster_id = cluster_id.to_string(); + let node_id = node_id.to_string(); + async move { + let mut client = Client::connect(endpoint).await?; + let node = get_json_key::( + &mut client, + &key_node(&cluster_namespace, &cluster_id, &node_id), + ) + .await? + .with_context(|| format!("node {} not found", node_id))?; + + let desired_system = if include_desired_system { + get_json_key::( + &mut client, + &key_desired_system(&cluster_namespace, &cluster_id, &node_id), + ) + .await? + } else { + None + }; + + let observed_system = if include_observed_system { + get_json_key::( + &mut client, + &key_observed_system(&cluster_namespace, &cluster_id, &node_id), + ) + .await? + } else { + None + }; + + if json_output { + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "node": node, + "desired_system": desired_system, + "observed_system": observed_system, + }))? + ); + } else { + println!("node_id={}", node.node_id); + println!("hostname={}", node.hostname); + println!("ip={}", node.ip); + println!("state={}", node.state.as_deref().unwrap_or("unknown")); + println!( + "commission_state={}", + node.commission_state + .map(|value| serde_json::to_string(&value).unwrap_or_default()) + .unwrap_or_else(|| "\"unknown\"".to_string()) + ); + println!( + "install_state={}", + node.install_state + .map(|value| serde_json::to_string(&value).unwrap_or_default()) + .unwrap_or_else(|| "\"unknown\"".to_string()) + ); + if let Some(observed_system) = observed_system { + println!( + "observed_status={}", + observed_system.status.unwrap_or_else(|| "unknown".to_string()) + ); + } + } + + Ok(()) + } + }) + .await +} + +pub async fn set_node_states( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + node_id: &str, + state: Option, + commission_state: Option, + install_state: Option, + power_state: Option, + bmc_ref: Option, +) -> Result<()> { + let endpoints = chainfire_endpoints(endpoint); + with_chainfire_endpoint_failover(&endpoints, "set node state", |endpoint| { + let endpoint = endpoint.to_string(); + let cluster_namespace = cluster_namespace.to_string(); + let cluster_id = cluster_id.to_string(); + let node_id = node_id.to_string(); + let state = state.clone(); + let commission_state = commission_state.clone(); + let install_state = install_state.clone(); + let power_state = power_state.clone(); + let bmc_ref = bmc_ref.clone(); + async move { + let mut client = Client::connect(endpoint).await?; + let key = key_node(&cluster_namespace, &cluster_id, &node_id); + let mut node = get_json_key::(&mut client, &key) + .await? + .with_context(|| format!("node {} not found", node_id))?; + + if let Some(state) = state { + node.state = Some(state); + } + if let Some(commission_state) = commission_state { + let parsed = parse_commission_state(&commission_state)?; + if matches!(parsed, CommissionState::Commissioned) && node.commissioned_at.is_none() + { + node.commissioned_at = Some(Utc::now()); + } + node.commission_state = Some(parsed); + } + if let Some(install_state) = install_state { + node.install_state = Some(parse_install_state(&install_state)?); + } + if let Some(power_state) = power_state { + node.power_state = Some(parse_power_state(&power_state)?); + } + if let Some(bmc_ref) = bmc_ref { + node.bmc_ref = Some(bmc_ref); + } + + client.put(&key, &serde_json::to_vec(&node)?).await?; + println!("{}", serde_json::to_string_pretty(&node)?); + Ok(()) + } + }) + .await +} + +#[allow(clippy::too_many_arguments)] +pub async fn set_observed_system( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + node_id: &str, + status: Option, + nixos_configuration: Option, + target_system: Option, + current_system: Option, + configured_system: Option, + booted_system: Option, + rollback_system: Option, +) -> Result<()> { + let endpoints = chainfire_endpoints(endpoint); + with_chainfire_endpoint_failover(&endpoints, "set observed system", |endpoint| { + let endpoint = endpoint.to_string(); + let cluster_namespace = cluster_namespace.to_string(); + let cluster_id = cluster_id.to_string(); + let node_id = node_id.to_string(); + let status = status.clone(); + let nixos_configuration = nixos_configuration.clone(); + let target_system = target_system.clone(); + let current_system = current_system.clone(); + let configured_system = configured_system.clone(); + let booted_system = booted_system.clone(); + let rollback_system = rollback_system.clone(); + async move { + let mut client = Client::connect(endpoint).await?; + let key = key_observed_system(&cluster_namespace, &cluster_id, &node_id); + let mut observed = get_json_key::(&mut client, &key) + .await? + .unwrap_or_else(|| ObservedSystemState { + node_id: node_id.clone(), + ..ObservedSystemState::default() + }); + + observed.node_id = node_id.clone(); + if let Some(status) = status { + observed.status = Some(status); + } + if let Some(nixos_configuration) = nixos_configuration { + observed.nixos_configuration = Some(nixos_configuration); + } + if let Some(target_system) = target_system { + observed.target_system = Some(target_system); + } + if let Some(current_system) = current_system { + observed.current_system = Some(current_system); + } + if let Some(configured_system) = configured_system { + observed.configured_system = Some(configured_system); + } + if let Some(booted_system) = booted_system { + observed.booted_system = Some(booted_system); + } + if let Some(rollback_system) = rollback_system { + observed.rollback_system = Some(rollback_system); + } + + client.put(&key, &serde_json::to_vec(&observed)?).await?; + println!("{}", serde_json::to_string_pretty(&observed)?); + Ok(()) + } + }) + .await +} + +pub async fn inspect_host_deployment( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + deployment_name: &str, + json_output: bool, +) -> Result<()> { + let endpoints = chainfire_endpoints(endpoint); + with_chainfire_endpoint_failover(&endpoints, "inspect host deployment", |endpoint| { + let endpoint = endpoint.to_string(); + let cluster_namespace = cluster_namespace.to_string(); + let cluster_id = cluster_id.to_string(); + let deployment_name = deployment_name.to_string(); + async move { + let mut client = Client::connect(endpoint).await?; + let spec = get_json_key::( + &mut client, + &key_host_deployment_spec(&cluster_namespace, &cluster_id, &deployment_name), + ) + .await? + .with_context(|| format!("host deployment {} not found", deployment_name))?; + let status = get_json_key::( + &mut client, + &key_host_deployment_status(&cluster_namespace, &cluster_id, &deployment_name), + ) + .await?; + + if json_output { + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "spec": spec, + "status": status, + }))? + ); + } else { + println!("name={}", spec.name); + println!( + "nixos_configuration={}", + spec.nixos_configuration.as_deref().unwrap_or("unknown") + ); + if let Some(status) = status { + println!("phase={}", status.phase.as_deref().unwrap_or("unknown")); + println!("paused={}", status.paused); + println!("selected_nodes={}", status.selected_nodes.join(",")); + println!("completed_nodes={}", status.completed_nodes.join(",")); + println!("failed_nodes={}", status.failed_nodes.join(",")); + } + } + + Ok(()) + } + }) + .await +} + +pub async fn set_host_deployment_paused( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + deployment_name: &str, + paused: bool, +) -> Result<()> { + let endpoints = chainfire_endpoints(endpoint); + with_chainfire_endpoint_failover(&endpoints, "set host deployment pause state", |endpoint| { + let endpoint = endpoint.to_string(); + let cluster_namespace = cluster_namespace.to_string(); + let cluster_id = cluster_id.to_string(); + let deployment_name = deployment_name.to_string(); + async move { + let mut client = Client::connect(endpoint).await?; + let spec_key = key_host_deployment_spec(&cluster_namespace, &cluster_id, &deployment_name); + if client.get(&spec_key).await?.is_none() { + return Err(anyhow::anyhow!( + "host deployment {} not found", + deployment_name + )); + } + + let status_key = + key_host_deployment_status(&cluster_namespace, &cluster_id, &deployment_name); + let mut status = get_json_key::(&mut client, &status_key) + .await? + .unwrap_or_else(|| HostDeploymentStatus { + name: deployment_name.clone(), + ..HostDeploymentStatus::default() + }); + status.name = deployment_name.clone(); + status.paused_by_operator = paused; + status.paused = paused; + status.phase = Some(if paused { "paused" } else { "ready" }.to_string()); + status.message = Some(if paused { + "paused by operator".to_string() + } else { + "resumed by operator".to_string() + }); + status.updated_at = Some(Utc::now()); + client.put(&status_key, &serde_json::to_vec(&status)?).await?; + println!("{}", serde_json::to_string_pretty(&status)?); + Ok(()) + } + }) + .await +} + +pub async fn abort_host_deployment( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + deployment_name: &str, +) -> Result<()> { + let endpoints = chainfire_endpoints(endpoint); + with_chainfire_endpoint_failover(&endpoints, "abort host deployment", |endpoint| { + let endpoint = endpoint.to_string(); + let cluster_namespace = cluster_namespace.to_string(); + let cluster_id = cluster_id.to_string(); + let deployment_name = deployment_name.to_string(); + async move { + let mut client = Client::connect(endpoint).await?; + let spec_key = key_host_deployment_spec(&cluster_namespace, &cluster_id, &deployment_name); + if client.get(&spec_key).await?.is_none() { + return Err(anyhow::anyhow!( + "host deployment {} not found", + deployment_name + )); + } + + let node_prefix = format!("{}nodes/", cluster_prefix(&cluster_namespace, &cluster_id)); + let existing = client.get_prefix(node_prefix.as_bytes()).await?; + let mut cleared_nodes = Vec::new(); + + for (key, value) in &existing { + let key_str = String::from_utf8_lossy(&key); + if key_str.ends_with("/desired-system") { + let Ok(desired) = serde_json::from_slice::(value) else { + continue; + }; + if desired.deployment_id.as_deref() == Some(deployment_name.as_str()) { + client.delete(&key).await?; + cleared_nodes.push(desired.node_id.clone()); + } + } + } + + for (key, value) in existing { + let key_str = String::from_utf8_lossy(&key); + if key_str.ends_with("/desired-system") { + continue; + } + + let node_suffix = key_str + .strip_prefix(&node_prefix) + .filter(|suffix| !suffix.contains('/')); + let Some(node_id) = node_suffix else { + continue; + }; + let mut node = match serde_json::from_slice::(&value) { + Ok(node) => node, + Err(_) => continue, + }; + if cleared_nodes.iter().any(|cleared| cleared == node_id) + && node.state.as_deref() == Some("draining") + { + node.state = Some("active".to_string()); + client.put(&key, &serde_json::to_vec(&node)?).await?; + } + } + + let status = HostDeploymentStatus { + name: deployment_name.clone(), + phase: Some("aborted".to_string()), + paused: true, + paused_by_operator: true, + selected_nodes: Vec::new(), + completed_nodes: Vec::new(), + in_progress_nodes: Vec::new(), + failed_nodes: Vec::new(), + message: Some(format!( + "aborted by operator; cleared desired-system from {} node(s)", + cleared_nodes.len() + )), + updated_at: Some(Utc::now()), + }; + client + .put( + &key_host_deployment_status(&cluster_namespace, &cluster_id, &deployment_name), + &serde_json::to_vec(&status)?, + ) + .await?; + println!("{}", serde_json::to_string_pretty(&status)?); + Ok(()) + } + }) + .await +} + async fn prune_cluster_state( client: &mut Client, cluster_namespace: &str, @@ -762,6 +1271,16 @@ async fn prune_cluster_state( .to_string(), ); } + for deployment in &spec.host_deployments { + desired_keys.insert( + String::from_utf8_lossy(&key_host_deployment_spec( + cluster_namespace, + cluster_id, + &deployment.name, + )) + .to_string(), + ); + } for svc in &spec.services { desired_keys.insert( String::from_utf8_lossy(&key_service(cluster_namespace, cluster_id, &svc.name)) @@ -893,11 +1412,18 @@ mod tests { failure_domain: Some("rack-a".to_string()), nix_profile: None, install_plan: None, + hardware_facts: None, desired_system: None, state: Some(match NodeState::Pending { NodeState::Pending => "pending".to_string(), _ => unreachable!(), }), + commission_state: None, + install_state: None, + commissioned_at: None, + last_inventory_hash: None, + power_state: None, + bmc_ref: None, last_heartbeat: None, }], node_classes: vec![deployer_types::NodeClassSpec { @@ -922,6 +1448,7 @@ mod tests { labels: HashMap::from([("env".to_string(), "dev".to_string())]), }], enrollment_rules: vec![], + host_deployments: vec![], services: vec![], instances: vec![], mtls_policies: vec![], @@ -983,11 +1510,13 @@ mod tests { let mut spec = test_spec(); spec.nodes[0].desired_system = Some(DesiredSystemSpec { node_id: String::new(), + deployment_id: None, nixos_configuration: Some("node01-next".to_string()), flake_ref: Some("github:centra/cloud".to_string()), switch_action: Some("boot".to_string()), health_check_command: vec!["true".to_string()], rollback_on_failure: Some(false), + drain_before_apply: Some(false), }); let resolved = resolve_nodes(&spec).unwrap(); @@ -1012,6 +1541,14 @@ mod tests { &format!("{}nodes/node01/observed-system", prefix), &prefix )); + assert!(is_prunable_key( + &format!("{}deployments/hosts/worker-rollout/spec", prefix), + &prefix + )); + assert!(!is_prunable_key( + &format!("{}deployments/hosts/worker-rollout/status", prefix), + &prefix + )); } } @@ -1028,6 +1565,7 @@ fn is_prunable_key(key: &str, prefix: &str) -> bool { key.starts_with(&format!("{}node-classes/", prefix)) || key.starts_with(&format!("{}pools/", prefix)) || key.starts_with(&format!("{}enrollment-rules/", prefix)) + || key.starts_with(&format!("{}deployments/hosts/", prefix)) && key.ends_with("/spec") || key.starts_with(&format!("{}services/", prefix)) || key.starts_with(&format!("{}instances/", prefix)) || key.starts_with(&format!("{}mtls/policies/", prefix)) diff --git a/deployer/crates/deployer-ctl/src/main.rs b/deployer/crates/deployer-ctl/src/main.rs index 68b6e28..cb4d841 100644 --- a/deployer/crates/deployer-ctl/src/main.rs +++ b/deployer/crates/deployer-ctl/src/main.rs @@ -5,6 +5,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use tracing_subscriber::EnvFilter; mod chainfire; +mod power; mod remote; /// Deployer control CLI for PhotonCloud. @@ -82,6 +83,132 @@ enum Command { #[arg(long, default_value = "status")] action: String, }, + + /// ノード単位の inventory / lifecycle 状態を確認・更新する + Node { + #[command(subcommand)] + command: NodeCommand, + }, + + /// HostDeployment rollout object を確認・操作する + Deployment { + #[command(subcommand)] + command: DeploymentCommand, + }, +} + +#[derive(Subcommand, Debug)] +enum NodeCommand { + /// 指定ノードの記録と関連 state を表示する + Inspect { + #[arg(long)] + node_id: String, + + #[arg(long, default_value_t = false)] + include_desired_system: bool, + + #[arg(long, default_value_t = false)] + include_observed_system: bool, + + #[arg(long, value_enum, default_value_t = DumpFormat::Json)] + format: DumpFormat, + }, + + /// 指定ノードの lifecycle / commissioning 状態を更新する + SetState { + #[arg(long)] + node_id: String, + + #[arg(long, value_enum)] + state: Option, + + #[arg(long, value_enum)] + commission_state: Option, + + #[arg(long, value_enum)] + install_state: Option, + + #[arg(long, value_enum)] + power_state: Option, + + #[arg(long)] + bmc_ref: Option, + }, + + /// 指定ノードの observed-system を更新する + SetObserved { + #[arg(long)] + node_id: String, + + #[arg(long)] + status: Option, + + #[arg(long)] + nixos_configuration: Option, + + #[arg(long)] + target_system: Option, + + #[arg(long)] + current_system: Option, + + #[arg(long)] + configured_system: Option, + + #[arg(long)] + booted_system: Option, + + #[arg(long)] + rollback_system: Option, + }, + + /// 指定ノードの電源操作を行う + Power { + #[arg(long)] + node_id: String, + + #[arg(long, value_enum)] + action: PowerActionArg, + }, + + /// 指定ノードに再インストールを要求する + Reinstall { + #[arg(long)] + node_id: String, + + #[arg(long, default_value_t = false)] + power_cycle: bool, + }, +} + +#[derive(Subcommand, Debug)] +enum DeploymentCommand { + /// HostDeployment の spec/status を表示する + Inspect { + #[arg(long)] + name: String, + + #[arg(long, value_enum, default_value_t = DumpFormat::Json)] + format: DumpFormat, + }, + + /// HostDeployment を一時停止する + Pause { + #[arg(long)] + name: String, + }, + + /// HostDeployment を再開する + Resume { + #[arg(long)] + name: String, + }, + + /// HostDeployment を中止し、配布済み desired-system を取り消す + Abort { + #[arg(long)] + name: String, + }, } #[derive(Clone, Copy, Debug, ValueEnum)] @@ -90,6 +217,103 @@ enum DumpFormat { Json, } +#[derive(Clone, Copy, Debug, ValueEnum)] +enum NodeLifecycleStateArg { + Pending, + Provisioning, + Active, + Failed, + Draining, +} + +impl NodeLifecycleStateArg { + fn as_str(self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Provisioning => "provisioning", + Self::Active => "active", + Self::Failed => "failed", + Self::Draining => "draining", + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum CommissionStateArg { + Discovered, + Commissioning, + Commissioned, +} + +impl CommissionStateArg { + fn as_str(self) -> &'static str { + match self { + Self::Discovered => "discovered", + Self::Commissioning => "commissioning", + Self::Commissioned => "commissioned", + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum InstallStateArg { + Pending, + Installing, + Installed, + Failed, + ReinstallRequested, +} + +impl InstallStateArg { + fn as_str(self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Installing => "installing", + Self::Installed => "installed", + Self::Failed => "failed", + Self::ReinstallRequested => "reinstall_requested", + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum PowerStateArg { + On, + Off, + Cycling, + Unknown, +} + +impl PowerStateArg { + fn as_str(self) -> &'static str { + match self { + Self::On => "on", + Self::Off => "off", + Self::Cycling => "cycling", + Self::Unknown => "unknown", + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum PowerActionArg { + On, + Off, + Cycle, + Refresh, +} + +impl PowerActionArg { + fn as_str(self) -> &'static str { + match self { + Self::On => "on", + Self::Off => "off", + Self::Cycle => "cycle", + Self::Refresh => "refresh", + } + } +} + #[tokio::main] async fn main() -> Result<()> { let env_filter = @@ -139,6 +363,149 @@ async fn main() -> Result<()> { Command::Deployer { endpoint, action } => { remote::run_deployer_command(&endpoint, &action).await?; } + Command::Node { command } => { + let cluster_id = cli + .cluster_id + .as_deref() + .ok_or_else(|| anyhow::anyhow!("--cluster-id is required for node commands"))?; + + match command { + NodeCommand::Inspect { + node_id, + include_desired_system, + include_observed_system, + format, + } => { + chainfire::inspect_node( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &node_id, + include_desired_system, + include_observed_system, + matches!(format, DumpFormat::Json), + ) + .await?; + } + NodeCommand::SetState { + node_id, + state, + commission_state, + install_state, + power_state, + bmc_ref, + } => { + chainfire::set_node_states( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &node_id, + state.map(|value| value.as_str().to_string()), + commission_state.map(|value| value.as_str().to_string()), + install_state.map(|value| value.as_str().to_string()), + power_state.map(|value| value.as_str().to_string()), + bmc_ref, + ) + .await?; + } + NodeCommand::SetObserved { + node_id, + status, + nixos_configuration, + target_system, + current_system, + configured_system, + booted_system, + rollback_system, + } => { + chainfire::set_observed_system( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &node_id, + status, + nixos_configuration, + target_system, + current_system, + configured_system, + booted_system, + rollback_system, + ) + .await?; + } + NodeCommand::Power { node_id, action } => { + power::power_node( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &node_id, + action.as_str(), + ) + .await?; + } + NodeCommand::Reinstall { + node_id, + power_cycle, + } => { + power::request_reinstall( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &node_id, + power_cycle, + ) + .await?; + } + } + } + Command::Deployment { command } => { + let cluster_id = cli + .cluster_id + .as_deref() + .ok_or_else(|| anyhow::anyhow!("--cluster-id is required for deployment commands"))?; + + match command { + DeploymentCommand::Inspect { name, format } => { + chainfire::inspect_host_deployment( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &name, + matches!(format, DumpFormat::Json), + ) + .await?; + } + DeploymentCommand::Pause { name } => { + chainfire::set_host_deployment_paused( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &name, + true, + ) + .await?; + } + DeploymentCommand::Resume { name } => { + chainfire::set_host_deployment_paused( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &name, + false, + ) + .await?; + } + DeploymentCommand::Abort { name } => { + chainfire::abort_host_deployment( + &cli.chainfire_endpoint, + &cli.cluster_namespace, + cluster_id, + &name, + ) + .await?; + } + } + } } Ok(()) diff --git a/deployer/crates/deployer-ctl/src/power.rs b/deployer/crates/deployer-ctl/src/power.rs new file mode 100644 index 0000000..6f86ad7 --- /dev/null +++ b/deployer/crates/deployer-ctl/src/power.rs @@ -0,0 +1,372 @@ +use anyhow::{Context, Result}; +use chainfire_client::Client; +use deployer_types::{ClusterNodeRecord, InstallState, PowerState}; +use reqwest::{Client as HttpClient, Url}; +use serde::Deserialize; +use serde_json::json; + +fn cluster_prefix(cluster_namespace: &str, cluster_id: &str) -> String { + format!("{}/clusters/{}/", cluster_namespace, cluster_id) +} + +fn key_node(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec { + format!( + "{}nodes/{}", + cluster_prefix(cluster_namespace, cluster_id), + node_id + ) + .into_bytes() +} + +fn key_desired_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec { + format!( + "{}nodes/{}/desired-system", + cluster_prefix(cluster_namespace, cluster_id), + node_id + ) + .into_bytes() +} + +fn key_observed_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec { + format!( + "{}nodes/{}/observed-system", + cluster_prefix(cluster_namespace, cluster_id), + node_id + ) + .into_bytes() +} + +fn chainfire_endpoints(raw: &str) -> Vec { + raw.split(',') + .map(str::trim) + .filter(|endpoint| !endpoint.is_empty()) + .map(ToOwned::to_owned) + .collect() +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PowerAction { + On, + Off, + Cycle, + Refresh, +} + +impl PowerAction { + fn parse(value: &str) -> Result { + match value { + "on" => Ok(Self::On), + "off" => Ok(Self::Off), + "cycle" => Ok(Self::Cycle), + "refresh" => Ok(Self::Refresh), + other => Err(anyhow::anyhow!("unsupported power action {}", other)), + } + } + + fn reset_type(self) -> Option<&'static str> { + match self { + Self::On => Some("On"), + Self::Off => Some("ForceOff"), + Self::Cycle => Some("PowerCycle"), + Self::Refresh => None, + } + } +} + +#[derive(Debug)] +struct RedfishTarget { + resource_url: Url, + username: Option, + password: Option, + insecure: bool, +} + +#[derive(Debug, Deserialize)] +struct RedfishSystemView { + #[serde(rename = "PowerState")] + power_state: Option, +} + +impl RedfishTarget { + fn parse(reference: &str) -> Result { + let rewritten = if let Some(rest) = reference.strip_prefix("redfish+http://") { + format!("http://{rest}") + } else if let Some(rest) = reference.strip_prefix("redfish+https://") { + format!("https://{rest}") + } else if let Some(rest) = reference.strip_prefix("redfish://") { + format!("https://{rest}") + } else { + return Err(anyhow::anyhow!( + "unsupported BMC reference {}; expected redfish:// or redfish+http(s)://", + reference + )); + }; + + let mut resource_url = Url::parse(&rewritten) + .with_context(|| format!("failed to parse BMC reference {}", reference))?; + let insecure = resource_url + .query_pairs() + .any(|(key, value)| key == "insecure" && (value == "1" || value == "true")); + let username = if resource_url.username().is_empty() { + None + } else { + Some(resource_url.username().to_string()) + }; + let password = resource_url.password().map(ToOwned::to_owned); + let system_path = normalize_redfish_system_path(resource_url.path()); + resource_url + .set_username("") + .map_err(|_| anyhow::anyhow!("failed to clear username from BMC reference"))?; + resource_url + .set_password(None) + .map_err(|_| anyhow::anyhow!("failed to clear password from BMC reference"))?; + resource_url.set_query(None); + resource_url.set_path(&system_path); + + Ok(Self { + resource_url, + username, + password, + insecure, + }) + } + + fn action_url(&self) -> Result { + let mut action_url = self.resource_url.clone(); + let path = format!( + "{}/Actions/ComputerSystem.Reset", + self.resource_url.path().trim_end_matches('/') + ); + action_url.set_path(&path); + Ok(action_url) + } + + async fn perform(&self, action: PowerAction) -> Result { + let client = HttpClient::builder() + .danger_accept_invalid_certs(self.insecure) + .build() + .context("failed to create Redfish client")?; + + if let Some(reset_type) = action.reset_type() { + let request = self + .with_auth(client.post(self.action_url()?)) + .json(&json!({ "ResetType": reset_type })); + request + .send() + .await + .context("failed to send Redfish reset request")? + .error_for_status() + .context("Redfish reset request failed")?; + } + + match action { + PowerAction::Cycle => Ok(PowerState::Cycling), + PowerAction::On | PowerAction::Off | PowerAction::Refresh => self.refresh(&client).await, + } + } + + async fn refresh(&self, client: &HttpClient) -> Result { + let response = self + .with_auth(client.get(self.resource_url.clone())) + .send() + .await + .context("failed to query Redfish system resource")? + .error_for_status() + .context("Redfish system query failed")?; + let system: RedfishSystemView = response + .json() + .await + .context("failed to decode Redfish system response")?; + map_redfish_power_state(system.power_state.as_deref()) + } + + fn with_auth(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + match self.username.as_deref() { + Some(username) => request.basic_auth(username, self.password.clone()), + None => request, + } + } +} + +fn normalize_redfish_system_path(path: &str) -> String { + let trimmed = path.trim(); + if trimmed.is_empty() || trimmed == "/" { + return "/redfish/v1/Systems/System.Embedded.1".to_string(); + } + if trimmed.starts_with("/redfish/") { + return trimmed.to_string(); + } + format!("/redfish/v1/Systems/{}", trimmed.trim_start_matches('/')) +} + +fn map_redfish_power_state(value: Option<&str>) -> Result { + match value.unwrap_or("Unknown").to_ascii_lowercase().as_str() { + "on" => Ok(PowerState::On), + "off" => Ok(PowerState::Off), + "poweringon" | "poweringoff" | "cycling" => Ok(PowerState::Cycling), + "unknown" => Ok(PowerState::Unknown), + other => Err(anyhow::anyhow!("unsupported Redfish power state {}", other)), + } +} + +async fn load_node_record( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + node_id: &str, +) -> Result<(Client, ClusterNodeRecord, Vec)> { + let endpoints = chainfire_endpoints(endpoint); + let mut last_error = None; + + for endpoint in endpoints { + match Client::connect(endpoint.clone()).await { + Ok(mut client) => { + let key = key_node(cluster_namespace, cluster_id, node_id); + let Some(bytes) = client.get(&key).await? else { + return Err(anyhow::anyhow!("node {} not found", node_id)); + }; + let node = serde_json::from_slice::(&bytes) + .context("failed to decode node record")?; + return Ok((client, node, key)); + } + Err(error) => last_error = Some(anyhow::Error::new(error)), + } + } + + Err(last_error.unwrap_or_else(|| anyhow::anyhow!("no Chainfire endpoints configured"))) +} + +pub async fn power_node( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + node_id: &str, + action: &str, +) -> Result<()> { + let action = PowerAction::parse(action)?; + let (mut client, mut node, key) = + load_node_record(endpoint, cluster_namespace, cluster_id, node_id).await?; + let bmc_ref = node + .bmc_ref + .clone() + .with_context(|| format!("node {} does not have a bmc_ref", node_id))?; + let target = RedfishTarget::parse(&bmc_ref)?; + let power_state = target.perform(action).await?; + + node.power_state = Some(power_state); + client.put(&key, &serde_json::to_vec(&node)?).await?; + println!("{}", serde_json::to_string_pretty(&node)?); + Ok(()) +} + +pub async fn request_reinstall( + endpoint: &str, + cluster_namespace: &str, + cluster_id: &str, + node_id: &str, + power_cycle: bool, +) -> Result<()> { + let (mut client, mut node, key) = + load_node_record(endpoint, cluster_namespace, cluster_id, node_id).await?; + + node.state = Some("provisioning".to_string()); + node.install_state = Some(InstallState::ReinstallRequested); + + if power_cycle { + let bmc_ref = node + .bmc_ref + .clone() + .with_context(|| format!("node {} does not have a bmc_ref", node_id))?; + let target = RedfishTarget::parse(&bmc_ref)?; + node.power_state = Some(target.perform(PowerAction::Cycle).await?); + } + + client.put(&key, &serde_json::to_vec(&node)?).await?; + client + .delete(&key_desired_system(cluster_namespace, cluster_id, node_id)) + .await?; + client + .delete(&key_observed_system(cluster_namespace, cluster_id, node_id)) + .await?; + println!("{}", serde_json::to_string_pretty(&node)?); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router}; + use serde_json::Value; + use std::sync::{Arc, Mutex}; + use tokio::net::TcpListener; + + #[test] + fn parse_redfish_short_reference_defaults_to_https() { + let parsed = RedfishTarget::parse("redfish://lab-bmc/node01").unwrap(); + assert_eq!(parsed.resource_url.as_str(), "https://lab-bmc/redfish/v1/Systems/node01"); + } + + #[test] + fn parse_redfish_explicit_http_reference_keeps_query_flags_local() { + let parsed = + RedfishTarget::parse("redfish+http://user:pass@127.0.0.1/system-1?insecure=1").unwrap(); + assert_eq!( + parsed.resource_url.as_str(), + "http://127.0.0.1/redfish/v1/Systems/system-1" + ); + assert_eq!(parsed.username.as_deref(), Some("user")); + assert_eq!(parsed.password.as_deref(), Some("pass")); + assert!(parsed.insecure); + } + + #[tokio::test] + async fn redfish_adapter_refreshes_and_resets_power() { + #[derive(Clone, Default)] + struct TestState { + seen_payloads: Arc>>, + } + + async fn system_handler() -> Json { + Json(json!({ "PowerState": "On" })) + } + + async fn reset_handler( + State(state): State, + Json(payload): Json, + ) -> StatusCode { + state + .seen_payloads + .lock() + .unwrap() + .push(payload.to_string()); + StatusCode::NO_CONTENT + } + + let state = TestState::default(); + let app = Router::new() + .route("/redfish/v1/Systems/node01", get(system_handler)) + .route( + "/redfish/v1/Systems/node01/Actions/ComputerSystem.Reset", + post(reset_handler), + ) + .with_state(state.clone()); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let target = RedfishTarget::parse(&format!( + "redfish+http://{}/redfish/v1/Systems/node01", + addr + )) + .unwrap(); + assert_eq!(target.perform(PowerAction::Refresh).await.unwrap(), PowerState::On); + assert_eq!(target.perform(PowerAction::Off).await.unwrap(), PowerState::On); + + let payloads = state.seen_payloads.lock().unwrap().clone(); + assert_eq!(payloads, vec![r#"{"ResetType":"ForceOff"}"#.to_string()]); + + server.abort(); + } +} diff --git a/deployer/crates/deployer-server/Cargo.toml b/deployer/crates/deployer-server/Cargo.toml index 0800807..c397220 100644 --- a/deployer/crates/deployer-server/Cargo.toml +++ b/deployer/crates/deployer-server/Cargo.toml @@ -29,6 +29,7 @@ tracing-subscriber = { workspace = true } chrono = { workspace = true } rcgen = { workspace = true } clap = { workspace = true } +sha2 = "0.10" # ChainFire for state management chainfire-client = { workspace = true } diff --git a/deployer/crates/deployer-server/src/phone_home.rs b/deployer/crates/deployer-server/src/phone_home.rs index 83ca165..317eee7 100644 --- a/deployer/crates/deployer-server/src/phone_home.rs +++ b/deployer/crates/deployer-server/src/phone_home.rs @@ -1,9 +1,11 @@ use axum::{extract::State, http::HeaderMap, http::StatusCode, Json}; use chrono::Utc; use deployer_types::{ - EnrollmentRuleSpec, HardwareFacts, InstallPlan, NodeClassSpec, NodeConfig, NodeInfo, - NodePoolSpec, NodeState, PhoneHomeRequest, PhoneHomeResponse, + CommissionState, EnrollmentRuleSpec, HardwareFacts, InstallPlan, InstallState, + NodeClassSpec, NodeConfig, NodeInfo, NodePoolSpec, NodeState, PhoneHomeRequest, + PhoneHomeResponse, PowerState, }; +use sha2::{Digest, Sha256}; use std::sync::Arc; use tracing::{debug, error, info, warn}; @@ -49,6 +51,14 @@ fn merge_hardware_summary_metadata( } } +fn inventory_hash(hardware_facts: Option<&HardwareFacts>) -> Option { + let hardware_facts = hardware_facts?; + let payload = serde_json::to_vec(hardware_facts).ok()?; + let mut hasher = Sha256::new(); + hasher.update(payload); + Some(format!("{:x}", hasher.finalize())) +} + /// POST /api/v1/phone-home /// /// Handles node registration during first boot. @@ -794,6 +804,21 @@ async fn store_cluster_node_if_configured( install_plan: node_config.install_plan.clone(), hardware_facts: hardware_facts.cloned(), state: Some(format!("{:?}", node_info.state).to_lowercase()), + commission_state: hardware_facts.map(|_| CommissionState::Discovered), + install_state: node_config.install_plan.as_ref().map(|_| InstallState::Pending), + commissioned_at: None, + last_inventory_hash: inventory_hash(hardware_facts), + power_state: node_info + .metadata + .get("power_state") + .and_then(|value| match value.as_str() { + "on" => Some(PowerState::On), + "off" => Some(PowerState::Off), + "cycling" => Some(PowerState::Cycling), + "unknown" => Some(PowerState::Unknown), + _ => None, + }), + bmc_ref: node_info.metadata.get("bmc_ref").cloned(), last_heartbeat: Some(node_info.last_heartbeat), }; diff --git a/deployer/crates/deployer-types/src/lib.rs b/deployer/crates/deployer-types/src/lib.rs index 93bd480..2704f60 100644 --- a/deployer/crates/deployer-types/src/lib.rs +++ b/deployer/crates/deployer-types/src/lib.rs @@ -24,6 +24,62 @@ impl Default for NodeState { } } +/// Commissioning lifecycle for inventory-driven bare-metal onboarding. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CommissionState { + /// Node has been discovered and reported inventory but not yet approved. + Discovered, + /// Manual or automated commissioning is actively validating the node. + Commissioning, + /// Inventory has been accepted and the node can be installed or rolled out. + Commissioned, +} + +impl Default for CommissionState { + fn default() -> Self { + CommissionState::Discovered + } +} + +/// Installation lifecycle for host provisioning and reprovisioning. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InstallState { + /// No install is currently running, but an install may be planned. + Pending, + /// Bootstrap or reinstall is actively writing the target system. + Installing, + /// The desired system has been installed successfully. + Installed, + /// Installation failed and needs operator or controller intervention. + Failed, + /// A reinstall has been requested but not started yet. + ReinstallRequested, +} + +impl Default for InstallState { + fn default() -> Self { + InstallState::Pending + } +} + +/// Best-effort power state tracked by external management adapters. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PowerState { + On, + Off, + Cycling, + Unknown, +} + +impl Default for PowerState { + fn default() -> Self { + PowerState::Unknown + } +} + /// Node information tracked by Deployer #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeInfo { @@ -492,6 +548,18 @@ pub struct ClusterNodeRecord { pub hardware_facts: Option, #[serde(default)] pub state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub commission_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub install_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub commissioned_at: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_inventory_hash: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub power_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bmc_ref: Option, #[serde(default)] pub last_heartbeat: Option>, } @@ -534,6 +602,8 @@ pub struct DesiredSystemSpec { #[serde(default)] pub node_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] + pub deployment_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub nixos_configuration: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub flake_ref: Option, @@ -543,6 +613,8 @@ pub struct DesiredSystemSpec { pub health_check_command: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub rollback_on_failure: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub drain_before_apply: Option, } /// Cluster metadata (PhotonCloud scope). @@ -576,9 +648,23 @@ pub struct NodeSpec { #[serde(default)] pub install_plan: Option, #[serde(default)] + pub hardware_facts: Option, + #[serde(default)] pub desired_system: Option, #[serde(default)] pub state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub commission_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub install_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub commissioned_at: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_inventory_hash: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub power_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bmc_ref: Option, #[serde(default)] pub last_heartbeat: Option>, } @@ -647,6 +733,74 @@ pub struct EnrollmentRuleSpec { pub node_id_prefix: Option, } +/// Selector used by host deployments to target bare-metal nodes declaratively. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct HostDeploymentSelector { + #[serde(default)] + pub node_ids: Vec, + #[serde(default)] + pub roles: Vec, + #[serde(default)] + pub pools: Vec, + #[serde(default)] + pub node_classes: Vec, + #[serde(default)] + pub match_labels: HashMap, +} + +/// Declarative rollout intent for host-level NixOS updates. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct HostDeploymentSpec { + pub name: String, + #[serde(default)] + pub selector: HostDeploymentSelector, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nixos_configuration: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flake_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub batch_size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_unavailable: Option, + #[serde(default)] + pub health_check_command: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub switch_action: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rollback_on_failure: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub drain_before_apply: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reboot_policy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub paused: Option, +} + +/// Controller-observed rollout state for a host deployment. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct HostDeploymentStatus { + #[serde(default)] + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub phase: Option, + #[serde(default)] + pub paused: bool, + #[serde(default)] + pub paused_by_operator: bool, + #[serde(default)] + pub selected_nodes: Vec, + #[serde(default)] + pub completed_nodes: Vec, + #[serde(default)] + pub in_progress_nodes: Vec, + #[serde(default)] + pub failed_nodes: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub updated_at: Option>, +} + /// Service ports for logical service definitions. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServicePorts { @@ -807,6 +961,8 @@ pub struct ClusterStateSpec { #[serde(default)] pub enrollment_rules: Vec, #[serde(default)] + pub host_deployments: Vec, + #[serde(default)] pub services: Vec, #[serde(default)] pub instances: Vec, @@ -1080,19 +1236,92 @@ mod tests { fn test_desired_system_spec_roundtrip() { let desired = DesiredSystemSpec { node_id: "node01".to_string(), + deployment_id: Some("worker-rollout".to_string()), nixos_configuration: Some("node01".to_string()), flake_ref: Some("/opt/plasmacloud-src".to_string()), switch_action: Some("switch".to_string()), health_check_command: vec!["systemctl".to_string(), "is-system-running".to_string()], rollback_on_failure: Some(true), + drain_before_apply: Some(true), }; let json = serde_json::to_string(&desired).unwrap(); let decoded: DesiredSystemSpec = serde_json::from_str(&json).unwrap(); assert_eq!(decoded.node_id, "node01"); + assert_eq!(decoded.deployment_id.as_deref(), Some("worker-rollout")); assert_eq!(decoded.nixos_configuration.as_deref(), Some("node01")); assert_eq!(decoded.health_check_command.len(), 2); assert_eq!(decoded.rollback_on_failure, Some(true)); + assert_eq!(decoded.drain_before_apply, Some(true)); + } + + #[test] + fn test_host_deployment_roundtrip() { + let spec = HostDeploymentSpec { + name: "worker-rollout".to_string(), + selector: HostDeploymentSelector { + node_ids: vec![], + roles: vec!["worker".to_string()], + pools: vec!["general".to_string()], + node_classes: vec!["worker-linux".to_string()], + match_labels: HashMap::from([("tier".to_string(), "general".to_string())]), + }, + nixos_configuration: Some("worker-golden".to_string()), + flake_ref: Some("/opt/plasmacloud-src".to_string()), + batch_size: Some(1), + max_unavailable: Some(1), + health_check_command: vec!["true".to_string()], + switch_action: Some("boot".to_string()), + rollback_on_failure: Some(true), + drain_before_apply: Some(true), + reboot_policy: Some("always".to_string()), + paused: Some(false), + }; + + let json = serde_json::to_string(&spec).unwrap(); + let decoded: HostDeploymentSpec = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.name, "worker-rollout"); + assert_eq!(decoded.batch_size, Some(1)); + assert_eq!(decoded.max_unavailable, Some(1)); + assert_eq!(decoded.selector.roles, vec!["worker".to_string()]); + assert_eq!( + decoded.selector.match_labels.get("tier").map(String::as_str), + Some("general") + ); + assert_eq!(decoded.drain_before_apply, Some(true)); + } + + #[test] + fn test_cluster_node_record_commissioning_roundtrip() { + let node = ClusterNodeRecord { + node_id: "node01".to_string(), + machine_id: Some("machine-01".to_string()), + ip: "10.0.0.11".to_string(), + hostname: "node01".to_string(), + roles: vec!["worker".to_string()], + labels: HashMap::new(), + pool: Some("general".to_string()), + node_class: Some("worker-linux".to_string()), + failure_domain: Some("rack-a".to_string()), + nix_profile: Some("profiles/worker-linux".to_string()), + install_plan: None, + hardware_facts: None, + state: Some("provisioning".to_string()), + commission_state: Some(CommissionState::Commissioned), + install_state: Some(InstallState::Installed), + commissioned_at: Some(Utc::now()), + last_inventory_hash: Some("abc123".to_string()), + power_state: Some(PowerState::On), + bmc_ref: Some("redfish://lab-rack-a/node01".to_string()), + last_heartbeat: Some(Utc::now()), + }; + + let json = serde_json::to_string(&node).unwrap(); + let decoded: ClusterNodeRecord = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.commission_state, Some(CommissionState::Commissioned)); + assert_eq!(decoded.install_state, Some(InstallState::Installed)); + assert_eq!(decoded.power_state, Some(PowerState::On)); + assert_eq!(decoded.bmc_ref.as_deref(), Some("redfish://lab-rack-a/node01")); } #[test] diff --git a/deployer/crates/fleet-scheduler/src/main.rs b/deployer/crates/fleet-scheduler/src/main.rs index 1271392..7fcbbf3 100644 --- a/deployer/crates/fleet-scheduler/src/main.rs +++ b/deployer/crates/fleet-scheduler/src/main.rs @@ -899,6 +899,12 @@ mod tests { install_plan: None, hardware_facts: None, state: Some("active".to_string()), + commission_state: None, + install_state: None, + commissioned_at: None, + last_inventory_hash: None, + power_state: None, + bmc_ref: None, last_heartbeat: Some(Utc::now() - ChronoDuration::seconds(10)), } } diff --git a/deployer/crates/nix-agent/src/main.rs b/deployer/crates/nix-agent/src/main.rs index dd0d433..e375eec 100644 --- a/deployer/crates/nix-agent/src/main.rs +++ b/deployer/crates/nix-agent/src/main.rs @@ -2,6 +2,7 @@ use std::fs; use std::path::Path; use std::process::Stdio; use std::time::Duration; +use std::time::Instant; use anyhow::{anyhow, Context, Result}; use chainfire_client::Client; @@ -135,7 +136,15 @@ impl Agent { } async fn tick(&self) -> Result<()> { + info!( + endpoint = %self.endpoint, + cluster_namespace = %self.cluster_namespace, + cluster_id = %self.cluster_id, + node_id = %self.node_id, + "starting reconciliation tick" + ); let mut client = Client::connect(self.endpoint.clone()).await?; + info!("connected to ChainFire"); let node_key = key_node(&self.cluster_namespace, &self.cluster_id, &self.node_id); let node_raw = client.get_with_revision(&node_key).await?; let Some((node_bytes, _revision)) = node_raw else { @@ -149,6 +158,11 @@ impl Agent { let node: ClusterNodeRecord = serde_json::from_slice(&node_bytes).context("failed to parse node record")?; + info!( + hostname = %node.hostname, + state = node.state.as_deref().unwrap_or("unknown"), + "loaded node record" + ); let desired = client .get(key_desired_system( @@ -160,6 +174,11 @@ impl Agent { .map(|bytes| serde_json::from_slice::(&bytes)) .transpose() .context("failed to parse desired-system spec")?; + info!( + has_desired_system = desired.is_some(), + has_install_plan = node.install_plan.is_some(), + "resolved desired-state inputs" + ); let previous_observed = client .get(key_observed_system( @@ -173,24 +192,87 @@ impl Agent { .context("failed to parse observed-system state")?; let mut observed = self.base_observed_state(&node); + observed.status = Some("planning".to_string()); + info!( + current_system = observed.current_system.as_deref().unwrap_or(""), + configured_system = observed.configured_system.as_deref().unwrap_or(""), + booted_system = observed.booted_system.as_deref().unwrap_or(""), + "publishing planning status" + ); + self.publish_observed_state(&mut client, &observed).await?; let reconcile_result = self - .reconcile_node(&node, desired.as_ref(), previous_observed.as_ref(), &mut observed) + .reconcile_node( + &node, + desired.as_ref(), + previous_observed.as_ref(), + &mut observed, + ) .await; if let Err(error) = reconcile_result { observed.status = Some("failed".to_string()); - observed.last_error = Some(error.to_string()); + observed.last_error = Some(format!("{error:#}")); } + info!( + status = observed.status.as_deref().unwrap_or("unknown"), + "publishing final observed status" + ); + self.publish_observed_state_with_retry(&observed).await?; + + Ok(()) + } + + async fn publish_observed_state( + &self, + client: &mut Client, + observed: &ObservedSystemState, + ) -> Result<()> { + info!( + status = observed.status.as_deref().unwrap_or("unknown"), + "writing observed-system state" + ); client .put( &key_observed_system(&self.cluster_namespace, &self.cluster_id, &self.node_id), - &serde_json::to_vec(&observed)?, + &serde_json::to_vec(observed)?, ) .await?; - Ok(()) } + async fn publish_observed_state_with_retry( + &self, + observed: &ObservedSystemState, + ) -> Result<()> { + let payload = serde_json::to_vec(observed)?; + let key = key_observed_system(&self.cluster_namespace, &self.cluster_id, &self.node_id); + let deadline = Instant::now() + Duration::from_secs(30); + let mut attempt = 1u32; + + loop { + let result = async { + let mut client = Client::connect(self.endpoint.clone()).await?; + client.put(&key, &payload).await?; + Result::<()>::Ok(()) + } + .await; + + match result { + Ok(()) => return Ok(()), + Err(error) if Instant::now() < deadline => { + warn!( + attempt, + error = %error, + "failed to publish observed-system state; retrying with a fresh connection" + ); + attempt += 1; + sleep(Duration::from_secs(2)).await; + } + Err(error) => return Err(error), + } + } + } + fn base_observed_state(&self, node: &ClusterNodeRecord) -> ObservedSystemState { ObservedSystemState { node_id: node.node_id.clone(), @@ -209,7 +291,18 @@ impl Agent { observed: &mut ObservedSystemState, ) -> Result<()> { match node.state.as_deref() { - Some("failed") | Some("draining") => { + Some("failed") => { + observed.status = Some("paused".to_string()); + return Ok(()); + } + Some("draining") + if !desired + .map(|spec| { + spec.deployment_id.is_some() + && spec.drain_before_apply.unwrap_or(false) + }) + .unwrap_or(false) => + { observed.status = Some("paused".to_string()); return Ok(()); } @@ -227,6 +320,14 @@ impl Agent { observed.status = Some("idle".to_string()); return Ok(()); }; + info!( + nixos_configuration = %desired.nixos_configuration, + flake_ref = %desired.flake_ref, + switch_action = %desired.switch_action, + rollback_on_failure = desired.rollback_on_failure, + health_check_command = ?desired.health_check_command, + "resolved desired system" + ); observed.nixos_configuration = Some(desired.nixos_configuration.clone()); observed.flake_root = Some(desired.flake_ref.clone()); @@ -236,6 +337,10 @@ impl Agent { .and_then(|state| state.rollback_system.clone()) .or_else(|| observed.current_system.clone()); observed.rollback_system = previous_system.clone(); + info!( + previous_system = previous_system.as_deref().unwrap_or(""), + "selected rollback baseline" + ); let target_system = self .build_target_system(&desired.flake_ref, &desired.nixos_configuration) .await @@ -246,8 +351,10 @@ impl Agent { ) })?; observed.target_system = Some(target_system.clone()); + info!(target_system = %target_system, "built target system"); if observed.current_system.as_deref() == Some(target_system.as_str()) { + info!("target system already active"); if should_run_post_boot_health_check(previous_observed, &desired, &target_system) { observed.status = Some("verifying".to_string()); observed.last_attempt = Some(Utc::now()); @@ -279,8 +386,14 @@ impl Agent { observed.status = Some("reconciling".to_string()); observed.last_attempt = Some(Utc::now()); + info!( + target_system = %target_system, + switch_action = %desired.switch_action, + "switching to target system" + ); self.switch_to_target(&target_system, &desired.switch_action) .await?; + info!("switch-to-configuration completed"); observed.configured_system = read_symlink_target("/nix/var/nix/profiles/system"); observed.current_system = read_symlink_target("/run/current-system"); @@ -327,15 +440,20 @@ impl Agent { async fn build_target_system(&self, flake_ref: &str, configuration: &str) -> Result { let flake_attr = target_flake_attr(flake_ref, configuration); - let output = run_command( - "nix", - &["build", "--no-link", "--print-out-paths", flake_attr.as_str()], - ) - .await?; + info!(flake_attr = %flake_attr, "building target system"); + let mut build_args = vec![ + "build", + "-L", + "--no-link", + "--no-write-lock-file", + "--print-out-paths", + ]; + build_args.push(flake_attr.as_str()); + let output = run_command("nix", &build_args).await?; let path = output .lines() - .find(|line| !line.trim().is_empty()) .map(str::trim) + .find(|line| line.starts_with("/nix/store/")) .ok_or_else(|| anyhow!("nix build returned no output path"))?; Ok(path.to_string()) } @@ -349,7 +467,12 @@ impl Agent { )); } - run_command( + info!( + switch_bin = %switch_bin.display(), + switch_action = %switch_action, + "executing switch-to-configuration" + ); + run_command_inherit_output( switch_bin .to_str() .ok_or_else(|| anyhow!("invalid switch path"))?, @@ -369,9 +492,15 @@ impl Agent { return Ok(HealthCheckOutcome::Passed); } + info!( + command = ?desired.health_check_command, + rollback_on_failure = desired.rollback_on_failure, + "running post-activation health check" + ); if let Err(error) = run_vec_command(&desired.health_check_command).await { let error_message = format!("health check failed after activation: {error}"); if desired.rollback_on_failure { + info!("health check failed; rolling back to previous system"); self.rollback_to_previous(previous_system).await?; observed.configured_system = read_symlink_target("/nix/var/nix/profiles/system"); observed.current_system = read_symlink_target("/run/current-system"); @@ -385,6 +514,7 @@ impl Agent { return Err(anyhow!(error_message)); } + info!("post-activation health check passed"); Ok(HealthCheckOutcome::Passed) } @@ -392,7 +522,42 @@ impl Agent { let previous_system = previous_system .filter(|value| !value.is_empty()) .ok_or_else(|| anyhow!("rollback requested but no previous system is known"))?; - self.switch_to_target(previous_system, "switch").await + info!(previous_system = %previous_system, "rolling back to previous system"); + let switch_bin = Path::new(previous_system).join("bin/switch-to-configuration"); + if switch_bin.exists() { + return self.switch_to_target(previous_system, "switch").await; + } + + let activate = Path::new(previous_system).join("activate"); + if !activate.exists() { + return Err(anyhow!( + "previous system {} does not contain switch-to-configuration or activate", + previous_system + )); + } + + info!( + previous_system = %previous_system, + activate = %activate.display(), + "previous system lacks switch-to-configuration; falling back to profile set + activate" + ); + run_command( + "nix-env", + &[ + "--profile", + "/nix/var/nix/profiles/system", + "--set", + previous_system, + ], + ) + .await?; + run_command_inherit_output( + activate + .to_str() + .ok_or_else(|| anyhow!("invalid activate path"))?, + &[], + ) + .await } } @@ -458,6 +623,8 @@ fn read_symlink_target(path: &str) -> Option { } async fn run_command(program: &str, args: &[&str]) -> Result { + let started_at = Instant::now(); + info!(program = %program, args = ?args, "running command"); let output = Command::new(program) .args(args) .stdin(Stdio::null()) @@ -468,10 +635,25 @@ async fn run_command(program: &str, args: &[&str]) -> Result { .with_context(|| format!("failed to execute {}", program))?; if output.status.success() { + info!( + program = %program, + args = ?args, + elapsed_ms = started_at.elapsed().as_millis(), + "command completed successfully" + ); Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } else { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + warn!( + program = %program, + args = ?args, + elapsed_ms = started_at.elapsed().as_millis(), + status = %output.status, + stdout = %stdout, + stderr = %stderr, + "command failed" + ); Err(anyhow!( "{} {:?} failed with status {}: stdout='{}' stderr='{}'", program, @@ -491,6 +673,47 @@ async fn run_vec_command(command: &[String]) -> Result { run_command(program, &arg_refs).await } +async fn run_command_inherit_output(program: &str, args: &[&str]) -> Result<()> { + let started_at = Instant::now(); + info!( + program = %program, + args = ?args, + "running command with inherited output" + ); + let status = Command::new(program) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .await + .with_context(|| format!("failed to execute {}", program))?; + + if status.success() { + info!( + program = %program, + args = ?args, + elapsed_ms = started_at.elapsed().as_millis(), + "command completed successfully" + ); + Ok(()) + } else { + warn!( + program = %program, + args = ?args, + elapsed_ms = started_at.elapsed().as_millis(), + status = %status, + "command failed" + ); + Err(anyhow!( + "{} {:?} failed with status {}", + program, + args, + status + )) + } +} + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() @@ -543,6 +766,12 @@ mod tests { }), hardware_facts: None, state: Some("active".to_string()), + commission_state: None, + install_state: None, + commissioned_at: None, + last_inventory_hash: None, + power_state: None, + bmc_ref: None, last_heartbeat: None, } } @@ -568,11 +797,13 @@ mod tests { fn resolve_desired_system_prefers_chainfire_spec() { let desired = DesiredSystemSpec { node_id: "node01".to_string(), + deployment_id: None, nixos_configuration: Some("node01-next".to_string()), flake_ref: Some("github:centra/cloud".to_string()), switch_action: Some("boot".to_string()), health_check_command: vec!["true".to_string()], rollback_on_failure: Some(true), + drain_before_apply: Some(false), }; let resolved = resolve_desired_system( @@ -595,11 +826,13 @@ mod tests { fn resolve_desired_system_uses_local_health_check_defaults_when_spec_omits_them() { let desired = DesiredSystemSpec { node_id: "node01".to_string(), + deployment_id: None, nixos_configuration: Some("node01-next".to_string()), flake_ref: None, switch_action: None, health_check_command: Vec::new(), rollback_on_failure: None, + drain_before_apply: None, }; let resolved = resolve_desired_system( @@ -631,7 +864,10 @@ mod tests { #[test] fn read_symlink_target_returns_none_for_missing_path() { - assert_eq!(read_symlink_target("/tmp/photoncloud-nix-agent-missing-link"), None); + assert_eq!( + read_symlink_target("/tmp/photoncloud-nix-agent-missing-link"), + None + ); } #[test] diff --git a/deployer/crates/plasmacloud-reconciler/Cargo.toml b/deployer/crates/plasmacloud-reconciler/Cargo.toml index c2c6a5a..ea1ee1d 100644 --- a/deployer/crates/plasmacloud-reconciler/Cargo.toml +++ b/deployer/crates/plasmacloud-reconciler/Cargo.toml @@ -9,6 +9,8 @@ repository.workspace = true [dependencies] anyhow.workspace = true +chainfire-client.workspace = true +chrono.workspace = true serde.workspace = true serde_json.workspace = true tokio.workspace = true @@ -16,5 +18,6 @@ tracing.workspace = true tracing-subscriber.workspace = true fiberlb-api.workspace = true flashdns-api.workspace = true +deployer-types.workspace = true clap = { version = "4.5", features = ["derive"] } tonic = "0.12" diff --git a/deployer/crates/plasmacloud-reconciler/src/hosts.rs b/deployer/crates/plasmacloud-reconciler/src/hosts.rs new file mode 100644 index 0000000..9afe4ec --- /dev/null +++ b/deployer/crates/plasmacloud-reconciler/src/hosts.rs @@ -0,0 +1,823 @@ +use anyhow::Result; +use chainfire_client::Client; +use chrono::Utc; +use clap::Args; +use deployer_types::{ + ClusterNodeRecord, CommissionState, DesiredSystemSpec, HostDeploymentSelector, + HostDeploymentSpec, HostDeploymentStatus, InstallState, ObservedSystemState, ServiceInstanceSpec, +}; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::time::Duration; +use tokio::time::sleep; +use tracing::{info, warn}; + +fn cluster_prefix(cluster_namespace: &str, cluster_id: &str) -> String { + format!("{}/clusters/{}/", cluster_namespace, cluster_id) +} + +fn key_node(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec { + format!( + "{}nodes/{}", + cluster_prefix(cluster_namespace, cluster_id), + node_id + ) + .into_bytes() +} + +fn key_desired_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec { + format!( + "{}nodes/{}/desired-system", + cluster_prefix(cluster_namespace, cluster_id), + node_id + ) + .into_bytes() +} + +fn key_host_deployment_status( + cluster_namespace: &str, + cluster_id: &str, + deployment_name: &str, +) -> Vec { + format!( + "{}deployments/hosts/{}/status", + cluster_prefix(cluster_namespace, cluster_id), + deployment_name + ) + .into_bytes() +} + +#[derive(Debug, Clone, Args)] +pub struct HostsCommand { + #[arg(long)] + pub endpoint: String, + + #[arg(long, default_value = "photoncloud")] + pub cluster_namespace: String, + + #[arg(long)] + pub cluster_id: String, + + #[arg(long, default_value_t = 15)] + pub interval_secs: u64, + + #[arg(long, default_value_t = 300)] + pub heartbeat_timeout_secs: u64, + + #[arg(long, default_value_t = false)] + pub dry_run: bool, + + #[arg(long, default_value_t = false)] + pub once: bool, +} + +pub async fn run(command: HostsCommand) -> Result<()> { + let controller = HostDeploymentController::new(command); + if controller.once { + controller.reconcile_once().await + } else { + loop { + if let Err(error) = controller.reconcile_once().await { + warn!(error = %error, "host deployment reconciliation failed"); + } + sleep(controller.interval).await; + } + } +} + +struct HostDeploymentController { + endpoint: String, + cluster_namespace: String, + cluster_id: String, + interval: Duration, + heartbeat_timeout_secs: u64, + dry_run: bool, + once: bool, +} + +impl HostDeploymentController { + fn new(command: HostsCommand) -> Self { + Self { + endpoint: command.endpoint, + cluster_namespace: command.cluster_namespace, + cluster_id: command.cluster_id, + interval: Duration::from_secs(command.interval_secs), + heartbeat_timeout_secs: command.heartbeat_timeout_secs, + dry_run: command.dry_run, + once: command.once, + } + } + + async fn reconcile_once(&self) -> Result<()> { + let mut client = Client::connect(self.endpoint.clone()).await?; + let nodes = self.load_nodes(&mut client).await?; + let desired_systems = self.load_desired_systems(&mut client).await?; + let observed_systems = self.load_observed_systems(&mut client).await?; + let instances = self.load_instances(&mut client).await?; + let deployments = self.load_host_deployments(&mut client).await?; + let statuses = self.load_host_deployment_statuses(&mut client).await?; + + info!( + nodes = nodes.len(), + deployments = deployments.len(), + instances = instances.len(), + "loaded host deployment inputs" + ); + + for deployment in deployments { + let existing_status = statuses.get(&deployment.name).cloned(); + let plan = plan_host_deployment( + &deployment, + existing_status.as_ref(), + &nodes, + &desired_systems, + &observed_systems, + &instances, + self.heartbeat_timeout_secs, + ); + + if self.dry_run { + info!( + deployment = %deployment.name, + phase = plan.status.phase.as_deref().unwrap_or("unknown"), + desired_upserts = plan.desired_upserts.len(), + desired_deletes = plan.desired_deletes.len(), + node_updates = plan.node_updates.len(), + "would reconcile host deployment" + ); + continue; + } + + for desired in &plan.desired_upserts { + client + .put( + &key_desired_system( + &self.cluster_namespace, + &self.cluster_id, + &desired.node_id, + ), + &serde_json::to_vec(desired)?, + ) + .await?; + } + + for node_id in &plan.desired_deletes { + client + .delete(&key_desired_system( + &self.cluster_namespace, + &self.cluster_id, + node_id, + )) + .await?; + } + + for node in plan.node_updates.values() { + client + .put( + &key_node(&self.cluster_namespace, &self.cluster_id, &node.node_id), + &serde_json::to_vec(node)?, + ) + .await?; + } + + client + .put( + &key_host_deployment_status( + &self.cluster_namespace, + &self.cluster_id, + &deployment.name, + ), + &serde_json::to_vec(&plan.status)?, + ) + .await?; + } + + Ok(()) + } + + async fn load_nodes(&self, client: &mut Client) -> Result> { + let prefix = format!( + "{}nodes/", + cluster_prefix(&self.cluster_namespace, &self.cluster_id) + ); + let kvs = client.get_prefix(prefix.as_bytes()).await?; + let mut nodes = Vec::new(); + + for (key, value) in kvs { + let key = String::from_utf8_lossy(&key); + let Some(suffix) = key.strip_prefix(&prefix) else { + continue; + }; + if suffix.contains('/') { + continue; + } + match serde_json::from_slice::(&value) { + Ok(node) => nodes.push(node), + Err(error) => warn!(error = %error, key = %key, "failed to decode cluster node"), + } + } + + nodes.sort_by(|lhs, rhs| lhs.node_id.cmp(&rhs.node_id)); + Ok(nodes) + } + + async fn load_desired_systems( + &self, + client: &mut Client, + ) -> Result> { + let prefix = format!( + "{}nodes/", + cluster_prefix(&self.cluster_namespace, &self.cluster_id) + ); + let kvs = client.get_prefix(prefix.as_bytes()).await?; + let mut desired = HashMap::new(); + + for (key, value) in kvs { + let key = String::from_utf8_lossy(&key); + if !key.ends_with("/desired-system") { + continue; + } + match serde_json::from_slice::(&value) { + Ok(spec) => { + desired.insert(spec.node_id.clone(), spec); + } + Err(error) => warn!(error = %error, key = %key, "failed to decode desired-system"), + } + } + + Ok(desired) + } + + async fn load_observed_systems( + &self, + client: &mut Client, + ) -> Result> { + let prefix = format!( + "{}nodes/", + cluster_prefix(&self.cluster_namespace, &self.cluster_id) + ); + let kvs = client.get_prefix(prefix.as_bytes()).await?; + let mut observed = HashMap::new(); + + for (key, value) in kvs { + let key = String::from_utf8_lossy(&key); + if !key.ends_with("/observed-system") { + continue; + } + match serde_json::from_slice::(&value) { + Ok(state) => { + observed.insert(state.node_id.clone(), state); + } + Err(error) => warn!(error = %error, key = %key, "failed to decode observed-system"), + } + } + + Ok(observed) + } + + async fn load_instances(&self, client: &mut Client) -> Result> { + let prefix = format!( + "{}instances/", + cluster_prefix(&self.cluster_namespace, &self.cluster_id) + ); + let kvs = client.get_prefix(prefix.as_bytes()).await?; + let mut instances = Vec::new(); + + for (key, value) in kvs { + let key = String::from_utf8_lossy(&key); + match serde_json::from_slice::(&value) { + Ok(instance) => instances.push(instance), + Err(error) => warn!(error = %error, key = %key, "failed to decode service instance"), + } + } + + Ok(instances) + } + + async fn load_host_deployments(&self, client: &mut Client) -> Result> { + let prefix = format!( + "{}deployments/hosts/", + cluster_prefix(&self.cluster_namespace, &self.cluster_id) + ); + let kvs = client.get_prefix(prefix.as_bytes()).await?; + let mut deployments = Vec::new(); + + for (key, value) in kvs { + let key = String::from_utf8_lossy(&key); + if !key.ends_with("/spec") { + continue; + } + match serde_json::from_slice::(&value) { + Ok(spec) => deployments.push(spec), + Err(error) => warn!(error = %error, key = %key, "failed to decode host deployment"), + } + } + + deployments.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + Ok(deployments) + } + + async fn load_host_deployment_statuses( + &self, + client: &mut Client, + ) -> Result> { + let prefix = format!( + "{}deployments/hosts/", + cluster_prefix(&self.cluster_namespace, &self.cluster_id) + ); + let kvs = client.get_prefix(prefix.as_bytes()).await?; + let mut statuses = HashMap::new(); + + for (key, value) in kvs { + let key = String::from_utf8_lossy(&key); + if !key.ends_with("/status") { + continue; + } + match serde_json::from_slice::(&value) { + Ok(status) => { + statuses.insert(status.name.clone(), status); + } + Err(error) => warn!(error = %error, key = %key, "failed to decode host deployment status"), + } + } + + Ok(statuses) + } +} + +#[derive(Debug, Default)] +struct HostDeploymentPlan { + status: HostDeploymentStatus, + desired_upserts: Vec, + desired_deletes: Vec, + node_updates: BTreeMap, +} + +fn plan_host_deployment( + deployment: &HostDeploymentSpec, + existing_status: Option<&HostDeploymentStatus>, + nodes: &[ClusterNodeRecord], + desired_systems: &HashMap, + observed_systems: &HashMap, + instances: &[ServiceInstanceSpec], + heartbeat_timeout_secs: u64, +) -> HostDeploymentPlan { + let now = Utc::now(); + let target_configuration = deployment.nixos_configuration.clone(); + let selector_matches = select_nodes(nodes, &deployment.selector); + let selected_node_ids = selector_matches + .iter() + .map(|node| node.node_id.clone()) + .collect::>(); + let instance_counts = active_instances_per_node(instances); + let mut completed = Vec::new(); + let mut in_progress = Vec::new(); + let mut failed = Vec::new(); + let mut eligible_candidates = Vec::new(); + let mut desired_upserts = Vec::new(); + let mut node_updates = BTreeMap::new(); + let batch_size = deployment.batch_size.unwrap_or(1).max(1) as usize; + let max_unavailable = deployment.max_unavailable.unwrap_or(1).max(1) as usize; + let operator_paused = existing_status + .map(|status| status.paused_by_operator) + .unwrap_or(false); + let spec_paused = deployment.paused.unwrap_or(false); + let mut desired_deletes = desired_systems + .iter() + .filter(|(node_id, desired)| { + desired.deployment_id.as_deref() == Some(deployment.name.as_str()) + && !selected_node_ids.contains(node_id.as_str()) + }) + .map(|(node_id, _)| node_id.clone()) + .collect::>(); + + for node in &selector_matches { + let desired = desired_systems.get(&node.node_id); + let observed = observed_systems.get(&node.node_id); + let is_completed = + is_node_completed(deployment, node, desired, observed, target_configuration.as_deref()); + let is_failed = is_node_failed(deployment, desired, observed); + let is_in_progress = is_node_in_progress(deployment, desired, observed, is_completed, is_failed) + || (deployment.drain_before_apply == Some(true) + && node.state.as_deref() == Some("draining") + && instance_counts.get(&node.node_id).copied().unwrap_or_default() > 0); + + if is_completed { + completed.push(node.node_id.clone()); + if deployment.drain_before_apply == Some(true) && node.state.as_deref() == Some("draining") + { + let mut updated = (*node).clone(); + updated.state = Some("active".to_string()); + node_updates.insert(updated.node_id.clone(), updated); + } + continue; + } + + if is_failed { + failed.push(node.node_id.clone()); + continue; + } + + if is_in_progress { + in_progress.push(node.node_id.clone()); + continue; + } + + if node_is_rollout_candidate(node, heartbeat_timeout_secs) { + eligible_candidates.push((*node).clone()); + } + } + + let unavailable = in_progress.len() + failed.len(); + let paused = operator_paused || spec_paused || !failed.is_empty(); + let remaining_unavailable_budget = max_unavailable.saturating_sub(unavailable); + let remaining_batch_budget = batch_size.saturating_sub(in_progress.len()); + let max_starts = if deployment.nixos_configuration.is_some() { + remaining_unavailable_budget.min(remaining_batch_budget) + } else { + 0 + }; + let mut planned = 0usize; + let mut newly_started = Vec::new(); + + if !paused && max_starts > 0 { + for node in eligible_candidates { + if planned >= max_starts { + break; + } + + let remaining_instances = instance_counts.get(&node.node_id).copied().unwrap_or_default(); + if deployment.drain_before_apply == Some(true) && remaining_instances > 0 { + let mut updated = node.clone(); + updated.state = Some("draining".to_string()); + node_updates.insert(updated.node_id.clone(), updated); + in_progress.push(node.node_id.clone()); + newly_started.push(node.node_id.clone()); + planned += 1; + continue; + } + + let desired = DesiredSystemSpec { + node_id: node.node_id.clone(), + deployment_id: Some(deployment.name.clone()), + nixos_configuration: deployment.nixos_configuration.clone(), + flake_ref: deployment.flake_ref.clone(), + switch_action: deployment.switch_action.clone().or_else(|| Some("switch".to_string())), + health_check_command: deployment.health_check_command.clone(), + rollback_on_failure: Some(deployment.rollback_on_failure.unwrap_or(true)), + drain_before_apply: Some(deployment.drain_before_apply.unwrap_or(false)), + }; + newly_started.push(node.node_id.clone()); + in_progress.push(node.node_id.clone()); + planned += 1; + if deployment.drain_before_apply == Some(true) && node.state.as_deref() != Some("draining") + { + let mut updated = node.clone(); + updated.state = Some("draining".to_string()); + node_updates.insert(updated.node_id.clone(), updated); + } + desired_upserts.push(desired); + } + } + + let mut status = existing_status.cloned().unwrap_or_default(); + status.name = deployment.name.clone(); + status.selected_nodes = selector_matches.iter().map(|node| node.node_id.clone()).collect(); + status.completed_nodes = dedup_sorted(completed); + status.in_progress_nodes = dedup_sorted(in_progress); + status.failed_nodes = dedup_sorted(failed); + status.paused_by_operator = operator_paused; + status.paused = paused; + status.phase = Some(if status.selected_nodes.is_empty() { + "idle" + } else if deployment.nixos_configuration.is_none() { + "invalid" + } else if status.paused { + "paused" + } else if status.completed_nodes.len() == status.selected_nodes.len() { + "completed" + } else if !newly_started.is_empty() || !status.in_progress_nodes.is_empty() { + "running" + } else { + "ready" + } + .to_string()); + status.message = Some(format!( + "selected={} completed={} in_progress={} failed={} newly_started={}", + status.selected_nodes.len(), + status.completed_nodes.len(), + status.in_progress_nodes.len(), + status.failed_nodes.len(), + newly_started.len() + )); + status.updated_at = Some(now); + + HostDeploymentPlan { + status, + desired_upserts, + desired_deletes: { + desired_deletes.sort(); + desired_deletes.dedup(); + desired_deletes + }, + node_updates, + } +} + +fn select_nodes<'a>( + nodes: &'a [ClusterNodeRecord], + selector: &HostDeploymentSelector, +) -> Vec<&'a ClusterNodeRecord> { + let explicit_nodes = selector.node_ids.iter().collect::>(); + let explicit_mode = !explicit_nodes.is_empty(); + let mut selected = nodes + .iter() + .filter(|node| { + (!explicit_mode || explicit_nodes.contains(&node.node_id)) + && (selector.roles.is_empty() + || node + .roles + .iter() + .any(|role| selector.roles.iter().any(|expected| expected == role))) + && (selector.pools.is_empty() + || node + .pool + .as_deref() + .map(|pool| selector.pools.iter().any(|expected| expected == pool)) + .unwrap_or(false)) + && (selector.node_classes.is_empty() + || node + .node_class + .as_deref() + .map(|node_class| { + selector + .node_classes + .iter() + .any(|expected| expected == node_class) + }) + .unwrap_or(false)) + && selector + .match_labels + .iter() + .all(|(key, value)| node.labels.get(key) == Some(value)) + }) + .collect::>(); + selected.sort_by(|lhs, rhs| lhs.node_id.cmp(&rhs.node_id)); + selected +} + +fn active_instances_per_node(instances: &[ServiceInstanceSpec]) -> HashMap { + let mut counts = HashMap::new(); + for instance in instances { + if matches!(instance.state.as_deref(), Some("failed") | Some("deleted")) { + continue; + } + *counts.entry(instance.node_id.clone()).or_insert(0usize) += 1; + } + counts +} + +fn node_is_rollout_candidate(node: &ClusterNodeRecord, heartbeat_timeout_secs: u64) -> bool { + if matches!( + node.commission_state, + Some(CommissionState::Discovered | CommissionState::Commissioning) + ) { + return false; + } + if matches!( + node.install_state, + Some( + InstallState::Installing | InstallState::Failed | InstallState::ReinstallRequested + ) + ) { + return false; + } + if !matches!(node.state.as_deref(), Some("active") | Some("draining")) { + return false; + } + if heartbeat_timeout_secs == 0 { + return true; + } + let Some(last) = node.last_heartbeat else { + return true; + }; + Utc::now().signed_duration_since(last).num_seconds() <= heartbeat_timeout_secs as i64 +} + +fn is_node_completed( + deployment: &HostDeploymentSpec, + _node: &ClusterNodeRecord, + desired: Option<&DesiredSystemSpec>, + observed: Option<&ObservedSystemState>, + target_configuration: Option<&str>, +) -> bool { + observed + .filter(|observed| observed.status.as_deref() == Some("active")) + .and_then(|observed| observed.nixos_configuration.as_deref()) + .zip(target_configuration) + .map(|(observed_configuration, target)| observed_configuration == target) + .unwrap_or(false) + && desired + .and_then(|desired| desired.deployment_id.as_deref()) + .map(|deployment_id| deployment_id == deployment.name) + .unwrap_or(false) +} + +fn is_node_failed( + deployment: &HostDeploymentSpec, + desired: Option<&DesiredSystemSpec>, + observed: Option<&ObservedSystemState>, +) -> bool { + desired + .and_then(|desired| desired.deployment_id.as_deref()) + .map(|deployment_id| deployment_id == deployment.name) + .unwrap_or(false) + && observed + .and_then(|observed| observed.status.as_deref()) + .map(|status| matches!(status, "failed" | "rolled-back")) + .unwrap_or(false) +} + +fn is_node_in_progress( + deployment: &HostDeploymentSpec, + desired: Option<&DesiredSystemSpec>, + observed: Option<&ObservedSystemState>, + is_completed: bool, + is_failed: bool, +) -> bool { + if is_completed || is_failed { + return false; + } + desired + .and_then(|desired| desired.deployment_id.as_deref()) + .map(|deployment_id| deployment_id == deployment.name) + .unwrap_or(false) + || observed + .and_then(|observed| observed.status.as_deref()) + .map(|status| matches!(status, "planning" | "pending" | "reconciling" | "verifying" | "staged")) + .unwrap_or(false) +} + +fn dedup_sorted(mut values: Vec) -> Vec { + values.sort(); + values.dedup(); + values +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_node(node_id: &str, failure_domain: &str) -> ClusterNodeRecord { + ClusterNodeRecord { + node_id: node_id.to_string(), + machine_id: None, + ip: "10.0.0.1".to_string(), + hostname: node_id.to_string(), + roles: vec!["worker".to_string()], + labels: HashMap::from([ + ("tier".to_string(), "general".to_string()), + ("failure_domain".to_string(), failure_domain.to_string()), + ]), + pool: Some("general".to_string()), + node_class: Some("worker-linux".to_string()), + failure_domain: Some(failure_domain.to_string()), + nix_profile: None, + install_plan: None, + hardware_facts: None, + state: Some("active".to_string()), + commission_state: Some(CommissionState::Commissioned), + install_state: Some(InstallState::Installed), + commissioned_at: None, + last_inventory_hash: None, + power_state: None, + bmc_ref: None, + last_heartbeat: Some(Utc::now()), + } + } + + fn test_deployment() -> HostDeploymentSpec { + HostDeploymentSpec { + name: "worker-rollout".to_string(), + selector: HostDeploymentSelector { + node_ids: vec![], + roles: vec!["worker".to_string()], + pools: vec!["general".to_string()], + node_classes: vec!["worker-linux".to_string()], + match_labels: HashMap::from([("tier".to_string(), "general".to_string())]), + }, + nixos_configuration: Some("worker-golden".to_string()), + flake_ref: Some("/opt/plasmacloud-src".to_string()), + batch_size: Some(1), + max_unavailable: Some(1), + health_check_command: vec!["true".to_string()], + switch_action: Some("switch".to_string()), + rollback_on_failure: Some(true), + drain_before_apply: Some(false), + reboot_policy: None, + paused: Some(false), + } + } + + #[test] + fn plan_rollout_starts_one_node_per_batch() { + let deployment = test_deployment(); + let nodes = vec![test_node("node01", "rack-a"), test_node("node02", "rack-b")]; + let plan = plan_host_deployment( + &deployment, + None, + &nodes, + &HashMap::new(), + &HashMap::new(), + &[], + 300, + ); + + assert_eq!(plan.desired_upserts.len(), 1); + assert_eq!(plan.status.in_progress_nodes, vec!["node01".to_string()]); + assert_eq!(plan.status.phase.as_deref(), Some("running")); + } + + #[test] + fn plan_rollout_pauses_on_failed_node() { + let deployment = test_deployment(); + let nodes = vec![test_node("node01", "rack-a"), test_node("node02", "rack-b")]; + let desired = HashMap::from([( + "node01".to_string(), + DesiredSystemSpec { + node_id: "node01".to_string(), + deployment_id: Some("worker-rollout".to_string()), + nixos_configuration: Some("worker-golden".to_string()), + flake_ref: None, + switch_action: Some("switch".to_string()), + health_check_command: Vec::new(), + rollback_on_failure: Some(true), + drain_before_apply: Some(false), + }, + )]); + let observed = HashMap::from([( + "node01".to_string(), + ObservedSystemState { + node_id: "node01".to_string(), + nixos_configuration: Some("worker-golden".to_string()), + status: Some("rolled-back".to_string()), + ..ObservedSystemState::default() + }, + )]); + + let plan = plan_host_deployment( + &deployment, + None, + &nodes, + &desired, + &observed, + &[], + 300, + ); + + assert!(plan.desired_upserts.is_empty()); + assert!(plan.status.paused); + assert_eq!(plan.status.failed_nodes, vec!["node01".to_string()]); + } + + #[test] + fn plan_rollout_drains_before_apply_when_instances_exist() { + let mut deployment = test_deployment(); + deployment.drain_before_apply = Some(true); + let nodes = vec![test_node("node01", "rack-a")]; + let instances = vec![ServiceInstanceSpec { + instance_id: "api-node01".to_string(), + service: "api".to_string(), + node_id: "node01".to_string(), + ip: "10.0.0.1".to_string(), + port: 8080, + mesh_port: None, + version: None, + health_check: None, + process: None, + container: None, + managed_by: Some("fleet-scheduler".to_string()), + state: Some("active".to_string()), + last_heartbeat: None, + observed_at: None, + }]; + + let plan = plan_host_deployment( + &deployment, + None, + &nodes, + &HashMap::new(), + &HashMap::new(), + &instances, + 300, + ); + + assert!(plan.desired_upserts.is_empty()); + assert_eq!( + plan.node_updates + .get("node01") + .and_then(|node| node.state.as_deref()), + Some("draining") + ); + assert_eq!(plan.status.in_progress_nodes, vec!["node01".to_string()]); + } +} diff --git a/deployer/crates/plasmacloud-reconciler/src/main.rs b/deployer/crates/plasmacloud-reconciler/src/main.rs index f94c522..55ebd3d 100644 --- a/deployer/crates/plasmacloud-reconciler/src/main.rs +++ b/deployer/crates/plasmacloud-reconciler/src/main.rs @@ -29,9 +29,9 @@ use fiberlb_api::{ }; use flashdns_api::RecordServiceClient; -use flashdns_api::ReverseZoneServiceClient; use flashdns_api::ZoneServiceClient; use flashdns_api::proto::{ + reverse_zone_service_client::ReverseZoneServiceClient, record_data, ARecord, AaaaRecord, CaaRecord, CnameRecord, CreateRecordRequest, CreateReverseZoneRequest, CreateZoneRequest, DeleteRecordRequest, DeleteReverseZoneRequest, DeleteZoneRequest, ListReverseZonesRequest, MxRecord, NsRecord, PtrRecord, RecordData, @@ -39,6 +39,8 @@ use flashdns_api::proto::{ ZoneInfo, }; +mod hosts; + #[derive(Parser)] #[command(author, version, about)] struct Cli { @@ -71,6 +73,9 @@ enum Command { #[arg(long, default_value_t = false)] prune: bool, }, + + /// Reconcile host deployments into per-node desired-system state + Hosts(hosts::HostsCommand), } #[derive(Debug, Deserialize)] @@ -294,6 +299,9 @@ async fn main() -> Result<()> { let spec: DnsConfig = read_json(&config).await?; reconcile_dns(spec, endpoint, prune).await?; } + Command::Hosts(command) => { + hosts::run(command).await?; + } } Ok(()) diff --git a/deployer/scripts/verify-deployer-bootstrap-e2e.sh b/deployer/scripts/verify-deployer-bootstrap-e2e.sh index d8e2cdd..fa1076b 100755 --- a/deployer/scripts/verify-deployer-bootstrap-e2e.sh +++ b/deployer/scripts/verify-deployer-bootstrap-e2e.sh @@ -7,6 +7,30 @@ if [[ -z "${PHOTONCLOUD_E2E_IN_NIX:-}" ]]; then exec nix develop "$ROOT" -c env PHOTONCLOUD_E2E_IN_NIX=1 bash "$0" "$@" fi +run_chainfire_server_bin() { + if [[ -n "${PHOTONCLOUD_CHAINFIRE_SERVER_BIN:-}" ]]; then + "$PHOTONCLOUD_CHAINFIRE_SERVER_BIN" "$@" + else + cargo run --manifest-path "$ROOT/chainfire/Cargo.toml" -p chainfire-server -- "$@" + fi +} + +run_deployer_server_bin() { + if [[ -n "${PHOTONCLOUD_DEPLOYER_SERVER_BIN:-}" ]]; then + "$PHOTONCLOUD_DEPLOYER_SERVER_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-server -- "$@" + fi +} + +run_deployer_ctl_bin() { + if [[ -n "${PHOTONCLOUD_DEPLOYER_CTL_BIN:-}" ]]; then + "$PHOTONCLOUD_DEPLOYER_CTL_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-ctl -- "$@" + fi +} + tmp_dir="$(mktemp -d)" cf_pid="" deployer_pid="" @@ -128,7 +152,7 @@ role = "voter" EOF echo "Starting ChainFire on 127.0.0.1:${api_port}" -cargo run --manifest-path "$ROOT/chainfire/Cargo.toml" -p chainfire-server -- \ +run_chainfire_server_bin \ --config "$tmp_dir/chainfire.toml" \ >"$tmp_dir/chainfire.log" 2>&1 & cf_pid="$!" @@ -155,7 +179,7 @@ namespace = "deployer" EOF echo "Starting Deployer on 127.0.0.1:${deployer_port}" -cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-server -- \ +run_deployer_server_bin \ --config "$tmp_dir/deployer.toml" \ >"$tmp_dir/deployer.log" 2>&1 & deployer_pid="$!" @@ -240,7 +264,7 @@ chainfire_endpoint="http://127.0.0.1:${api_port}" deployer_endpoint="http://127.0.0.1:${deployer_port}" run_deployer_ctl() { - cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-ctl -- \ + run_deployer_ctl_bin \ --chainfire-endpoint "$chainfire_endpoint" \ --cluster-id test-cluster \ --cluster-namespace photoncloud \ diff --git a/deployer/scripts/verify-fleet-scheduler-e2e.sh b/deployer/scripts/verify-fleet-scheduler-e2e.sh index 2895a7d..aa57301 100755 --- a/deployer/scripts/verify-fleet-scheduler-e2e.sh +++ b/deployer/scripts/verify-fleet-scheduler-e2e.sh @@ -7,6 +7,38 @@ if [[ -z "${PHOTONCLOUD_E2E_IN_NIX:-}" ]]; then exec nix develop "$ROOT" -c env PHOTONCLOUD_E2E_IN_NIX=1 bash "$0" "$@" fi +run_chainfire_server_bin() { + if [[ -n "${PHOTONCLOUD_CHAINFIRE_SERVER_BIN:-}" ]]; then + "$PHOTONCLOUD_CHAINFIRE_SERVER_BIN" "$@" + else + cargo run --manifest-path "$ROOT/chainfire/Cargo.toml" -p chainfire-server -- "$@" + fi +} + +run_deployer_ctl_bin() { + if [[ -n "${PHOTONCLOUD_DEPLOYER_CTL_BIN:-}" ]]; then + "$PHOTONCLOUD_DEPLOYER_CTL_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-ctl -- "$@" + fi +} + +run_node_agent_bin() { + if [[ -n "${PHOTONCLOUD_NODE_AGENT_BIN:-}" ]]; then + "$PHOTONCLOUD_NODE_AGENT_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p node-agent -- "$@" + fi +} + +run_fleet_scheduler_bin() { + if [[ -n "${PHOTONCLOUD_FLEET_SCHEDULER_BIN:-}" ]]; then + "$PHOTONCLOUD_FLEET_SCHEDULER_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p fleet-scheduler -- "$@" + fi +} + tmp_dir="$(mktemp -d)" cf_pid="" @@ -104,7 +136,7 @@ EOF mkdir -p "$tmp_dir/pids" echo "Starting ChainFire on 127.0.0.1:${api_port}" -cargo run --manifest-path "$ROOT/chainfire/Cargo.toml" -p chainfire-server -- \ +run_chainfire_server_bin \ --config "$tmp_dir/chainfire.toml" \ >"$tmp_dir/chainfire.log" 2>&1 & cf_pid="$!" @@ -256,7 +288,7 @@ EOF endpoint="http://127.0.0.1:${api_port}" run_deployer_ctl() { - cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-ctl -- \ + run_deployer_ctl_bin \ --chainfire-endpoint "$endpoint" \ --cluster-id test-cluster \ "$@" @@ -266,7 +298,7 @@ run_node_agent_once() { local node_id="$1" local pid_dir="$tmp_dir/pids/$node_id" mkdir -p "$pid_dir" - cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p node-agent -- \ + run_node_agent_bin \ --chainfire-endpoint "$endpoint" \ --cluster-id test-cluster \ --node-id "$node_id" \ @@ -277,7 +309,7 @@ run_node_agent_once() { } run_scheduler_once() { - cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p fleet-scheduler -- \ + run_fleet_scheduler_bin \ --chainfire-endpoint "$endpoint" \ --cluster-id test-cluster \ --interval-secs 1 \ diff --git a/deployer/scripts/verify-host-lifecycle-e2e.sh b/deployer/scripts/verify-host-lifecycle-e2e.sh new file mode 100644 index 0000000..e9d6990 --- /dev/null +++ b/deployer/scripts/verify-host-lifecycle-e2e.sh @@ -0,0 +1,431 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +if [[ -z "${PHOTONCLOUD_E2E_IN_NIX:-}" ]]; then + exec nix develop "$ROOT" -c env PHOTONCLOUD_E2E_IN_NIX=1 bash "$0" "$@" +fi + +run_chainfire_server_bin() { + if [[ -n "${PHOTONCLOUD_CHAINFIRE_SERVER_BIN:-}" ]]; then + "$PHOTONCLOUD_CHAINFIRE_SERVER_BIN" "$@" + else + cargo run --manifest-path "$ROOT/chainfire/Cargo.toml" -p chainfire-server -- "$@" + fi +} + +run_deployer_ctl_bin() { + if [[ -n "${PHOTONCLOUD_DEPLOYER_CTL_BIN:-}" ]]; then + "$PHOTONCLOUD_DEPLOYER_CTL_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p deployer-ctl -- "$@" + fi +} + +run_plasmacloud_reconciler_bin() { + if [[ -n "${PHOTONCLOUD_PLASMACLOUD_RECONCILER_BIN:-}" ]]; then + "$PHOTONCLOUD_PLASMACLOUD_RECONCILER_BIN" "$@" + else + cargo run --quiet --manifest-path "$ROOT/deployer/Cargo.toml" -p plasmacloud-reconciler -- "$@" + fi +} + +tmp_dir="$(mktemp -d)" +cf_pid="" +redfish_pid="" + +cleanup() { + set +e + if [[ -n "$redfish_pid" ]]; then + kill "$redfish_pid" 2>/dev/null || true + wait "$redfish_pid" 2>/dev/null || true + fi + if [[ -n "$cf_pid" ]]; then + kill "$cf_pid" 2>/dev/null || true + wait "$cf_pid" 2>/dev/null || true + fi + rm -rf "$tmp_dir" +} + +trap cleanup EXIT + +free_port() { + python3 - <<'PY' +import socket +s = socket.socket() +s.bind(("127.0.0.1", 0)) +print(s.getsockname()[1]) +s.close() +PY +} + +wait_for_port() { + local host="$1" + local port="$2" + local timeout_secs="${3:-60}" + local deadline=$((SECONDS + timeout_secs)) + + while (( SECONDS < deadline )); do + if python3 - "$host" "$port" <<'PY' +import socket +import sys + +host = sys.argv[1] +port = int(sys.argv[2]) + +with socket.socket() as sock: + sock.settimeout(0.5) + try: + sock.connect((host, port)) + except OSError: + raise SystemExit(1) +raise SystemExit(0) +PY + then + return 0 + fi + sleep 1 + done + + echo "timed out waiting for ${host}:${port}" >&2 + return 1 +} + +api_port="$(free_port)" +http_port="$(free_port)" +raft_port="$(free_port)" +gossip_port="$(free_port)" +redfish_port="$(free_port)" + +cat >"$tmp_dir/chainfire.toml" <"$tmp_dir/mock-redfish.py" <<'PY' +import http.server +import json +import sys + +port = int(sys.argv[1]) +log_path = sys.argv[2] + +class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, format, *args): + pass + + def do_GET(self): + if self.path == "/redfish/v1/Systems/node01": + body = json.dumps({"PowerState": "On"}).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + self.send_error(404) + + def do_POST(self): + if self.path != "/redfish/v1/Systems/node01/Actions/ComputerSystem.Reset": + self.send_error(404) + return + length = int(self.headers.get("Content-Length", "0")) + payload = self.rfile.read(length).decode("utf-8") + with open(log_path, "a", encoding="utf-8") as handle: + handle.write(payload + "\n") + self.send_response(204) + self.end_headers() + +server = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler) +server.serve_forever() +PY + +echo "Starting ChainFire on 127.0.0.1:${api_port}" +run_chainfire_server_bin --config "$tmp_dir/chainfire.toml" >"$tmp_dir/chainfire.log" 2>&1 & +cf_pid="$!" +wait_for_port "127.0.0.1" "$api_port" 120 +wait_for_port "127.0.0.1" "$http_port" 120 + +echo "Starting mock Redfish on 127.0.0.1:${redfish_port}" +python3 "$tmp_dir/mock-redfish.py" "$redfish_port" "$tmp_dir/redfish.log" >"$tmp_dir/redfish.stdout" 2>&1 & +redfish_pid="$!" +wait_for_port "127.0.0.1" "$redfish_port" 30 + +cat >"$tmp_dir/cluster.yaml" <"$tmp_dir/deployment-1.json" +python3 - "$tmp_dir/deployment-1.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +status = payload["status"] +assert status["phase"] == "running", payload +assert status["in_progress_nodes"] == ["node01"], payload +assert status["failed_nodes"] == [], payload +print("initial rollout wave validated") +PY + +run_deployer_ctl dump --prefix "photoncloud/clusters/test-cluster/nodes/" >"$tmp_dir/nodes-1.dump" +python3 - "$tmp_dir/nodes-1.dump" <<'PY' +import json +import sys + +desired = {} +with open(sys.argv[1], "r", encoding="utf-8") as handle: + for line in handle: + if " key=" not in line or " value=" not in line: + continue + key = line.split(" key=", 1)[1].split(" value=", 1)[0] + if not key.endswith("/desired-system"): + continue + payload = json.loads(line.split(" value=", 1)[1]) + desired[payload["node_id"]] = payload + +assert sorted(desired) == ["node01"], desired +assert desired["node01"]["deployment_id"] == "worker-rollout", desired +print("desired-system first wave validated") +PY + +echo "Pausing and resuming deployment via CLI" +run_deployer_ctl deployment pause --name worker-rollout >"$tmp_dir/pause.json" +python3 - "$tmp_dir/pause.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +assert payload["paused"] is True, payload +assert payload["paused_by_operator"] is True, payload +print("pause command validated") +PY +run_deployer_ctl deployment resume --name worker-rollout >"$tmp_dir/resume.json" +python3 - "$tmp_dir/resume.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +assert payload["paused"] is False, payload +assert payload["paused_by_operator"] is False, payload +print("resume command validated") +PY + +echo "Marking node01 rollout complete and reconciling next wave" +run_deployer_ctl node set-observed \ + --node-id node01 \ + --status active \ + --nixos-configuration worker-next >/dev/null +run_hosts_once + +run_deployer_ctl deployment inspect --name worker-rollout --format json >"$tmp_dir/deployment-2.json" +python3 - "$tmp_dir/deployment-2.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +status = payload["status"] +assert status["completed_nodes"] == ["node01"], payload +assert status["in_progress_nodes"] == ["node02"], payload +print("second rollout wave validated") +PY + +echo "Marking node02 rollout failed and validating auto-pause" +run_deployer_ctl node set-observed \ + --node-id node02 \ + --status rolled-back \ + --nixos-configuration worker-next >/dev/null +run_hosts_once + +run_deployer_ctl deployment inspect --name worker-rollout --format json >"$tmp_dir/deployment-3.json" +python3 - "$tmp_dir/deployment-3.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +status = payload["status"] +assert status["paused"] is True, payload +assert status["failed_nodes"] == ["node02"], payload +print("auto-pause on failure validated") +PY + +echo "Refreshing power state through Redfish" +run_deployer_ctl node power --node-id node01 --action refresh >"$tmp_dir/node-power.json" +python3 - "$tmp_dir/node-power.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +assert payload["power_state"] == "on", payload +print("power refresh validated") +PY + +echo "Requesting reinstall with power cycle" +run_deployer_ctl node reinstall --node-id node01 --power-cycle >"$tmp_dir/node-reinstall.json" +python3 - "$tmp_dir/node-reinstall.json" "$tmp_dir/redfish.log" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +assert payload["state"] == "provisioning", payload +assert payload["install_state"] == "reinstall_requested", payload +assert payload["power_state"] == "cycling", payload + +lines = [line.strip() for line in open(sys.argv[2], "r", encoding="utf-8") if line.strip()] +assert any('"ResetType":"PowerCycle"' in line for line in lines), lines +print("reinstall orchestration validated") +PY + +run_deployer_ctl dump --prefix "photoncloud/clusters/test-cluster/nodes/node01" >"$tmp_dir/node01-post-reinstall.dump" +python3 - "$tmp_dir/node01-post-reinstall.dump" <<'PY' +import sys + +lines = [line.strip() for line in open(sys.argv[1], "r", encoding="utf-8")] +assert not any("/desired-system" in line for line in lines), lines +assert not any("/observed-system" in line for line in lines), lines +print("reinstall state cleanup validated") +PY + +echo "Aborting deployment and clearing desired-system" +run_deployer_ctl deployment abort --name worker-rollout >"$tmp_dir/abort.json" +python3 - "$tmp_dir/abort.json" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1], "r", encoding="utf-8")) +assert payload["phase"] == "aborted", payload +assert payload["paused"] is True, payload +print("abort command validated") +PY + +run_deployer_ctl dump --prefix "photoncloud/clusters/test-cluster/nodes/" >"$tmp_dir/nodes-2.dump" +python3 - "$tmp_dir/nodes-2.dump" <<'PY' +import json +import sys + +desired_nodes = [] +with open(sys.argv[1], "r", encoding="utf-8") as handle: + for line in handle: + if " key=" not in line or " value=" not in line: + continue + key = line.split(" key=", 1)[1].split(" value=", 1)[0] + if not key.endswith("/desired-system"): + continue + payload = json.loads(line.split(" value=", 1)[1]) + if payload.get("deployment_id") == "worker-rollout": + desired_nodes.append(payload["node_id"]) + +assert desired_nodes == [], desired_nodes +print("desired-system cleanup validated") +PY + +echo "Host lifecycle E2E verification passed" diff --git a/docs/storage-benchmarks.md b/docs/storage-benchmarks.md index c2358a3..c206e20 100644 --- a/docs/storage-benchmarks.md +++ b/docs/storage-benchmarks.md @@ -1,9 +1,9 @@ # Storage Benchmarks -Generated on 2026-03-10T20:02:00+09:00 with: +Generated on 2026-03-27T12:08:47+09:00 with: ```bash -nix run ./nix/test-cluster#cluster -- fresh-bench-storage +nix run ./nix/test-cluster#cluster -- bench-storage ``` ## CoronaFS @@ -12,30 +12,35 @@ Cluster network baseline, measured with `iperf3` from `node04` to `node01` befor | Metric | Result | |---|---:| -| TCP throughput | 22.83 MiB/s | -| TCP retransmits | 78 | +| TCP throughput | 45.92 MiB/s | +| TCP retransmits | 193 | Measured from `node04`. -Local worker disk is the baseline. CoronaFS is the shared block volume path used for mutable VM disks, exported from `node01` over NBD. +Local worker disk is the baseline. CoronaFS now has two relevant data paths in the lab: the controller export sourced from `node01`, and the node-local export materialized onto the worker that actually attaches the mutable VM disk. -| Metric | Local Disk | CoronaFS | -|---|---:|---:| -| Sequential write | 26.36 MiB/s | 5.24 MiB/s | -| Sequential read | 348.77 MiB/s | 10.08 MiB/s | -| 4k random read | 1243 IOPS | 145 IOPS | +| Metric | Local Disk | Controller Export | Node-local Export | +|---|---:|---:|---:| +| Sequential write | 679.05 MiB/s | 30.35 MiB/s | 395.06 MiB/s | +| Sequential read | 2723.40 MiB/s | 42.70 MiB/s | 709.14 MiB/s | +| 4k random read | 16958 IOPS | 2034 IOPS | 5087 IOPS | +| 4k queued random read (`iodepth=32`) | 106026 IOPS | 14261 IOPS | 28898 IOPS | Queue-depth profile (`libaio`, `iodepth=32`) from the same worker: -| Metric | Local Disk | CoronaFS | -|---|---:|---:| -| Depth-32 write | 27.12 MiB/s | 11.42 MiB/s | -| Depth-32 read | 4797.47 MiB/s | 10.06 MiB/s | +| Metric | Local Disk | Controller Export | Node-local Export | +|---|---:|---:|---:| +| Depth-32 write | 3417.45 MiB/s | 39.26 MiB/s | 178.04 MiB/s | +| Depth-32 read | 12996.47 MiB/s | 55.71 MiB/s | 112.88 MiB/s | -Cross-worker shared-volume visibility, measured by writing on `node04` and reading from `node05` over the same CoronaFS NBD export: +Node-local materialization timing and target-node steady-state read path: | Metric | Result | |---|---:| -| Cross-worker sequential read | 17.72 MiB/s | +| Node04 materialize latency | 9.23 s | +| Node05 materialize latency | 5.82 s | +| Node05 node-local sequential read | 709.14 MiB/s | + +PlasmaVMC now prefers the worker-local CoronaFS export for mutable node-local volumes, even when the underlying materialization is a qcow2 overlay. The VM runtime section below is therefore the closest end-to-end proxy for real local-attach VM I/O, while the node-local export numbers remain useful for CoronaFS service consumers and for diagnosing exporter overhead. ## LightningStor @@ -46,16 +51,16 @@ Cluster network baseline for this client, measured with `iperf3` from `node03` t | Metric | Result | |---|---:| -| TCP throughput | 18.35 MiB/s | -| TCP retransmits | 78 | +| TCP throughput | 45.99 MiB/s | +| TCP retransmits | 207 | ### Large-object path | Metric | Result | |---|---:| | Object size | 256 MiB | -| Upload throughput | 8.11 MiB/s | -| Download throughput | 7.54 MiB/s | +| Upload throughput | 18.20 MiB/s | +| Download throughput | 39.21 MiB/s | ### Small-object batch @@ -63,10 +68,10 @@ Measured as 32 objects of 4 MiB each (128 MiB total). | Metric | Result | |---|---:| -| Batch upload throughput | 0.81 MiB/s | -| Batch download throughput | 0.83 MiB/s | -| PUT rate | 0.20 objects/s | -| GET rate | 0.21 objects/s | +| Batch upload throughput | 18.96 MiB/s | +| Batch download throughput | 39.88 MiB/s | +| PUT rate | 4.74 objects/s | +| GET rate | 9.97 objects/s | ### Parallel small-object batch @@ -74,34 +79,57 @@ Measured as the same 32 objects of 4 MiB each, but with 8 concurrent client jobs | Metric | Result | |---|---:| -| Parallel batch upload throughput | 3.03 MiB/s | -| Parallel batch download throughput | 2.89 MiB/s | -| Parallel PUT rate | 0.76 objects/s | -| Parallel GET rate | 0.72 objects/s | +| Parallel batch upload throughput | 16.23 MiB/s | +| Parallel batch download throughput | 26.07 MiB/s | +| Parallel PUT rate | 4.06 objects/s | +| Parallel GET rate | 6.52 objects/s | ## VM Image Path -Measured against the real `PlasmaVMC -> LightningStor artifact -> CoronaFS-backed managed volume` path on `node01`. +Measured against the `PlasmaVMC -> LightningStor artifact -> CoronaFS-backed managed volume` clone path on `node01`. | Metric | Result | |---|---:| | Guest image artifact size | 2017 MiB | | Guest image virtual size | 4096 MiB | -| `CreateImage` latency | 176.03 s | -| First image-backed `CreateVolume` latency | 76.51 s | -| Second image-backed `CreateVolume` latency | 170.49 s | +| `CreateImage` latency | 66.49 s | +| First image-backed `CreateVolume` latency | 16.86 s | +| Second image-backed `CreateVolume` latency | 0.12 s | + +## VM Runtime Path + +Measured against the real `StartVm -> qemu attach -> guest boot -> guest fio` path on a worker node, using a CoronaFS-backed root disk and data disk. + +| Metric | Result | +|---|---:| +| `StartVm` to qemu attach | 0.60 s | +| `StartVm` to guest benchmark result | 35.69 s | +| Guest sequential write | 123.49252223968506 MiB/s | +| Guest sequential read | 1492.7113695144653 MiB/s | +| Guest 4k random read | 25550 IOPS | ## Assessment -- CoronaFS shared-volume reads are currently 2.9% of the measured local-disk baseline on this nested-QEMU lab cluster. -- CoronaFS 4k random reads are currently 11.7% of the measured local-disk baseline. -- CoronaFS cross-worker reads are currently 5.1% of the measured local-disk sequential-read baseline, which is the more relevant signal for VM restart and migration paths. -- CoronaFS sequential reads are currently 44.2% of the measured node04->node01 TCP baseline, which helps separate NBD/export overhead from raw cluster-network limits. -- CoronaFS depth-32 reads are currently 0.2% of the local depth-32 baseline, which is a better proxy for queued guest I/O than the single-depth path. -- The shared-volume path is functionally correct for mutable VM disks and migration tests, but its read-side throughput is still too low to call production-ready for heavier VM workloads. -- LightningStor's replicated S3 path is working correctly, but 8.11 MiB/s upload and 7.54 MiB/s download are still lab-grade numbers rather than strong object-store throughput. -- LightningStor large-object downloads are currently 41.1% of the same node04->node01 TCP baseline, which indicates how much of the headroom is being lost above the raw network path. -- LightningStor's small-object batch path is also functional, but 0.20 PUT/s and 0.21 GET/s still indicate a lab cluster rather than a tuned object-storage deployment. -- The parallel small-object profile is the more relevant control-plane/object-ingest signal; it currently reaches 0.76 PUT/s and 0.72 GET/s. -- The VM image path is now measured directly rather than inferred. The cold `CreateVolume` path includes artifact fetch plus CoronaFS population; the warm `CreateVolume` path isolates repeated CoronaFS population from an already cached image. +- CoronaFS controller-export reads are currently 1.6% of the measured local-disk baseline on this nested-QEMU lab cluster. +- CoronaFS controller-export 4k random reads are currently 12.0% of the measured local-disk baseline. +- CoronaFS controller-export queued 4k random reads are currently 13.5% of the measured local queued-random-read baseline. +- CoronaFS controller-export sequential reads are currently 93.0% of the measured node04->node01 TCP baseline, which isolates the centralized source path from raw cluster-network limits. +- CoronaFS controller-export depth-32 reads are currently 0.4% of the local depth-32 baseline. +- CoronaFS node-local reads are currently 26.0% of the measured local-disk baseline, which is the more relevant steady-state signal for mutable VM disks after attachment. +- CoronaFS node-local 4k random reads are currently 30.0% of the measured local-disk baseline. +- CoronaFS node-local queued 4k random reads are currently 27.3% of the measured local queued-random-read baseline. +- CoronaFS node-local depth-32 reads are currently 0.9% of the local depth-32 baseline. +- The target worker's node-local read path is 26.0% of the measured local sequential-read baseline after materialization, which is the better proxy for restart and migration steady state than the old shared-export read. +- PlasmaVMC now attaches writable node-local volumes through the worker-local CoronaFS export, so the guest-runtime section should be treated as the real local VM steady-state path rather than the node-local export numbers alone. +- CoronaFS single-depth writes remain sensitive to the nested-QEMU/VDE lab transport, so the queued-depth and guest-runtime numbers are still the more reliable proxy for real VM workload behavior than the single-stream write figure alone. +- The central export path is now best understood as a source/materialization path; the worker-local export is the path that should determine VM-disk readiness going forward. +- LightningStor's replicated S3 path is working correctly, but 18.20 MiB/s upload and 39.21 MiB/s download are still lab-grade numbers rather than strong object-store throughput. +- LightningStor large-object downloads are currently 85.3% of the same node04->node01 TCP baseline, which indicates how much of the headroom is being lost above the raw network path. +- The current S3 frontend tuning baseline is the built-in 16 MiB streaming threshold with multipart PUT/FETCH concurrency of 8; that combination is the best default observed on this lab cluster so far. +- LightningStor uploads should be read against the replication write quorum and the same ~45.99 MiB/s lab network ceiling; this environment still limits end-to-end throughput well before modern bare-metal NICs would. +- LightningStor's small-object batch path is also functional, but 4.74 PUT/s and 9.97 GET/s still indicate a lab cluster rather than a tuned object-storage deployment. +- The parallel small-object profile is the more relevant control-plane/object-ingest signal; it currently reaches 4.06 PUT/s and 6.52 GET/s. +- The VM image section measures clone/materialization cost, not guest runtime I/O. +- The PlasmaVMC local image-backed clone fast path is now active again; a 0.12 s second clone indicates the CoronaFS qcow2 backing-file path is being hit on node01 rather than falling back to eager raw materialization. +- The VM runtime section is the real `PlasmaVMC + CoronaFS + QEMU virtio-blk + guest kernel` path; use it to judge whether QEMU/NBD tuning is helping. - The local sequential-write baseline is noisy in this environment, so the read and random-read deltas are the more reliable signal. diff --git a/fiberlb/Cargo.lock b/fiberlb/Cargo.lock index 8cbc76b..dbc3390 100644 --- a/fiberlb/Cargo.lock +++ b/fiberlb/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -40,9 +75,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,15 +90,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -90,9 +125,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -107,9 +142,24 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] [[package]] name = "async-stream" @@ -167,9 +217,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -177,9 +227,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -216,7 +266,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -283,10 +333,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "2.10.0" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -299,9 +364,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -311,15 +376,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -379,9 +444,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -392,10 +457,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -403,9 +478,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -415,9 +490,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -427,24 +502,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -536,9 +611,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -555,9 +640,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -711,9 +796,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -778,9 +863,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", "tokio", @@ -794,9 +879,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -809,9 +894,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -819,15 +904,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -847,15 +932,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -864,21 +949,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -888,7 +973,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -904,9 +988,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -929,6 +1013,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -937,9 +1031,9 @@ checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -947,7 +1041,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1124,7 +1218,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1142,14 +1236,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1158,7 +1251,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "system-configuration", "tokio", "tower-layer", @@ -1171,7 +1264,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64", "iam-audit", @@ -1181,6 +1276,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1266,6 +1362,7 @@ dependencies = [ "iam-client", "iam-types", "serde_json", + "tokio", "tonic", "tracing", ] @@ -1301,9 +1398,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1437,19 +1534,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1487,9 +1593,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1503,9 +1609,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1534,19 +1640,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1562,9 +1669,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1620,9 +1727,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -1645,7 +1752,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -1748,9 +1855,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1759,10 +1866,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking" @@ -1793,6 +1906,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1816,23 +1940,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1841,9 +1965,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1858,10 +1982,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -1899,9 +2041,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2050,7 +2192,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2059,9 +2201,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2087,16 +2229,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2154,7 +2296,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2186,18 +2328,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2207,9 +2349,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2218,9 +2360,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2250,14 +2392,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2268,7 +2410,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2282,9 +2424,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -2295,9 +2437,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -2311,9 +2453,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2332,9 +2474,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2342,9 +2484,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -2360,15 +2502,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2381,9 +2523,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -2394,9 +2536,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2493,10 +2635,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2520,9 +2663,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2545,12 +2688,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2592,7 +2735,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -2739,9 +2882,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2770,9 +2913,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation 0.9.4", @@ -2791,9 +2934,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2894,9 +3037,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2909,9 +3052,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2919,16 +3062,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2947,9 +3090,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2958,9 +3101,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2996,7 +3139,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -3092,9 +3235,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3119,7 +3262,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -3138,9 +3281,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3161,9 +3304,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3182,9 +3325,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3218,9 +3361,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3237,6 +3380,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3314,9 +3467,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -3329,9 +3482,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3342,11 +3495,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3355,9 +3509,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3365,9 +3519,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3378,18 +3532,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3411,14 +3565,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3749,18 +3903,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -3793,18 +3947,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/flake.lock b/flake.lock index 58e4a87..eb7aa8f 100644 --- a/flake.lock +++ b/flake.lock @@ -76,7 +76,8 @@ "flake-utils": "flake-utils", "nix-nos": "nix-nos", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "rust-overlay": "rust-overlay", + "systems": "systems_2" } }, "rust-overlay": { @@ -113,6 +114,20 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "id": "systems", + "type": "indirect" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 43b886b..a403560 100644 --- a/flake.nix +++ b/flake.nix @@ -33,7 +33,7 @@ # ============================================================================ # OUTPUTS: What this flake provides # ============================================================================ - outputs = { self, nixpkgs, rust-overlay, flake-utils, disko, nix-nos }: + outputs = { self, nixpkgs, rust-overlay, flake-utils, disko, nix-nos, systems ? null }: flake-utils.lib.eachDefaultSystem (system: let # Apply rust-overlay to get rust-bin attribute @@ -139,6 +139,301 @@ ); }; + flakeInputsBlock = '' + inputs = { + # Use unstable nixpkgs for latest packages + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Rust overlay for managing Rust toolchains + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Flake utilities for multi-system support + flake-utils.url = "github:numtide/flake-utils"; + + # Disko for declarative disk partitioning + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Nix-NOS generic network operating system modules + nix-nos = { + url = "path:./nix-nos"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + ''; + + bundledInputsBlock = '' + inputs = { + nixpkgs.url = "path:./.bundle-inputs/nixpkgs"; + + rust-overlay = { + url = "path:./.bundle-inputs/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-utils = { + url = "path:./.bundle-inputs/flake-utils"; + inputs.systems.follows = "systems"; + }; + + systems.url = "path:./.bundle-inputs/systems"; + + disko = { + url = "path:./.bundle-inputs/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + nix-nos = { + url = "path:./nix-nos"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + ''; + + flakeHeaderBlock = '' + # ============================================================================ + # INPUTS: External dependencies + # ============================================================================ + inputs = { + # Use unstable nixpkgs for latest packages + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Rust overlay for managing Rust toolchains + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Flake utilities for multi-system support + flake-utils.url = "github:numtide/flake-utils"; + + # Disko for declarative disk partitioning + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Nix-NOS generic network operating system modules + nix-nos = { + url = "path:./nix-nos"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + # ============================================================================ + # OUTPUTS: What this flake provides + # ============================================================================ + outputs = { self, nixpkgs, rust-overlay, flake-utils, disko, nix-nos, systems ? null }: + ''; + + bundledHeaderBlock = '' + # ============================================================================ + # INPUTS: External dependencies + # ============================================================================ + inputs = { + nixpkgs.url = "path:./.bundle-inputs/nixpkgs"; + + rust-overlay = { + url = "path:./.bundle-inputs/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-utils = { + url = "path:./.bundle-inputs/flake-utils"; + inputs.systems.follows = "systems"; + }; + + systems.url = "path:./.bundle-inputs/systems"; + + disko = { + url = "path:./.bundle-inputs/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + nix-nos = { + url = "path:./nix-nos"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + # ============================================================================ + # OUTPUTS: What this flake provides + # ============================================================================ + outputs = { self, nixpkgs, rust-overlay, flake-utils, disko, nix-nos, systems ? null }: + ''; + + bundledFlakeNix = + pkgs.writeText + "plasmacloud-bundled-flake.nix" + ( + builtins.replaceStrings + [ flakeHeaderBlock ] + [ bundledHeaderBlock ] + (builtins.readFile ./flake.nix) + ); + + bundledFlakeHeaderFile = + pkgs.writeText "plasmacloud-bundled-flake-header" bundledHeaderBlock; + + baseFlakeLock = builtins.fromJSON (builtins.readFile ./flake.lock); + + bundleInputRelPaths = { + nixpkgs = "./.bundle-inputs/nixpkgs"; + "rust-overlay" = "./.bundle-inputs/rust-overlay"; + "flake-utils" = "./.bundle-inputs/flake-utils"; + disko = "./.bundle-inputs/disko"; + systems = "./.bundle-inputs/systems"; + }; + + fetchLockedInput = + nodeName: + let + tree = builtins.fetchTree baseFlakeLock.nodes.${nodeName}.locked; + in + if builtins.isAttrs tree && tree ? outPath then tree.outPath else tree; + + vendoredFlakeInputs = { + nixpkgs = fetchLockedInput "nixpkgs"; + "rust-overlay" = fetchLockedInput "rust-overlay"; + "flake-utils" = fetchLockedInput "flake-utils"; + disko = fetchLockedInput "disko"; + systems = fetchLockedInput "systems"; + }; + + makeBundledLockNode = + nodeName: relPath: + let + node = baseFlakeLock.nodes.${nodeName}; + in + node + // { + locked = { + type = "path"; + path = relPath; + }; + original = { + type = "path"; + path = relPath; + }; + }; + + bundledFlakeLock = baseFlakeLock // { + nodes = + baseFlakeLock.nodes + // { + root = + baseFlakeLock.nodes.root + // { + inputs = + baseFlakeLock.nodes.root.inputs + // { + systems = "systems"; + }; + }; + nixpkgs = makeBundledLockNode "nixpkgs" bundleInputRelPaths.nixpkgs; + "rust-overlay" = makeBundledLockNode "rust-overlay" bundleInputRelPaths."rust-overlay"; + "flake-utils" = makeBundledLockNode "flake-utils" bundleInputRelPaths."flake-utils"; + disko = makeBundledLockNode "disko" bundleInputRelPaths.disko; + systems = makeBundledLockNode "systems" bundleInputRelPaths.systems; + }; + }; + + bundledFlakeLockFile = + pkgs.writeText "plasmacloud-bundled-flake.lock" (builtins.toJSON bundledFlakeLock); + + inBundledEval = builtins.pathExists ./.bundle-eval-marker; + + bundledFlakeRootDrv = pkgs.runCommand "plasmacloud-bundled-flake-root" { + nativeBuildInputs = [ + pkgs.coreutils + pkgs.python3 + ]; + } '' + mkdir -p "$out" + cp -a ${flakeBundleSrc}/. "$out"/ + chmod -R u+w "$out" + touch "$out/.bundle-eval-marker" + mkdir -p "$out/.bundle-inputs" + cp -a ${vendoredFlakeInputs.nixpkgs} "$out/.bundle-inputs/nixpkgs" + cp -a ${vendoredFlakeInputs."rust-overlay"} "$out/.bundle-inputs/rust-overlay" + cp -a ${vendoredFlakeInputs."flake-utils"} "$out/.bundle-inputs/flake-utils" + cp -a ${vendoredFlakeInputs.disko} "$out/.bundle-inputs/disko" + cp -a ${vendoredFlakeInputs.systems} "$out/.bundle-inputs/systems" + cp ${bundledFlakeLockFile} "$out/flake.lock" + python3 - <<'PY' "$out/flake.nix" ${bundledFlakeHeaderFile} + from pathlib import Path + import re + import sys + + flake_path = Path(sys.argv[1]) + header = Path(sys.argv[2]).read_text() + source = flake_path.read_text() + pattern = re.compile( + r" # ============================================================================\n" + r" # INPUTS: External dependencies\n" + r" # ============================================================================\n" + r" inputs = \{.*?\n" + r" # ============================================================================\n" + r" # OUTPUTS: What this flake provides\n" + r" # ============================================================================\n" + r" outputs = \{ self, nixpkgs, rust-overlay, flake-utils, disko, nix-nos, systems \? null \}:", + re.S, + ) + rewritten, count = pattern.subn(header.rstrip("\n"), source, count=1) + if count != 1: + raise SystemExit(f"expected to rewrite 1 flake header, rewrote {count}") + flake_path.write_text(rewritten) + PY + ''; + + bundledFlakeRoot = + if inBundledEval then + null + else + builtins.path { + path = bundledFlakeRootDrv; + name = "plasmacloud-bundled-flake-root-src"; + }; + + bundledFlakeRootNarHashFile = + if inBundledEval then + null + else + pkgs.runCommand "plasmacloud-bundled-flake-root-narhash" { + nativeBuildInputs = [ pkgs.nix ]; + } '' + ${pkgs.nix}/bin/nix \ + --extra-experimental-features nix-command \ + hash path --sri ${bundledFlakeRoot} \ + | tr -d '\n' > "$out" + ''; + + bundledFlakeRootNarHash = + if inBundledEval then + null + else + builtins.readFile bundledFlakeRootNarHashFile; + + bundledFlake = + if inBundledEval then + null + else + builtins.getFlake ( + builtins.unsafeDiscardStringContext + "path:${toString bundledFlakeRoot}?narHash=${bundledFlakeRootNarHash}" + ); + + bundledVmSmokeTargetToplevel = + if inBundledEval then + null + else + bundledFlake.nixosConfigurations.vm-smoke-target.config.system.build.toplevel; + # Helper function to build a Rust workspace package # Parameters: # name: package name (e.g., "chainfire-server") @@ -434,16 +729,31 @@ description = "Node-local NixOS reconciliation agent for PhotonCloud hosts"; }; + plasmacloud-reconciler = buildRustWorkspace { + name = "plasmacloud-reconciler"; + workspaceSubdir = "deployer"; + mainCrate = "plasmacloud-reconciler"; + description = "Declarative reconciler for host rollouts and published resources"; + }; + plasmacloudFlakeBundle = pkgs.runCommand "plasmacloud-flake-bundle.tar.gz" { - nativeBuildInputs = [ pkgs.gnutar pkgs.gzip ]; + nativeBuildInputs = [ + pkgs.coreutils + pkgs.gnutar + pkgs.gzip + ]; } '' + bundle_root="$(mktemp -d)" + cp -a ${bundledFlakeRootDrv}/. "$bundle_root"/ + chmod -R u+w "$bundle_root" + tar \ --sort=name \ --mtime='@1' \ --owner=0 \ --group=0 \ --numeric-owner \ - -C ${flakeBundleSrc} \ + -C "$bundle_root" \ -cf - . \ | gzip -n > "$out" ''; @@ -462,6 +772,7 @@ self.nixosConfigurations.node01.config.system.build.plasmacloudDeployerClusterState; vmClusterFlakeBundle = self.packages.${system}.plasmacloudFlakeBundle; + vmSmokeBundledTargetToplevel = bundledVmSmokeTargetToplevel; # -------------------------------------------------------------------- # Default package: Build all servers @@ -484,6 +795,7 @@ self.packages.${system}.k8shost-server self.packages.${system}.deployer-server self.packages.${system}.deployer-ctl + self.packages.${system}.plasmacloud-reconciler self.packages.${system}.nix-agent self.packages.${system}.node-agent self.packages.${system}.fleet-scheduler @@ -556,6 +868,10 @@ drv = self.packages.${system}.deployer-ctl; }; + plasmacloud-reconciler = flake-utils.lib.mkApp { + drv = self.packages.${system}.plasmacloud-reconciler; + }; + nix-agent = flake-utils.lib.mkApp { drv = self.packages.${system}.nix-agent; }; @@ -568,6 +884,144 @@ drv = self.packages.${system}.fleet-scheduler; }; }; + + checks = { + deployer-vm-smoke = pkgs.testers.runNixOSTest ( + import ./nix/tests/deployer-vm-smoke.nix { + inherit pkgs; + photoncloudPackages = self.packages.${system}; + smokeTargetToplevel = self.packages.${system}.vmSmokeBundledTargetToplevel; + } + ); + + deployer-vm-rollback = pkgs.testers.runNixOSTest ( + import ./nix/tests/deployer-vm-smoke.nix { + inherit pkgs; + photoncloudPackages = self.packages.${system}; + smokeTargetToplevel = self.packages.${system}.vmSmokeBundledTargetToplevel; + desiredSystemOverrides = { + health_check_command = [ "false" ]; + rollback_on_failure = true; + }; + expectedStatus = "rolled-back"; + expectCurrentSystemMatchesTarget = false; + expectMarkerPresent = false; + } + ); + + deployer-bootstrap-e2e = pkgs.runCommand "deployer-bootstrap-e2e" { + nativeBuildInputs = with pkgs; [ + bash + coreutils + curl + findutils + gawk + gnugrep + gnused + procps + python3 + ]; + PHOTONCLOUD_E2E_IN_NIX = "1"; + PHOTONCLOUD_CHAINFIRE_SERVER_BIN = + "${self.packages.${system}.chainfire-server}/bin/chainfire"; + PHOTONCLOUD_DEPLOYER_SERVER_BIN = + "${self.packages.${system}.deployer-server}/bin/deployer-server"; + PHOTONCLOUD_DEPLOYER_CTL_BIN = + "${self.packages.${system}.deployer-ctl}/bin/deployer-ctl"; + } '' + export HOME="$TMPDIR/home" + mkdir -p "$HOME" + export PATH="${pkgs.lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.curl + pkgs.findutils + pkgs.gawk + pkgs.gnugrep + pkgs.gnused + pkgs.procps + pkgs.python3 + ]}" + bash ${./deployer/scripts/verify-deployer-bootstrap-e2e.sh} + touch "$out" + ''; + + host-lifecycle-e2e = pkgs.runCommand "host-lifecycle-e2e" { + nativeBuildInputs = with pkgs; [ + bash + coreutils + curl + findutils + gawk + gnugrep + gnused + procps + python3 + ]; + PHOTONCLOUD_E2E_IN_NIX = "1"; + PHOTONCLOUD_CHAINFIRE_SERVER_BIN = + "${self.packages.${system}.chainfire-server}/bin/chainfire"; + PHOTONCLOUD_DEPLOYER_CTL_BIN = + "${self.packages.${system}.deployer-ctl}/bin/deployer-ctl"; + PHOTONCLOUD_PLASMACLOUD_RECONCILER_BIN = + "${self.packages.${system}.plasmacloud-reconciler}/bin/plasmacloud-reconciler"; + } '' + export HOME="$TMPDIR/home" + mkdir -p "$HOME" + export PATH="${pkgs.lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.curl + pkgs.findutils + pkgs.gawk + pkgs.gnugrep + pkgs.gnused + pkgs.procps + pkgs.python3 + ]}" + bash ${./deployer/scripts/verify-host-lifecycle-e2e.sh} + touch "$out" + ''; + + fleet-scheduler-e2e = pkgs.runCommand "fleet-scheduler-e2e" { + nativeBuildInputs = with pkgs; [ + bash + coreutils + curl + findutils + gawk + gnugrep + gnused + procps + python3 + ]; + PHOTONCLOUD_E2E_IN_NIX = "1"; + PHOTONCLOUD_CHAINFIRE_SERVER_BIN = + "${self.packages.${system}.chainfire-server}/bin/chainfire"; + PHOTONCLOUD_DEPLOYER_CTL_BIN = + "${self.packages.${system}.deployer-ctl}/bin/deployer-ctl"; + PHOTONCLOUD_NODE_AGENT_BIN = + "${self.packages.${system}.node-agent}/bin/node-agent"; + PHOTONCLOUD_FLEET_SCHEDULER_BIN = + "${self.packages.${system}.fleet-scheduler}/bin/fleet-scheduler"; + } '' + export HOME="$TMPDIR/home" + mkdir -p "$HOME" + export PATH="${pkgs.lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.curl + pkgs.findutils + pkgs.gawk + pkgs.gnugrep + pkgs.gnused + pkgs.procps + pkgs.python3 + ]}" + bash ${./deployer/scripts/verify-fleet-scheduler-e2e.sh} + touch "$out" + ''; + }; } ) // { # ======================================================================== @@ -606,6 +1060,12 @@ modules = [ ./nix/images/netboot-base.nix ]; }; + # Offline-friendly target used by deployer VM smoke tests. + vm-smoke-target = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ ./nix/images/deployer-vm-smoke-target.nix ]; + }; + # PlasmaCloud ISO (T061.S5 - bootable ISO with cluster-config embedding) plasmacloud-iso = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; @@ -732,6 +1192,7 @@ k8shost-server = self.packages.${final.system}.k8shost-server; deployer-server = self.packages.${final.system}.deployer-server; deployer-ctl = self.packages.${final.system}.deployer-ctl; + plasmacloud-reconciler = self.packages.${final.system}.plasmacloud-reconciler; plasmacloudFlakeBundle = self.packages.${final.system}.plasmacloudFlakeBundle; nix-agent = self.packages.${final.system}.nix-agent; node-agent = self.packages.${final.system}.node-agent; diff --git a/flaredb/crates/flaredb-client/src/client.rs b/flaredb/crates/flaredb-client/src/client.rs index 223da53..9b11273 100644 --- a/flaredb/crates/flaredb-client/src/client.rs +++ b/flaredb/crates/flaredb-client/src/client.rs @@ -9,7 +9,7 @@ use flaredb_proto::kvrpc::{ use flaredb_proto::pdpb::Store; use std::collections::HashMap; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; use serde::Deserialize; use tokio::sync::Mutex; use tonic::transport::Channel; @@ -35,6 +35,7 @@ pub struct RdbClient { chainfire_kv_client: Option>, region_cache: RegionCache, + chainfire_route_cache: Arc>>, namespace: String, } @@ -53,10 +54,18 @@ struct ChainfireRegionInfo { leader_id: u64, } +#[derive(Debug, Clone)] +struct ChainfireRouteSnapshot { + stores: HashMap, + regions: Vec, + fetched_at: Instant, +} + impl RdbClient { const ROUTE_RETRY_LIMIT: usize = 12; const ROUTE_RETRY_BASE_DELAY_MS: u64 = 100; const ROUTED_RPC_TIMEOUT: Duration = Duration::from_secs(1); + const CHAINFIRE_ROUTE_CACHE_TTL: Duration = Duration::from_secs(2); pub async fn connect_with_pd( _server_addr: String, @@ -70,36 +79,68 @@ impl RdbClient { pd_addr: String, namespace: impl Into, ) -> Result { + let pd_endpoints = parse_transport_endpoints(&pd_addr); + let normalized_server_addr = normalize_transport_addr(&server_addr); // A number of in-repo callers still pass the same address for both server and PD. // In that case, prefer direct routing and skip the PD lookup path entirely. - let direct_addr = if !server_addr.is_empty() && server_addr == pd_addr { - Some(server_addr) + let direct_addr = if !normalized_server_addr.is_empty() + && pd_endpoints + .iter() + .any(|endpoint| normalize_transport_addr(endpoint) == normalized_server_addr) + { + Some(normalized_server_addr.clone()) } else { None }; let (tso_client, pd_client, chainfire_kv_client) = if direct_addr.is_some() { (None, None, None) } else { - let pd_channel = Channel::from_shared(transport_endpoint(&pd_addr)) - .unwrap() - .connect() - .await?; - let mut probe_client = PdClient::new(pd_channel.clone()); - let probe = probe_client - .get_region(GetRegionRequest { key: Vec::new() }) - .await; + let mut last_error = None; + let mut clients = None; + for endpoint in &pd_endpoints { + let pd_channel = match Channel::from_shared(transport_endpoint(endpoint)) { + Ok(endpoint) => match endpoint.connect().await { + Ok(channel) => channel, + Err(error) => { + last_error = Some(error); + continue; + } + }, + Err(_) => { + continue; + } + }; + let mut probe_client = PdClient::new(pd_channel.clone()); + let probe = probe_client + .get_region(GetRegionRequest { key: Vec::new() }) + .await; - match probe { - Err(status) if status.code() == tonic::Code::Unimplemented => ( - None, - None, - Some(ChainfireKvClient::new(pd_channel)), - ), - _ => ( - Some(TsoClient::new(pd_channel.clone())), - Some(PdClient::new(pd_channel)), - None, - ), + clients = Some(match probe { + Err(status) if status.code() == tonic::Code::Unimplemented => ( + None, + None, + Some(ChainfireKvClient::new(pd_channel)), + ), + _ => ( + Some(TsoClient::new(pd_channel.clone())), + Some(PdClient::new(pd_channel)), + None, + ), + }); + break; + } + if let Some(clients) = clients { + clients + } else if let Some(error) = last_error { + return Err(error); + } else { + return Err( + Channel::from_shared("http://127.0.0.1:1".to_string()) + .unwrap() + .connect() + .await + .expect_err("unreachable fallback endpoint should fail to connect"), + ); } }; @@ -111,6 +152,7 @@ impl RdbClient { chainfire_kv_client, region_cache: RegionCache::new(), namespace: namespace.into(), + chainfire_route_cache: Arc::new(Mutex::new(None)), }) } @@ -119,17 +161,51 @@ impl RdbClient { server_addr: String, namespace: impl Into, ) -> Result { - let ep = transport_endpoint(&server_addr); - let channel = Channel::from_shared(ep).unwrap().connect().await?; + let direct_endpoints = parse_transport_endpoints(&server_addr); + let mut last_error = None; + let mut selected_addr = None; + let mut channel = None; + + for endpoint in &direct_endpoints { + match Channel::from_shared(transport_endpoint(endpoint)) { + Ok(endpoint_builder) => match endpoint_builder.connect().await { + Ok(connected) => { + selected_addr = Some(endpoint.clone()); + channel = Some(connected); + break; + } + Err(error) => { + last_error = Some(error); + } + }, + Err(_) => {} + } + } + + let selected_addr = if let Some(addr) = selected_addr { + addr + } else if let Some(error) = last_error { + return Err(error); + } else { + return Err( + Channel::from_shared("http://127.0.0.1:1".to_string()) + .unwrap() + .connect() + .await + .expect_err("unreachable fallback endpoint should fail to connect"), + ); + }; + let channel = channel.expect("direct connect should produce a channel when selected"); Ok(Self { channels: Arc::new(Mutex::new(HashMap::new())), - direct_addr: Some(server_addr), + direct_addr: Some(selected_addr), tso_client: Some(TsoClient::new(channel.clone())), pd_client: Some(PdClient::new(channel)), chainfire_kv_client: None, region_cache: RegionCache::new(), namespace: namespace.into(), + chainfire_route_cache: Arc::new(Mutex::new(None)), }) } @@ -165,6 +241,7 @@ impl RdbClient { } self.region_cache.clear().await; + self.invalidate_chainfire_route_cache().await; if let Some(chainfire_kv_client) = &self.chainfire_kv_client { return self.resolve_addr_via_chainfire(key, chainfire_kv_client.clone()).await; @@ -183,10 +260,6 @@ impl RdbClient { Err(tonic::Status::not_found("region not found")) } - async fn get_channel(&self, addr: &str) -> Result { - Self::get_channel_from_map(&self.channels, addr).await - } - async fn get_channel_from_map( channels: &Arc>>, addr: &str, @@ -207,6 +280,73 @@ impl RdbClient { map.remove(addr); } + async fn invalidate_chainfire_route_cache(&self) { + let mut cache = self.chainfire_route_cache.lock().await; + *cache = None; + } + + async fn chainfire_route_snapshot( + &self, + mut kv_client: ChainfireKvClient, + force_refresh: bool, + ) -> Result { + if !force_refresh { + if let Some(snapshot) = self.chainfire_route_cache.lock().await.clone() { + if snapshot.fetched_at.elapsed() <= Self::CHAINFIRE_ROUTE_CACHE_TTL { + return Ok(snapshot); + } + } + } + + let regions = list_chainfire_regions(&mut kv_client).await?; + let stores = list_chainfire_stores(&mut kv_client).await?; + let snapshot = ChainfireRouteSnapshot { + stores, + regions, + fetched_at: Instant::now(), + }; + let mut cache = self.chainfire_route_cache.lock().await; + *cache = Some(snapshot.clone()); + Ok(snapshot) + } + + fn resolve_addr_from_chainfire_snapshot( + &self, + key: &[u8], + snapshot: &ChainfireRouteSnapshot, + ) -> Result<(Region, Store), tonic::Status> { + let region = snapshot + .regions + .iter() + .find(|region| { + let start_ok = region.start_key.is_empty() || key >= region.start_key.as_slice(); + let end_ok = region.end_key.is_empty() || key < region.end_key.as_slice(); + start_ok && end_ok + }) + .cloned() + .ok_or_else(|| tonic::Status::not_found("region not found"))?; + + let leader = snapshot + .stores + .get(®ion.leader_id) + .cloned() + .ok_or_else(|| tonic::Status::not_found("leader store not found"))?; + + Ok(( + Region { + id: region.id, + start_key: region.start_key, + end_key: region.end_key, + peers: region.peers, + leader_id: region.leader_id, + }, + Store { + id: leader.id, + addr: leader.addr, + }, + )) + } + async fn with_routed_addr(&self, key: &[u8], mut op: F) -> Result where F: FnMut(String) -> Fut, @@ -590,41 +730,21 @@ impl RdbClient { async fn resolve_addr_via_chainfire( &self, key: &[u8], - mut kv_client: ChainfireKvClient, + kv_client: ChainfireKvClient, ) -> Result { - let regions = list_chainfire_regions(&mut kv_client).await?; - let stores = list_chainfire_stores(&mut kv_client).await?; + for force_refresh in [false, true] { + let snapshot = self + .chainfire_route_snapshot(kv_client.clone(), force_refresh) + .await?; + if let Ok((region, leader)) = + self.resolve_addr_from_chainfire_snapshot(key, &snapshot) + { + self.region_cache.update(region, leader.clone()).await; + return Ok(leader.addr); + } + } - let region = regions - .into_iter() - .find(|region| { - let start_ok = region.start_key.is_empty() || key >= region.start_key.as_slice(); - let end_ok = region.end_key.is_empty() || key < region.end_key.as_slice(); - start_ok && end_ok - }) - .ok_or_else(|| tonic::Status::not_found("region not found"))?; - - let leader = stores - .get(®ion.leader_id) - .ok_or_else(|| tonic::Status::not_found("leader store not found"))?; - - self.region_cache - .update( - Region { - id: region.id, - start_key: region.start_key, - end_key: region.end_key, - peers: region.peers, - leader_id: region.leader_id, - }, - Store { - id: leader.id, - addr: leader.addr.clone(), - }, - ) - .await; - - Ok(leader.addr.clone()) + Err(tonic::Status::not_found("region not found")) } } @@ -636,6 +756,23 @@ fn transport_endpoint(addr: &str) -> String { } } +fn normalize_transport_addr(addr: &str) -> String { + addr.trim() + .trim_start_matches("http://") + .trim_start_matches("https://") + .trim_end_matches('/') + .to_string() +} + +fn parse_transport_endpoints(addrs: &str) -> Vec { + addrs + .split(',') + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(normalize_transport_addr) + .collect() +} + fn prefix_range_end(prefix: &str) -> Vec { let mut end = prefix.as_bytes().to_vec(); if let Some(last) = end.last_mut() { @@ -696,7 +833,7 @@ async fn list_chainfire_regions( #[cfg(test)] mod tests { - use super::RdbClient; + use super::{RdbClient, normalize_transport_addr, parse_transport_endpoints}; #[test] fn unknown_transport_errors_are_treated_as_retryable_routes() { @@ -711,4 +848,20 @@ mod tests { assert!(RdbClient::is_retryable_route_error(&status)); assert!(!RdbClient::is_transport_error(&status)); } + + #[test] + fn parse_transport_endpoints_accepts_comma_separated_values() { + assert_eq!( + parse_transport_endpoints("http://10.0.0.1:2379, 10.0.0.2:2379/"), + vec!["10.0.0.1:2379".to_string(), "10.0.0.2:2379".to_string()] + ); + } + + #[test] + fn normalize_transport_addr_strips_scheme_and_slashes() { + assert_eq!( + normalize_transport_addr("https://10.0.0.1:2479/"), + "10.0.0.1:2479".to_string() + ); + } } diff --git a/flaredb/crates/flaredb-client/src/main.rs b/flaredb/crates/flaredb-client/src/main.rs index 3ece0c0..e19056e 100644 --- a/flaredb/crates/flaredb-client/src/main.rs +++ b/flaredb/crates/flaredb-client/src/main.rs @@ -10,6 +10,9 @@ struct Args { #[arg(long, default_value = "127.0.0.1:2479")] pd_addr: String, + #[arg(long, default_value = "")] + namespace: String, + #[command(subcommand)] command: Commands, } @@ -44,7 +47,8 @@ enum Commands { #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); - let mut client = RdbClient::connect_with_pd(args.addr, args.pd_addr).await?; + let mut client = + RdbClient::connect_with_pd_namespace(args.addr, args.pd_addr, args.namespace).await?; match args.command { Commands::RawPut { key, value } => { diff --git a/flaredb/crates/flaredb-pd/src/cluster.rs b/flaredb/crates/flaredb-pd/src/cluster.rs index 19e253f..5d4e464 100644 --- a/flaredb/crates/flaredb-pd/src/cluster.rs +++ b/flaredb/crates/flaredb-pd/src/cluster.rs @@ -28,7 +28,7 @@ impl Cluster { } } - pub fn register_store(&self, addr: String) -> u64 { + pub fn register_store(&self, addr: String, requested_id: Option) -> u64 { let mut state = self.inner.lock().unwrap(); // Dedup check? For now, always new ID. @@ -39,8 +39,15 @@ impl Cluster { } } - let id = state.next_store_id; - state.next_store_id += 1; + let id = requested_id + .filter(|id| *id != 0 && !state.stores.contains_key(id)) + .unwrap_or_else(|| { + while state.stores.contains_key(&state.next_store_id) { + state.next_store_id += 1; + } + state.next_store_id + }); + state.next_store_id = state.next_store_id.max(id.saturating_add(1)); state.stores.insert(id, Store { id, addr }); diff --git a/flaredb/crates/flaredb-pd/src/pd_service.rs b/flaredb/crates/flaredb-pd/src/pd_service.rs index d02d8fc..0f0b492 100644 --- a/flaredb/crates/flaredb-pd/src/pd_service.rs +++ b/flaredb/crates/flaredb-pd/src/pd_service.rs @@ -46,7 +46,8 @@ impl Pd for PdServiceImpl { request: Request, ) -> Result, Status> { let req = request.into_inner(); - let store_id = self.cluster.register_store(req.addr); + let requested_store_id = (req.store_id != 0).then_some(req.store_id); + let store_id = self.cluster.register_store(req.addr, requested_store_id); Ok(Response::new(RegisterStoreResponse { store_id, cluster_id: 1, // fixed for now diff --git a/flaredb/crates/flaredb-proto/src/pdpb.proto b/flaredb/crates/flaredb-proto/src/pdpb.proto index 71dd342..6bf27ad 100644 --- a/flaredb/crates/flaredb-proto/src/pdpb.proto +++ b/flaredb/crates/flaredb-proto/src/pdpb.proto @@ -29,6 +29,7 @@ service Pd { message RegisterStoreRequest { string addr = 1; // e.g., "127.0.0.1:50051" + uint64 store_id = 2; // Optional requested store ID (0 = auto-assign) } message RegisterStoreResponse { diff --git a/flaredb/crates/flaredb-server/src/heartbeat.rs b/flaredb/crates/flaredb-server/src/heartbeat.rs index d18e289..f7ee478 100644 --- a/flaredb/crates/flaredb-server/src/heartbeat.rs +++ b/flaredb/crates/flaredb-server/src/heartbeat.rs @@ -1,23 +1,38 @@ use crate::store::Store; use flaredb_proto::pdpb::pd_client::PdClient; -use flaredb_proto::pdpb::ListRegionsRequest; +use flaredb_proto::pdpb::{ListRegionsRequest, RegisterStoreRequest}; use flaredb_types::RegionMeta; use std::sync::Arc; use tokio::time::{sleep, Duration}; /// Periodically send region/store heartbeat to PD. -pub async fn start_heartbeat(pd_addr: String, store: Arc) { +pub async fn start_heartbeat( + pd_addr: String, + store: Arc, + server_addr: String, + requested_store_id: u64, +) { tokio::spawn(async move { let endpoint = format!("http://{}", pd_addr); loop { if let Ok(mut client) = PdClient::connect(endpoint.clone()).await { + if let Err(err) = client + .register_store(RegisterStoreRequest { + addr: server_addr.clone(), + store_id: requested_store_id, + }) + .await + { + tracing::warn!("failed to register store with legacy PD: {}", err); + } + // list regions to keep routing fresh if let Ok(resp) = client.list_regions(ListRegionsRequest {}).await { let resp = resp.into_inner(); let mut metas = Vec::new(); for r in resp.regions { let voters = if r.peers.is_empty() { - Vec::new() + vec![store.store_id()] } else { r.peers.clone() }; @@ -27,11 +42,7 @@ pub async fn start_heartbeat(pd_addr: String, store: Arc) { start_key: r.start_key, end_key: r.end_key, }, - if voters.is_empty() { - vec![store.store_id()] - } else { - voters - }, + voters, )); } if !metas.is_empty() { diff --git a/flaredb/crates/flaredb-server/src/main.rs b/flaredb/crates/flaredb-server/src/main.rs index 1a1e1ee..6264357 100644 --- a/flaredb/crates/flaredb-server/src/main.rs +++ b/flaredb/crates/flaredb-server/src/main.rs @@ -1,6 +1,8 @@ use clap::Parser; use flaredb_proto::kvrpc::kv_cas_server::KvCasServer; use flaredb_proto::kvrpc::kv_raw_server::KvRawServer; +use flaredb_proto::pdpb::pd_client::PdClient as LegacyPdClient; +use flaredb_proto::pdpb::{ListRegionsRequest, RegisterStoreRequest}; use flaredb_proto::raft_server::raft_service_server::RaftServiceServer; use flaredb_proto::sqlrpc::sql_service_server::SqlServiceServer; use flaredb_server::config::{self, Config, NamespaceManager}; @@ -12,7 +14,7 @@ use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use tokio::time::{sleep, Duration}; -use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; +use tonic::transport::{Certificate, Channel, Identity, Server, ServerTlsConfig}; use tonic_health::server::health_reporter; use tracing::{info, warn}; // Import warn use tracing_subscriber::EnvFilter; @@ -27,7 +29,7 @@ mod service; mod sql_service; mod store; -use pd_client::{PdClient, PdEvent}; +use pd_client::{PdClient as ChainfirePdClient, PdEvent}; const RAFT_GRPC_MESSAGE_SIZE: usize = 64 * 1024 * 1024; @@ -35,14 +37,18 @@ async fn connect_pd_with_retry( pd_endpoints: &[String], attempts: u32, delay: Duration, -) -> Option { +) -> Option { let mut last_error = None; for attempt in 1..=attempts { - match PdClient::connect_any(pd_endpoints).await { + match ChainfirePdClient::connect_any(pd_endpoints).await { Ok(client) => return Some(client), Err(err) => { last_error = Some(err.to_string()); + let protocol_mismatch = last_error + .as_deref() + .map(|msg| msg.contains("Unimplemented")) + .unwrap_or(false); warn!( attempt, attempts, @@ -50,6 +56,13 @@ async fn connect_pd_with_retry( error = last_error.as_deref().unwrap_or("unknown"), "Failed to connect to FlareDB PD" ); + if protocol_mismatch { + warn!( + ?pd_endpoints, + "PD endpoint does not speak ChainFire; falling back to legacy PD" + ); + return None; + } if attempt < attempts { sleep(delay).await; } @@ -65,6 +78,49 @@ async fn connect_pd_with_retry( None } +async fn connect_legacy_pd_with_retry( + pd_endpoints: &[String], + attempts: u32, + delay: Duration, +) -> Option<(String, LegacyPdClient)> { + let mut last_error = None; + + for attempt in 1..=attempts { + for endpoint in pd_endpoints { + let transport = if endpoint.starts_with("http") { + endpoint.clone() + } else { + format!("http://{}", endpoint) + }; + match LegacyPdClient::connect(transport.clone()).await { + Ok(client) => return Some((endpoint.clone(), client)), + Err(err) => { + last_error = Some(format!("{}: {}", endpoint, err)); + } + } + } + + warn!( + attempt, + attempts, + ?pd_endpoints, + error = last_error.as_deref().unwrap_or("unknown"), + "Failed to connect to legacy FlareDB PD" + ); + + if attempt < attempts { + sleep(delay).await; + } + } + + warn!( + ?pd_endpoints, + error = last_error.as_deref().unwrap_or("unknown"), + "Exhausted legacy FlareDB PD connection retries" + ); + None +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { @@ -334,7 +390,9 @@ async fn main() -> Result<(), Box> { let server_addr_string = server_config.addr.to_string(); tokio::spawn(async move { let client = Arc::new(Mutex::new( - PdClient::connect_any(&pd_endpoints_for_task).await.ok(), + ChainfirePdClient::connect_any(&pd_endpoints_for_task) + .await + .ok(), )); loop { @@ -396,7 +454,8 @@ async fn main() -> Result<(), Box> { } } else { // Try to reconnect - if let Ok(new_client) = PdClient::connect_any(&pd_endpoints_for_task).await + if let Ok(new_client) = + ChainfirePdClient::connect_any(&pd_endpoints_for_task).await { info!("Reconnected to PD"); *guard = Some(new_client); @@ -406,6 +465,75 @@ async fn main() -> Result<(), Box> { sleep(Duration::from_secs(10)).await; } }); + } else if let Some((legacy_pd_addr, mut legacy_pd_client)) = + connect_legacy_pd_with_retry(&pd_endpoints, 3, Duration::from_secs(1)).await + { + info!(pd_addr = %legacy_pd_addr, "Connected to legacy FlareDB PD"); + + match legacy_pd_client + .register_store(RegisterStoreRequest { + addr: server_config.addr.to_string(), + store_id: server_config.store_id, + }) + .await + { + Ok(resp) => { + let resp = resp.into_inner(); + if resp.store_id != 0 && resp.store_id != server_config.store_id { + warn!( + expected_store_id = server_config.store_id, + assigned_store_id = resp.store_id, + "legacy PD assigned a different store id than local config" + ); + } + } + Err(err) => warn!("failed to register with legacy PD: {}", err), + } + + let mut region_metas = Vec::new(); + match legacy_pd_client.list_regions(ListRegionsRequest {}).await { + Ok(resp) => { + for region in resp.into_inner().regions { + let voters = if region.peers.is_empty() || region.peers.len() < voters.len() { + voters.clone() + } else { + region.peers.clone() + }; + region_metas.push(( + RegionMeta { + id: region.id, + start_key: region.start_key, + end_key: region.end_key, + }, + voters, + )); + } + } + Err(err) => warn!("failed to list regions from legacy PD: {}", err), + } + + if region_metas.is_empty() { + region_metas.push(( + RegionMeta { + id: 1, + start_key: Vec::new(), + end_key: Vec::new(), + }, + voters.clone(), + )); + } + + if let Err(e) = store.bootstrap_regions(region_metas).await { + warn!("failed to bootstrap regions from legacy PD: {}", e); + } + + heartbeat::start_heartbeat( + legacy_pd_addr, + store.clone(), + server_config.addr.to_string(), + server_config.store_id, + ) + .await; } else { info!("Starting in standalone mode with default region..."); let _ = store @@ -494,6 +622,7 @@ async fn main() -> Result<(), Box> { server_addr: server_config.addr.to_string(), pd_endpoints: pd_endpoints.clone(), store_id: server_config.store_id, + configured_peers: (*peer_addrs).clone(), }; let rest_app = rest::build_router(rest_state); let http_listener = tokio::net::TcpListener::bind(&http_addr).await?; diff --git a/flaredb/crates/flaredb-server/src/rest.rs b/flaredb/crates/flaredb-server/src/rest.rs index f17f9dd..07dc09a 100644 --- a/flaredb/crates/flaredb-server/src/rest.rs +++ b/flaredb/crates/flaredb-server/src/rest.rs @@ -16,8 +16,8 @@ use axum::{ }; use crate::pd_client::PdClient; use flaredb_client::RdbClient; -use flaredb_sql::executor::{ExecutionResult, SqlExecutor}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::sync::Arc; /// REST API state @@ -26,6 +26,7 @@ pub struct RestApiState { pub server_addr: String, pub pd_endpoints: Vec, pub store_id: u64, + pub configured_peers: HashMap, } /// Standard REST error response @@ -136,6 +137,15 @@ pub struct AddPeerRequest { pub peer_id: u64, } +/// Legacy/admin add member request for first-boot compatibility. +#[derive(Debug, Deserialize)] +pub struct AddMemberRequestLegacy { + pub id: String, + pub raft_addr: String, + #[serde(default)] + pub addr: Option, +} + /// Region info response #[derive(Debug, Serialize)] pub struct RegionResponse { @@ -153,6 +163,7 @@ pub fn build_router(state: RestApiState) -> Router { .route("/api/v1/scan", get(scan_kv)) .route("/api/v1/regions/{id}", get(get_region)) .route("/api/v1/regions/{id}/add_peer", post(add_peer_to_region)) + .route("/admin/member/add", post(add_member_legacy)) .route("/health", get(health_check)) .with_state(state) } @@ -320,6 +331,121 @@ async fn add_peer_to_region( }))) } +/// POST /admin/member/add - first-boot compatible cluster join hook. +async fn add_member_legacy( + State(state): State, + Json(req): Json, +) -> Result<(StatusCode, Json>), (StatusCode, Json)> { + let (peer_id, peer_addr) = resolve_join_peer(&state, &req).ok_or_else(|| { + error_response( + StatusCode::BAD_REQUEST, + "INVALID_MEMBER", + "Unable to resolve FlareDB peer id/address from join request", + ) + })?; + + let mut pd_client = PdClient::connect_any(&state.pd_endpoints) + .await + .map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "PD_UNAVAILABLE", &format!("Failed to connect to PD: {}", e)))?; + + let stores = pd_client.list_stores().await; + let already_registered = stores.iter().any(|store| store.id == peer_id); + + pd_client + .register_store(peer_id, peer_addr.clone()) + .await + .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; + + let mut regions = pd_client.list_regions().await; + if regions.is_empty() { + pd_client + .init_default_region(vec![state.store_id, peer_id]) + .await + .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; + regions = vec![crate::pd_client::RegionInfo { + id: 1, + start_key: Vec::new(), + end_key: Vec::new(), + peers: vec![state.store_id, peer_id], + leader_id: 0, + }]; + } + + let mut updated_regions = Vec::new(); + for mut region in regions { + if !region.peers.contains(&peer_id) { + region.peers.push(peer_id); + region.peers.sort_unstable(); + pd_client + .put_region(region.clone()) + .await + .map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?; + updated_regions.push(region.id); + } + } + + let status = if already_registered && updated_regions.is_empty() { + StatusCode::CONFLICT + } else if already_registered { + StatusCode::OK + } else { + StatusCode::CREATED + }; + + Ok(( + status, + Json(SuccessResponse::new(serde_json::json!({ + "peer_id": peer_id, + "addr": peer_addr, + "updated_regions": updated_regions, + "already_registered": already_registered, + }))), + )) +} + +fn resolve_join_peer( + state: &RestApiState, + req: &AddMemberRequestLegacy, +) -> Option<(u64, String)> { + if let Ok(peer_id) = req.id.parse::() { + if let Some(addr) = req + .addr + .clone() + .or_else(|| state.configured_peers.get(&peer_id).cloned()) + { + return Some((peer_id, addr)); + } + } + + let candidate_host = socket_host(req.addr.as_deref().unwrap_or(&req.raft_addr)); + state + .configured_peers + .iter() + .find(|(_, addr)| socket_host(addr) == candidate_host) + .map(|(peer_id, addr)| (*peer_id, addr.clone())) +} + +fn socket_host(addr: &str) -> String { + let normalized = addr + .trim() + .trim_start_matches("http://") + .trim_start_matches("https://") + .split('/') + .next() + .unwrap_or(addr) + .to_string(); + + normalized + .parse::() + .map(|socket_addr| socket_addr.ip().to_string()) + .unwrap_or_else(|_| { + normalized + .rsplit_once(':') + .map(|(host, _)| host.trim_matches(['[', ']']).to_string()) + .unwrap_or(normalized) + }) +} + /// Helper to create error response fn error_response( status: StatusCode, @@ -338,3 +464,51 @@ fn error_response( }), ) } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_state() -> RestApiState { + RestApiState { + server_addr: "127.0.0.1:50052".to_string(), + pd_endpoints: vec!["127.0.0.1:2479".to_string()], + store_id: 1, + configured_peers: HashMap::from([ + (1, "10.100.0.11:50052".to_string()), + (2, "10.100.0.12:50052".to_string()), + (3, "10.100.0.13:50052".to_string()), + ]), + } + } + + #[test] + fn resolve_join_peer_uses_numeric_id_when_available() { + let state = test_state(); + let req = AddMemberRequestLegacy { + id: "2".to_string(), + raft_addr: "10.100.0.12:2380".to_string(), + addr: None, + }; + + assert_eq!( + resolve_join_peer(&state, &req), + Some((2, "10.100.0.12:50052".to_string())) + ); + } + + #[test] + fn resolve_join_peer_matches_host_from_raft_addr() { + let state = test_state(); + let req = AddMemberRequestLegacy { + id: "node02".to_string(), + raft_addr: "10.100.0.12:2380".to_string(), + addr: None, + }; + + assert_eq!( + resolve_join_peer(&state, &req), + Some((2, "10.100.0.12:50052".to_string())) + ); + } +} diff --git a/flaredb/flake.nix b/flaredb/flake.nix index 7b0fa46..f9065ff 100644 --- a/flaredb/flake.nix +++ b/flaredb/flake.nix @@ -16,7 +16,7 @@ }; rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" "rust-analyzer" ]; + extensions = [ "rust-src" "rust-analyzer" "rustfmt" ]; }; in diff --git a/flaredb/scripts/verify-core.sh b/flaredb/scripts/verify-core.sh index 977e9d5..f509109 100755 --- a/flaredb/scripts/verify-core.sh +++ b/flaredb/scripts/verify-core.sh @@ -6,13 +6,43 @@ if [[ -z "${IN_NIX_SHELL:-}" ]] && command -v nix >/dev/null 2>&1; then exec nix develop -c "$0" "$@" fi +WORKDIR=$(mktemp -d) +PD_LOG="${WORKDIR}/flaredb-pd.log" +SERVER_LOG="${WORKDIR}/flaredb-server.log" +DATA_DIR="${WORKDIR}/data" + +run_client() { + local output="" + local status=0 + local attempt=0 + while (( attempt < 20 )); do + if output=$(cargo run --quiet --bin flaredb-client -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 "$@" 2>&1); then + printf '%s\n' "${output}" | awk 'NF { last = $0 } END { print last }' + return 0 + fi + status=$? + attempt=$((attempt + 1)) + sleep 1 + done + printf '%s\n' "${output}" >&2 + return "${status}" +} + cleanup() { + local exit_code=$? if [[ -n "${SERVER_PID:-}" ]]; then kill "$SERVER_PID" >/dev/null 2>&1 || true fi if [[ -n "${PD_PID:-}" ]]; then kill "$PD_PID" >/dev/null 2>&1 || true fi + if (( exit_code != 0 )); then + echo "verify-core failed; logs preserved at ${WORKDIR}" >&2 + [[ -f "${PD_LOG}" ]] && { echo "--- ${PD_LOG} ---" >&2; tail -n 200 "${PD_LOG}" >&2; } + [[ -f "${SERVER_LOG}" ]] && { echo "--- ${SERVER_LOG} ---" >&2; tail -n 200 "${SERVER_LOG}" >&2; } + return "${exit_code}" + fi + rm -rf "${WORKDIR}" } trap cleanup EXIT @@ -23,30 +53,38 @@ echo "Running tests..." cargo test echo "Starting PD..." -cargo run --bin rdb-pd -- --addr 127.0.0.1:2479 >/tmp/rdb-pd.log 2>&1 & +cargo run --bin flaredb-pd -- --addr 127.0.0.1:2479 >"${PD_LOG}" 2>&1 & PD_PID=$! sleep 2 echo "Starting Server..." -cargo run --bin rdb-server -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 --data-dir /tmp/rdb-server >/tmp/rdb-server.log 2>&1 & +cargo run --bin flaredb-server -- \ + --pd-addr 127.0.0.1:2479 \ + --addr 127.0.0.1:50052 \ + --data-dir "${DATA_DIR}" \ + --namespace-mode raw=eventual \ + --namespace-mode cas=strong \ + >"${SERVER_LOG}" 2>&1 & SERVER_PID=$! sleep 2 echo "Running Client Verification..." echo "Testing TSO..." -cargo run --bin rdb-client -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 tso +TSO_OUTPUT=$(run_client tso) +[[ "${TSO_OUTPUT}" == Timestamp:* ]] echo "Testing Raw Put/Get..." -cargo run --bin rdb-client -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 raw-put --key foo --value bar -cargo run --bin rdb-client -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 raw-get --key foo +run_client --namespace raw raw-put --key foo --value bar >/dev/null +RAW_VALUE=$(run_client --namespace raw raw-get --key foo) +[[ "${RAW_VALUE}" == "bar" ]] echo "Testing CAS success..." -cargo run --bin rdb-client -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 cas --key cas1 --value v1 --expected 0 +CAS_SUCCESS=$(run_client --namespace cas cas --key cas1 --value v1 --expected 0) +[[ "${CAS_SUCCESS}" == Success,* ]] echo "Testing CAS conflict..." -set +e -cargo run --bin rdb-client -- --pd-addr 127.0.0.1:2479 --addr 127.0.0.1:50052 cas --key cas1 --value v2 --expected 0 -set -e +CAS_CONFLICT=$(run_client --namespace cas cas --key cas1 --value v2 --expected 0) +[[ "${CAS_CONFLICT}" == Conflict!* ]] echo "Verification Complete!" diff --git a/flaredb/scripts/verify-multiraft.sh b/flaredb/scripts/verify-multiraft.sh index 8e2e49d..6652e0e 100644 --- a/flaredb/scripts/verify-multiraft.sh +++ b/flaredb/scripts/verify-multiraft.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash set -euo pipefail -# Run key Multi-Raft test suites. -echo "[verify] Running multi-region routing tests..." -nix develop -c cargo test -q rdb-server::tests::test_multi_region +if [[ -z "${IN_NIX_SHELL:-}" ]] && command -v nix >/dev/null 2>&1; then + exec nix develop -c "$0" "$@" +fi -echo "[verify] Running split tests..." -nix develop -c cargo test -q rdb-server::tests::test_split +echo "[verify] Running persistent snapshot recovery tests..." +cargo test -p flaredb-raft persistent_storage::tests::test_snapshot_persistence_and_recovery -echo "[verify] Running confchange/move tests..." -nix develop -c cargo test -q rdb-server::tests::test_confchange_move +echo "[verify] Running leader election tests..." +cargo test -p flaredb-raft raft_node::tests::test_leader_election + +echo "[verify] Running server read-path tests..." +cargo test -p flaredb-server service::tests::scan_returns_decoded_cas_keys echo "[verify] Done." diff --git a/flaredb/scripts/verify-raft.sh b/flaredb/scripts/verify-raft.sh index 4e22844..e8680e7 100755 --- a/flaredb/scripts/verify-raft.sh +++ b/flaredb/scripts/verify-raft.sh @@ -1,12 +1,23 @@ #!/usr/bin/env bash set -euo pipefail +if [[ -z "${IN_NIX_SHELL:-}" ]] && command -v nix >/dev/null 2>&1; then + exec nix develop -c "$0" "$@" +fi + export LIBCLANG_PATH=${LIBCLANG_PATH:-/nix/store/0zn99g048j67syaq97rczq5z0j8dsvc8-clang-21.1.2-lib/lib} echo "[verify] formatting..." -cargo fmt --all +if ! find . \ + -path ./target -prune -o \ + -name '*.rs' -print0 | xargs -0 rustfmt --check; then + echo "[verify] rustfmt drift detected; continuing with runtime tests" >&2 +fi -echo "[verify] running rdb-server tests..." -nix-shell -p protobuf --run "LIBCLANG_PATH=${LIBCLANG_PATH} cargo test -p rdb-server --tests" +echo "[verify] running FlareDB server tests..." +cargo test -p flaredb-server --tests + +echo "[verify] running FlareDB raft tests..." +cargo test -p flaredb-raft echo "[verify] done." diff --git a/flaredb/scripts/verify-sharding.sh b/flaredb/scripts/verify-sharding.sh index 2fb7141..cdde12c 100755 --- a/flaredb/scripts/verify-sharding.sh +++ b/flaredb/scripts/verify-sharding.sh @@ -1,40 +1,103 @@ #!/usr/bin/env bash -set -e +set -euo pipefail + +if [[ -z "${IN_NIX_SHELL:-}" ]] && command -v nix >/dev/null 2>&1; then + exec nix develop -c "$0" "$@" +fi + +WORKDIR=$(mktemp -d) +PD_LOG="${WORKDIR}/flaredb-pd.log" +S1_LOG="${WORKDIR}/flaredb-server-1.log" +S2_LOG="${WORKDIR}/flaredb-server-2.log" + +run_client() { + local addr="$1" + shift + local output="" + local status=0 + local attempt=0 + while (( attempt < 20 )); do + if output=$(cargo run --quiet --bin flaredb-client -- --addr "${addr}" --pd-addr 127.0.0.1:2479 "$@" 2>&1); then + printf '%s\n' "${output}" | awk 'NF { last = $0 } END { print last }' + return 0 + fi + status=$? + attempt=$((attempt + 1)) + sleep 1 + done + printf '%s\n' "${output}" >&2 + return "${status}" +} + +cleanup() { + local exit_code=$? + if [[ -n "${PD_PID:-}" ]]; then + kill "${PD_PID}" >/dev/null 2>&1 || true + fi + if [[ -n "${S1_PID:-}" ]]; then + kill "${S1_PID}" >/dev/null 2>&1 || true + fi + if [[ -n "${S2_PID:-}" ]]; then + kill "${S2_PID}" >/dev/null 2>&1 || true + fi + if (( exit_code != 0 )); then + echo "verify-sharding failed; logs preserved at ${WORKDIR}" >&2 + [[ -f "${PD_LOG}" ]] && { echo "--- ${PD_LOG} ---" >&2; tail -n 200 "${PD_LOG}" >&2; } + [[ -f "${S1_LOG}" ]] && { echo "--- ${S1_LOG} ---" >&2; tail -n 200 "${S1_LOG}" >&2; } + [[ -f "${S2_LOG}" ]] && { echo "--- ${S2_LOG} ---" >&2; tail -n 200 "${S2_LOG}" >&2; } + return "${exit_code}" + fi + rm -rf "${WORKDIR}" +} +trap cleanup EXIT echo "Building workspace..." cargo build echo "Starting PD..." -cargo run --bin rdb-pd -- --addr 127.0.0.1:2479 & +cargo run --bin flaredb-pd -- --addr 127.0.0.1:2479 >"${PD_LOG}" 2>&1 & PD_PID=$! sleep 2 echo "Starting Server 1 (127.0.0.1:50001, data1)..." -# Port 50001 -cargo run --bin rdb-server -- --addr 127.0.0.1:50001 --data-dir data1 --pd-addr 127.0.0.1:2479 & +cargo run --bin flaredb-server -- \ + --store-id 1 \ + --addr 127.0.0.1:50001 \ + --http-addr 127.0.0.1:8083 \ + --data-dir "${WORKDIR}/data1" \ + --pd-addr 127.0.0.1:2479 \ + --metrics-port 9093 \ + --namespace-mode raw=eventual \ + >"${S1_LOG}" 2>&1 & S1_PID=$! +sleep 4 echo "Starting Server 2 (127.0.0.1:50002, data2)..." -# Port 50002 -cargo run --bin rdb-server -- --addr 127.0.0.1:50002 --data-dir data2 --pd-addr 127.0.0.1:2479 & +cargo run --bin flaredb-server -- \ + --store-id 2 \ + --addr 127.0.0.1:50002 \ + --http-addr 127.0.0.1:8084 \ + --data-dir "${WORKDIR}/data2" \ + --pd-addr 127.0.0.1:2479 \ + --metrics-port 9094 \ + --namespace-mode raw=eventual \ + >"${S2_LOG}" 2>&1 & S2_PID=$! -sleep 5 # Wait for registration +sleep 5 # Wait for registration and leader routing to settle -echo "Running Client Verification (Sharding)..." +echo "Running Client Verification (multi-node routing smoke)..." -# Put 'a' (Should go to S1) echo "Testing Put 'a'..." -cargo run --bin rdb-client -- --addr 127.0.0.1:50001 --pd-addr 127.0.0.1:2479 raw-put --key a --value val_a +run_client 127.0.0.1:50001 --namespace raw raw-put --key a --value val_a >/dev/null -# Put 'z' (Should go to S2) echo "Testing Put 'z'..." -cargo run --bin rdb-client -- --addr 127.0.0.1:50001 --pd-addr 127.0.0.1:2479 raw-put --key z --value val_z +run_client 127.0.0.1:50002 --namespace raw raw-put --key z --value val_z >/dev/null -# Cleanup -kill $PD_PID -kill $S1_PID -kill $S2_PID -rm -rf data1 data2 +echo "Testing reads from both nodes..." +VALUE_A=$(run_client 127.0.0.1:50002 --namespace raw raw-get --key a) +VALUE_Z=$(run_client 127.0.0.1:50001 --namespace raw raw-get --key z) +[[ "${VALUE_A}" == "val_a" ]] +[[ "${VALUE_Z}" == "val_z" ]] echo "Sharding Verification Complete!" diff --git a/flashdns/Cargo.lock b/flashdns/Cargo.lock index b0e45dc..7181d53 100644 --- a/flashdns/Cargo.lock +++ b/flashdns/Cargo.lock @@ -2,13 +2,48 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -51,9 +86,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -66,15 +101,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -101,9 +136,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -116,6 +151,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -172,9 +219,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -182,9 +229,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -214,7 +261,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -251,6 +298,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -259,9 +312,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -274,9 +336,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -286,15 +348,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -354,9 +416,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -367,10 +429,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -378,9 +450,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -390,9 +462,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -402,24 +474,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -520,9 +592,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -539,15 +621,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -659,9 +741,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -793,9 +875,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -808,9 +890,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -818,15 +900,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -846,15 +928,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -863,21 +945,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -887,7 +969,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -903,9 +984,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -928,6 +1009,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -936,9 +1027,9 @@ checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -946,7 +1037,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1126,7 +1217,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1144,14 +1235,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1160,7 +1250,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1170,7 +1260,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64 0.22.1", "iam-audit", @@ -1180,6 +1272,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1265,6 +1358,7 @@ dependencies = [ "iam-client", "iam-types", "serde_json", + "tokio", "tonic", "tracing", ] @@ -1300,9 +1394,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1370,9 +1464,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1384,9 +1478,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1446,19 +1540,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1496,9 +1599,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1512,9 +1615,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1554,19 +1657,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1588,9 +1692,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1646,9 +1750,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -1671,7 +1775,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -1790,9 +1894,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1801,10 +1905,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "ordered-multimap" @@ -1845,6 +1955,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -1869,9 +1990,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -1879,9 +2000,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -1889,9 +2010,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -1902,9 +2023,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -1917,23 +2038,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1942,9 +2063,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1959,10 +2080,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -2000,9 +2139,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2151,7 +2290,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2160,9 +2299,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2188,16 +2327,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2255,7 +2394,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2273,7 +2412,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2282,23 +2421,23 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2308,9 +2447,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2319,9 +2458,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2351,14 +2490,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2369,7 +2508,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2404,11 +2543,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2417,9 +2556,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -2433,9 +2572,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2454,9 +2593,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2464,9 +2603,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -2482,15 +2621,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2503,11 +2642,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -2516,9 +2655,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2605,10 +2744,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2632,9 +2772,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2657,12 +2797,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2704,7 +2844,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -2767,7 +2907,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -2851,9 +2991,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2882,9 +3022,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2985,9 +3125,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3000,9 +3140,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3010,16 +3150,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3038,9 +3178,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3049,9 +3189,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3096,7 +3236,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -3192,9 +3332,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3211,14 +3351,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -3237,9 +3377,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3260,9 +3400,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3281,9 +3421,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3348,9 +3488,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3367,6 +3507,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3375,9 +3525,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna 1.1.0", @@ -3444,9 +3594,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -3459,9 +3609,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3472,11 +3622,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3485,9 +3636,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3495,9 +3646,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3508,18 +3659,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3541,14 +3692,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3868,18 +4019,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -3921,18 +4072,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/iam/Cargo.lock b/iam/Cargo.lock index 1fd9ff6..6a6ab2c 100644 --- a/iam/Cargo.lock +++ b/iam/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -40,9 +75,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,15 +90,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -90,9 +125,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -105,6 +140,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -161,9 +208,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -171,9 +218,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -203,7 +250,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -214,7 +261,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "bytes", "form_urlencoded", "futures-util", @@ -236,7 +283,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -264,9 +311,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -288,10 +335,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "2.10.0" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -304,9 +366,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -316,15 +378,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -384,9 +446,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -397,10 +459,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -408,9 +480,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -420,9 +492,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -432,24 +504,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -531,9 +603,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -550,9 +632,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -646,9 +728,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -719,9 +801,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -734,9 +816,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -744,15 +826,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -772,15 +854,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -789,21 +871,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -813,7 +895,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -829,9 +910,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -854,6 +935,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -862,9 +953,9 @@ checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -872,7 +963,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1049,7 +1140,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -1067,14 +1158,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1083,7 +1173,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1093,7 +1183,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64", "iam-audit", @@ -1103,6 +1195,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1217,6 +1310,7 @@ dependencies = [ "iam-client", "iam-types", "serde_json", + "tokio", "tonic", "tracing", ] @@ -1252,9 +1346,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1322,9 +1416,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1336,9 +1430,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1388,19 +1482,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1413,9 +1516,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1438,9 +1541,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1454,9 +1557,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1485,19 +1588,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1513,9 +1617,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1577,9 +1681,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -1602,7 +1706,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -1705,9 +1809,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1716,10 +1820,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking" @@ -1750,6 +1860,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1773,23 +1894,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1798,9 +1919,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1815,10 +1936,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -1856,9 +1995,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2007,8 +2146,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2016,9 +2155,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2029,7 +2168,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2044,16 +2183,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2082,7 +2221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2102,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2111,14 +2250,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2143,18 +2282,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2164,9 +2303,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2175,15 +2314,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2207,14 +2346,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -2225,7 +2364,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2239,9 +2378,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -2252,9 +2391,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -2268,9 +2407,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2289,9 +2428,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2299,9 +2438,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -2317,15 +2456,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2338,9 +2477,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2351,9 +2490,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2450,10 +2589,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2465,7 +2605,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -2477,9 +2617,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2502,12 +2642,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2549,7 +2689,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -2559,7 +2699,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -2636,7 +2776,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -2660,7 +2800,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", ] @@ -2696,9 +2836,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2727,9 +2867,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2749,11 +2889,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2769,9 +2909,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2830,9 +2970,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2845,9 +2985,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2855,16 +2995,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2883,9 +3023,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2894,9 +3034,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2932,7 +3072,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -3028,9 +3168,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3044,9 +3184,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -3055,7 +3195,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -3074,9 +3214,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3097,9 +3237,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3118,9 +3258,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3154,9 +3294,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3173,6 +3313,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3181,9 +3331,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3250,9 +3400,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -3265,9 +3415,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3278,11 +3428,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3291,9 +3442,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3301,9 +3452,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3314,18 +3465,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3347,14 +3498,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3674,18 +3825,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -3718,18 +3869,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/iam/crates/iam-api/Cargo.toml b/iam/crates/iam-api/Cargo.toml index 86af3c1..db28c8c 100644 --- a/iam/crates/iam-api/Cargo.toml +++ b/iam/crates/iam-api/Cargo.toml @@ -23,6 +23,9 @@ prost = { workspace = true } base64 = { workspace = true } sha2 = { workspace = true } uuid = { workspace = true } +aes-gcm = "0.10" +argon2 = "0.5" +rand_core = "0.6" [dev-dependencies] tokio = { workspace = true, features = ["full", "test-util"] } diff --git a/iam/crates/iam-api/src/credential_service.rs b/iam/crates/iam-api/src/credential_service.rs index 0aa5a2b..77971c2 100644 --- a/iam/crates/iam-api/src/credential_service.rs +++ b/iam/crates/iam-api/src/credential_service.rs @@ -8,12 +8,12 @@ use rand_core::{OsRng, RngCore}; use tonic::{Request, Response, Status}; use iam_store::CredentialStore; -use iam_types::{Argon2Params, CredentialRecord}; +use iam_types::{Argon2Params, CredentialRecord, PrincipalKind as TypesPrincipalKind}; use crate::proto::{ iam_credential_server::IamCredential, CreateS3CredentialRequest, CreateS3CredentialResponse, Credential, GetSecretKeyRequest, GetSecretKeyResponse, - ListCredentialsRequest, ListCredentialsResponse, RevokeCredentialRequest, + ListCredentialsRequest, ListCredentialsResponse, PrincipalKind, RevokeCredentialRequest, RevokeCredentialResponse, }; @@ -95,6 +95,15 @@ impl IamCredentialService { } } +fn map_principal_kind(kind: i32) -> Result { + match PrincipalKind::try_from(kind).unwrap_or(PrincipalKind::Unspecified) { + PrincipalKind::User => Ok(TypesPrincipalKind::User), + PrincipalKind::ServiceAccount => Ok(TypesPrincipalKind::ServiceAccount), + PrincipalKind::Group => Ok(TypesPrincipalKind::Group), + PrincipalKind::Unspecified => Err(Status::invalid_argument("principal_kind is required")), + } +} + #[tonic::async_trait] impl IamCredential for IamCredentialService { async fn create_s3_credential( @@ -103,6 +112,7 @@ impl IamCredential for IamCredentialService { ) -> Result, Status> { let req = request.into_inner(); let now = now_ts(); + let principal_kind = map_principal_kind(req.principal_kind)?; let (secret_b64, raw_secret) = Self::generate_secret(); let (hash, kdf) = Self::hash_secret(&raw_secret); let secret_enc = self.encrypt_secret(&raw_secret)?; @@ -111,6 +121,9 @@ impl IamCredential for IamCredentialService { let record = CredentialRecord { access_key_id: access_key_id.clone(), principal_id: req.principal_id.clone(), + principal_kind, + org_id: req.org_id.clone(), + project_id: req.project_id.clone(), created_at: now, expires_at: req.expires_at, revoked: false, @@ -168,6 +181,13 @@ impl IamCredential for IamCredentialService { secret_key: STANDARD.encode(secret), principal_id: record.principal_id, expires_at: record.expires_at, + org_id: record.org_id, + project_id: record.project_id, + principal_kind: match record.principal_kind { + TypesPrincipalKind::User => PrincipalKind::User as i32, + TypesPrincipalKind::ServiceAccount => PrincipalKind::ServiceAccount as i32, + TypesPrincipalKind::Group => PrincipalKind::Group as i32, + }, })) } @@ -190,6 +210,13 @@ impl IamCredential for IamCredentialService { expires_at: c.expires_at, revoked: c.revoked, description: c.description.unwrap_or_default(), + org_id: c.org_id, + project_id: c.project_id, + principal_kind: match c.principal_kind { + TypesPrincipalKind::User => PrincipalKind::User as i32, + TypesPrincipalKind::ServiceAccount => PrincipalKind::ServiceAccount as i32, + TypesPrincipalKind::Group => PrincipalKind::Group as i32, + }, }) .collect(); Ok(Response::new(ListCredentialsResponse { credentials: creds })) @@ -230,6 +257,9 @@ mod tests { principal_id: "p1".into(), description: "".into(), expires_at: None, + org_id: Some("org-a".into()), + project_id: Some("project-a".into()), + principal_kind: PrincipalKind::ServiceAccount as i32, })) .await .unwrap() @@ -247,6 +277,9 @@ mod tests { let fetched = STANDARD.decode(get.secret_key).unwrap(); assert_eq!(orig, fetched); assert_eq!(get.principal_id, "p1"); + assert_eq!(get.org_id.as_deref(), Some("org-a")); + assert_eq!(get.project_id.as_deref(), Some("project-a")); + assert_eq!(get.principal_kind, PrincipalKind::ServiceAccount as i32); } #[tokio::test] @@ -257,6 +290,9 @@ mod tests { principal_id: "pA".into(), description: "".into(), expires_at: None, + org_id: Some("org-a".into()), + project_id: Some("project-a".into()), + principal_kind: PrincipalKind::ServiceAccount as i32, })) .await .unwrap() @@ -266,6 +302,9 @@ mod tests { principal_id: "pB".into(), description: "".into(), expires_at: None, + org_id: Some("org-b".into()), + project_id: Some("project-b".into()), + principal_kind: PrincipalKind::ServiceAccount as i32, })) .await .unwrap(); @@ -289,6 +328,9 @@ mod tests { principal_id: "p1".into(), description: "".into(), expires_at: None, + org_id: Some("org-a".into()), + project_id: Some("project-a".into()), + principal_kind: PrincipalKind::ServiceAccount as i32, })) .await .unwrap() @@ -297,7 +339,6 @@ mod tests { let revoke1 = svc .revoke_credential(Request::new(RevokeCredentialRequest { access_key_id: created.access_key_id.clone(), - reason: "test".into(), })) .await .unwrap() @@ -307,7 +348,6 @@ mod tests { let revoke2 = svc .revoke_credential(Request::new(RevokeCredentialRequest { access_key_id: created.access_key_id.clone(), - reason: "again".into(), })) .await .unwrap() @@ -330,6 +370,9 @@ mod tests { let expired = CredentialRecord { access_key_id: "expired-ak".into(), principal_id: "p1".into(), + principal_kind: TypesPrincipalKind::ServiceAccount, + org_id: Some("org-a".into()), + project_id: Some("project-a".into()), created_at: now_ts(), expires_at: Some(now_ts() - 10), revoked: false, diff --git a/iam/crates/iam-api/src/lib.rs b/iam/crates/iam-api/src/lib.rs index bd47178..bc3a694 100644 --- a/iam/crates/iam-api/src/lib.rs +++ b/iam/crates/iam-api/src/lib.rs @@ -1,4 +1,5 @@ mod conversions; +mod credential_service; mod gateway_auth_service; mod generated; pub mod iam_service; @@ -8,7 +9,10 @@ pub mod proto { pub use crate::generated::iam::v1::*; } -pub use generated::iam::v1::{iam_admin_server, iam_authz_server, iam_token_server}; +pub use generated::iam::v1::{ + iam_admin_server, iam_authz_server, iam_credential_server, iam_token_server, +}; +pub use credential_service::IamCredentialService; pub use gateway_auth_service::GatewayAuthServiceImpl; pub use iam_service::{IamAdminService, IamAuthzService}; pub use token_service::IamTokenService; diff --git a/iam/crates/iam-client/src/client.rs b/iam/crates/iam-client/src/client.rs index de92126..738fbf1 100644 --- a/iam/crates/iam-client/src/client.rs +++ b/iam/crates/iam-client/src/client.rs @@ -2,6 +2,7 @@ //! //! Provides a thin gRPC client for interacting with the IAM service. +use std::future::Future; use std::time::Duration; use iam_api::proto::{ @@ -19,6 +20,10 @@ use iam_types::{ }; use tonic::transport::{Channel, ClientTlsConfig, Endpoint}; +const TRANSIENT_RPC_RETRY_ATTEMPTS: usize = 3; +const TRANSIENT_RPC_INITIAL_BACKOFF: Duration = Duration::from_millis(200); +const TRANSIENT_RPC_MAX_BACKOFF: Duration = Duration::from_millis(1_000); + /// Configuration for the IAM client #[derive(Debug, Clone)] pub struct IamClientConfig { @@ -100,6 +105,40 @@ impl IamClient { IamTokenClient::new(self.channel.clone()) } + async fn call_with_retry(operation: &'static str, mut op: F) -> Result + where + F: FnMut() -> Fut, + Fut: Future>, + { + let mut last_status = None; + for attempt in 0..TRANSIENT_RPC_RETRY_ATTEMPTS { + match op().await { + Ok(value) => return Ok(value), + Err(status) + if attempt + 1 < TRANSIENT_RPC_RETRY_ATTEMPTS + && is_retryable_status(&status) => + { + let delay = retry_delay(attempt); + tracing::warn!( + operation, + attempt = attempt + 1, + retry_after_ms = delay.as_millis() as u64, + code = ?status.code(), + message = status.message(), + "retrying transient IAM RPC" + ); + last_status = Some(status); + tokio::time::sleep(delay).await; + } + Err(status) => return Err(map_status(status)), + } + } + + Err(map_status(last_status.unwrap_or_else(|| { + tonic::Status::internal(format!("IAM RPC {operation} failed without a status")) + }))) + } + // ======================================================================== // Authorization APIs // ======================================================================== @@ -128,7 +167,6 @@ impl IamClient { resource: &Resource, context: std::collections::HashMap, ) -> Result { - let mut client = self.authz_client(); let request = AuthorizeRequest { principal: Some(to_proto_principal_ref(&principal.to_ref())), action: action.to_string(), @@ -151,11 +189,13 @@ impl IamClient { }), }; - let resp = client - .authorize(request) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("authorize", || { + let mut client = self.authz_client(); + let request = request.clone(); + async move { client.authorize(request).await } + }) + .await? + .into_inner(); Ok(resp.allowed) } @@ -166,7 +206,6 @@ impl IamClient { /// Create a new user pub async fn create_user(&self, id: &str, name: &str) -> Result { - let mut client = self.admin_client(); let req = CreatePrincipalRequest { id: id.into(), kind: ProtoPrincipalKind::User as i32, @@ -177,25 +216,31 @@ impl IamClient { metadata: Default::default(), }; - let resp = client - .create_principal(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("create_principal", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.create_principal(req).await } + }) + .await? + .into_inner(); Ok(ProtoPrincipal::into(resp)) } /// Get a principal pub async fn get_principal(&self, principal_ref: &PrincipalRef) -> Result> { - let mut client = self.admin_client(); let req = GetPrincipalRequest { principal: Some(to_proto_principal_ref(principal_ref)), }; - let resp = client.get_principal(req).await; + let resp = Self::call_with_retry("get_principal", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.get_principal(req).await } + }) + .await; match resp { Ok(r) => Ok(Some(ProtoPrincipal::into(r.into_inner()))), - Err(status) if status.code() == tonic::Code::NotFound => Ok(None), - Err(status) => Err(map_status(status)), + Err(Error::Internal(message)) if tonic_not_found(&message) => Ok(None), + Err(err) => Err(err), } } @@ -206,7 +251,6 @@ impl IamClient { name: &str, project_id: &str, ) -> Result { - let mut client = self.admin_client(); let req = CreatePrincipalRequest { id: id.into(), kind: ProtoPrincipalKind::ServiceAccount as i32, @@ -216,17 +260,18 @@ impl IamClient { email: None, metadata: Default::default(), }; - let resp = client - .create_principal(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("create_service_account", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.create_principal(req).await } + }) + .await? + .into_inner(); Ok(ProtoPrincipal::into(resp)) } /// List users pub async fn list_users(&self) -> Result> { - let mut client = self.admin_client(); let req = ListPrincipalsRequest { kind: Some(ProtoPrincipalKind::User as i32), org_id: None, @@ -235,11 +280,13 @@ impl IamClient { page_token: String::new(), }; - let resp = client - .list_principals(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("list_principals", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.list_principals(req).await } + }) + .await? + .into_inner(); Ok(resp .principals @@ -254,36 +301,40 @@ impl IamClient { /// Get a role by name pub async fn get_role(&self, name: &str) -> Result> { - let mut client = self.admin_client(); let req = GetRoleRequest { name: name.into() }; - let resp = client.get_role(req).await; + let resp = Self::call_with_retry("get_role", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.get_role(req).await } + }) + .await; match resp { Ok(r) => Ok(Some(r.into_inner().into())), - Err(status) if status.code() == tonic::Code::NotFound => Ok(None), - Err(status) => Err(map_status(status)), + Err(Error::Internal(message)) if tonic_not_found(&message) => Ok(None), + Err(err) => Err(err), } } /// List all roles pub async fn list_roles(&self) -> Result> { - let mut client = self.admin_client(); let req = ListRolesRequest { scope: None, include_builtin: true, page_size: 0, page_token: String::new(), }; - let resp = client - .list_roles(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("list_roles", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.list_roles(req).await } + }) + .await? + .into_inner(); Ok(resp.roles.into_iter().map(Into::into).collect()) } /// Create a custom role pub async fn create_role(&self, role: &Role) -> Result { - let mut client = self.admin_client(); let req = CreateRoleRequest { name: role.name.clone(), display_name: role.display_name.clone(), @@ -297,11 +348,13 @@ impl IamClient { .collect(), }; - let resp = client - .create_role(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("create_role", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.create_role(req).await } + }) + .await? + .into_inner(); Ok(resp.into()) } @@ -311,7 +364,6 @@ impl IamClient { /// Create a policy binding pub async fn create_binding(&self, binding: &PolicyBinding) -> Result { - let mut client = self.admin_client(); let req = CreateBindingRequest { principal: Some(to_proto_principal_ref(&binding.principal_ref)), role: binding.role_ref.clone(), @@ -320,25 +372,28 @@ impl IamClient { expires_at: binding.expires_at, }; - let resp = client - .create_binding(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("create_binding", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.create_binding(req).await } + }) + .await? + .into_inner(); Ok(resp.into()) } /// Delete a policy binding pub async fn delete_binding(&self, binding_id: &str) -> Result { - let mut client = self.admin_client(); let req = DeleteBindingRequest { id: binding_id.into(), }; - let resp = client - .delete_binding(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("delete_binding", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.delete_binding(req).await } + }) + .await? + .into_inner(); Ok(resp.deleted) } @@ -347,7 +402,6 @@ impl IamClient { &self, principal: &PrincipalRef, ) -> Result> { - let mut client = self.admin_client(); let req = ListBindingsRequest { principal: Some(to_proto_principal_ref(principal)), role: None, @@ -357,17 +411,18 @@ impl IamClient { page_token: String::new(), }; - let resp = client - .list_bindings(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("list_bindings_for_principal", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.list_bindings(req).await } + }) + .await? + .into_inner(); Ok(resp.bindings.into_iter().map(Into::into).collect()) } /// List bindings for a scope pub async fn list_bindings_for_scope(&self, scope: &Scope) -> Result> { - let mut client = self.admin_client(); let req = ListBindingsRequest { principal: None, role: None, @@ -377,11 +432,13 @@ impl IamClient { page_token: String::new(), }; - let resp = client - .list_bindings(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("list_bindings_for_scope", || { + let mut client = self.admin_client(); + let req = req.clone(); + async move { client.list_bindings(req).await } + }) + .await? + .into_inner(); Ok(resp.bindings.into_iter().map(Into::into).collect()) } @@ -397,7 +454,6 @@ impl IamClient { scope: Scope, ttl_seconds: u64, ) -> Result { - let mut client = self.token_client(); let req = IssueTokenRequest { principal_id: principal.id.clone(), principal_kind: match principal.kind { @@ -410,25 +466,28 @@ impl IamClient { ttl_seconds, }; - let resp = client - .issue_token(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("issue_token", || { + let mut client = self.token_client(); + let req = req.clone(); + async move { client.issue_token(req).await } + }) + .await? + .into_inner(); Ok(resp.token) } /// Validate a token pub async fn validate_token(&self, token: &str) -> Result { - let mut client = self.token_client(); let req = ValidateTokenRequest { token: token.to_string(), }; - let resp = client - .validate_token(req) - .await - .map_err(map_status)? - .into_inner(); + let resp = Self::call_with_retry("validate_token", || { + let mut client = self.token_client(); + let req = req.clone(); + async move { client.validate_token(req).await } + }) + .await? + .into_inner(); if !resp.valid { return Err(Error::Iam(IamError::InvalidToken(resp.reason.clone()))); @@ -479,20 +538,55 @@ impl IamClient { /// Revoke a token pub async fn revoke_token(&self, token: &str) -> Result<()> { - let mut client = self.token_client(); let req = RevokeTokenRequest { token: token.to_string(), reason: "client revoke".into(), }; - client - .revoke_token(req) - .await - .map_err(map_status)? - .into_inner(); + Self::call_with_retry("revoke_token", || { + let mut client = self.token_client(); + let req = req.clone(); + async move { client.revoke_token(req).await } + }) + .await? + .into_inner(); Ok(()) } } +fn retry_delay(attempt: usize) -> Duration { + TRANSIENT_RPC_INITIAL_BACKOFF + .saturating_mul(1u32 << attempt.min(3)) + .min(TRANSIENT_RPC_MAX_BACKOFF) +} + +fn is_retryable_status(status: &tonic::Status) -> bool { + matches!( + status.code(), + tonic::Code::Unavailable + | tonic::Code::Cancelled + | tonic::Code::DeadlineExceeded + | tonic::Code::Unknown + ) || retryable_message(status.message()) +} + +fn retryable_message(message: &str) -> bool { + let lower = message.to_ascii_lowercase(); + [ + "transport error", + "connection was not ready", + "h2 protocol error", + "broken pipe", + "connection refused", + "connection reset", + ] + .iter() + .any(|needle| lower.contains(needle)) +} + +fn tonic_not_found(message: &str) -> bool { + message.contains("status: NotFound") || message.contains("code: NotFound") +} + fn map_status(status: tonic::Status) -> Error { Error::Internal(status.to_string()) } @@ -507,3 +601,75 @@ fn to_proto_principal_ref(principal_ref: &PrincipalRef) -> ProtoPrincipalRef { id: principal_ref.id.clone(), } } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; + + #[test] + fn retryable_message_covers_connection_readiness() { + assert!(retryable_message("transport error")); + assert!(retryable_message("connection was not ready")); + assert!(retryable_message("h2 protocol error")); + assert!(!retryable_message("permission denied")); + } + + #[test] + fn retry_delay_is_capped() { + assert_eq!(retry_delay(0), Duration::from_millis(200)); + assert_eq!(retry_delay(1), Duration::from_millis(400)); + assert_eq!(retry_delay(2), Duration::from_millis(800)); + assert_eq!(retry_delay(3), Duration::from_millis(1000)); + assert_eq!(retry_delay(7), Duration::from_millis(1000)); + } + + #[tokio::test(start_paused = true)] + async fn call_with_retry_retries_transient_statuses() { + let attempts = Arc::new(AtomicUsize::new(0)); + let attempts_for_task = attempts.clone(); + let task = tokio::spawn(async move { + IamClient::call_with_retry("test", || { + let attempts = attempts_for_task.clone(); + async move { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + if attempt < 2 { + Err(tonic::Status::unavailable("connection was not ready")) + } else { + Ok("ok") + } + } + }) + .await + }); + + tokio::time::advance(Duration::from_secs(3)).await; + assert_eq!(task.await.unwrap().unwrap(), "ok"); + assert_eq!(attempts.load(Ordering::SeqCst), 3); + } + + #[tokio::test(start_paused = true)] + async fn call_with_retry_stops_on_non_retryable_status() { + let attempts = Arc::new(AtomicUsize::new(0)); + let attempts_for_task = attempts.clone(); + + let err = IamClient::call_with_retry("test", || { + let attempts = attempts_for_task.clone(); + async move { + attempts.fetch_add(1, Ordering::SeqCst); + Err::<(), _>(tonic::Status::permission_denied("nope")) + } + }) + .await + .unwrap_err(); + + assert_eq!(attempts.load(Ordering::SeqCst), 1); + match err { + Error::Internal(message) => assert!(message.contains("PermissionDenied")), + other => panic!("unexpected error: {other:?}"), + } + } +} diff --git a/iam/crates/iam-server/src/main.rs b/iam/crates/iam-server/src/main.rs index 1b79c8c..338ce78 100644 --- a/iam/crates/iam-server/src/main.rs +++ b/iam/crates/iam-server/src/main.rs @@ -20,12 +20,15 @@ use tracing::{info, warn}; use iam_api::{ iam_admin_server::IamAdminServer, iam_authz_server::IamAuthzServer, - iam_token_server::IamTokenServer, GatewayAuthServiceImpl, GatewayAuthServiceServer, - IamAdminService, IamAuthzService, IamTokenService, + iam_credential_server::IamCredentialServer, iam_token_server::IamTokenServer, + GatewayAuthServiceImpl, GatewayAuthServiceServer, IamAdminService, IamAuthzService, + IamCredentialService, IamTokenService, }; use iam_authn::{InternalTokenConfig, InternalTokenService, SigningKey}; use iam_authz::{PolicyCache, PolicyCacheConfig, PolicyEvaluator}; -use iam_store::{Backend, BackendConfig, BindingStore, PrincipalStore, RoleStore, TokenStore}; +use iam_store::{ + Backend, BackendConfig, BindingStore, CredentialStore, PrincipalStore, RoleStore, TokenStore, +}; use config::{BackendKind, ServerConfig}; @@ -190,6 +193,7 @@ async fn main() -> Result<(), Box> { let principal_store = Arc::new(PrincipalStore::new(backend.clone())); let role_store = Arc::new(RoleStore::new(backend.clone())); let binding_store = Arc::new(BindingStore::new(backend.clone())); + let credential_store = Arc::new(CredentialStore::new(backend.clone())); let token_store = Arc::new(TokenStore::new(backend.clone())); // Initialize builtin roles @@ -238,7 +242,8 @@ async fn main() -> Result<(), Box> { ) }; - let token_config = InternalTokenConfig::new(signing_key, &config.authn.internal_token.issuer) + let token_config = + InternalTokenConfig::new(signing_key.clone(), &config.authn.internal_token.issuer) .with_default_ttl(Duration::from_secs( config.authn.internal_token.default_ttl_seconds, )) @@ -248,6 +253,16 @@ async fn main() -> Result<(), Box> { let token_service = Arc::new(InternalTokenService::new(token_config)); let admin_token = load_admin_token(); + let credential_master_key = std::env::var("IAM_CRED_MASTER_KEY") + .ok() + .map(|value| value.into_bytes()) + .filter(|value| value.len() == 32) + .unwrap_or_else(|| { + warn!( + "IAM_CRED_MASTER_KEY missing or not 32 bytes, deriving credential key from signing key", + ); + signing_key.sign(b"iam-credential-master-key") + }); // Create gRPC services let authz_service = IamAuthzService::new(evaluator.clone(), principal_store.clone()); @@ -262,6 +277,9 @@ async fn main() -> Result<(), Box> { token_store.clone(), evaluator.clone(), ); + let credential_service = + IamCredentialService::new(credential_store, &credential_master_key, "iam-cred-master") + .map_err(|e| format!("Failed to initialize credential service: {}", e))?; let admin_service = IamAdminService::new( principal_store.clone(), role_store.clone(), @@ -291,6 +309,9 @@ async fn main() -> Result<(), Box> { health_reporter .set_serving::>() .await; + health_reporter + .set_serving::>() + .await; health_reporter .set_serving::>() .await; @@ -357,6 +378,7 @@ async fn main() -> Result<(), Box> { .add_service(health_service) .add_service(IamAuthzServer::new(authz_service)) .add_service(IamTokenServer::new(token_grpc_service)) + .add_service(IamCredentialServer::new(credential_service)) .add_service(GatewayAuthServiceServer::new(gateway_auth_service)) .add_service(admin_server) .serve(config.server.addr); diff --git a/iam/crates/iam-service-auth/Cargo.toml b/iam/crates/iam-service-auth/Cargo.toml index 651736b..ae5c55c 100644 --- a/iam/crates/iam-service-auth/Cargo.toml +++ b/iam/crates/iam-service-auth/Cargo.toml @@ -9,5 +9,6 @@ iam-client = { path = "../iam-client" } iam-types = { path = "../iam-types" } tonic = { workspace = true } tracing = { workspace = true } +tokio = { workspace = true } http = "1" serde_json = "1" diff --git a/iam/crates/iam-service-auth/src/lib.rs b/iam/crates/iam-service-auth/src/lib.rs index 78c0488..0ed8cbc 100644 --- a/iam/crates/iam-service-auth/src/lib.rs +++ b/iam/crates/iam-service-auth/src/lib.rs @@ -16,6 +16,9 @@ use tracing::{debug, warn}; const PHOTON_AUTH_TOKEN_HEADER: &str = "x-photon-auth-token"; const DEFAULT_TOKEN_CACHE_TTL_MS: u64 = 5_000; const DEFAULT_AUTHZ_CACHE_TTL_MS: u64 = 3_000; +const AUTH_CONNECT_RETRY_ATTEMPTS: usize = 6; +const AUTH_CONNECT_INITIAL_BACKOFF: Duration = Duration::from_millis(500); +const AUTH_CONNECT_MAX_BACKOFF: Duration = Duration::from_secs(5); #[derive(Debug, Clone)] struct CacheEntry { @@ -64,9 +67,7 @@ impl AuthService { config = config.without_tls(); } - let iam_client = IamClient::connect(config) - .await - .map_err(|e| format!("Failed to connect to IAM server: {}", e))?; + let iam_client = connect_iam_with_retry(config).await?; Ok(Self { iam_client: Arc::new(iam_client), @@ -273,6 +274,59 @@ impl AuthService { } } +async fn connect_iam_with_retry(config: IamClientConfig) -> Result { + let mut last_error = None; + for attempt in 0..AUTH_CONNECT_RETRY_ATTEMPTS { + match IamClient::connect(config.clone()).await { + Ok(client) => return Ok(client), + Err(err) + if attempt + 1 < AUTH_CONNECT_RETRY_ATTEMPTS + && retryable_connect_error(&err.to_string()) => + { + let delay = auth_connect_retry_delay(attempt); + warn!( + attempt = attempt + 1, + retry_after_ms = delay.as_millis() as u64, + error = %err, + "retrying IAM auth service bootstrap connection" + ); + last_error = Some(err.to_string()); + tokio::time::sleep(delay).await; + } + Err(err) => { + return Err(format!("Failed to connect to IAM server: {}", err)); + } + } + } + + Err(format!( + "Failed to connect to IAM server: {}", + last_error.unwrap_or_else(|| "unknown connection error".to_string()) + )) +} + +fn auth_connect_retry_delay(attempt: usize) -> Duration { + AUTH_CONNECT_INITIAL_BACKOFF + .saturating_mul(1u32 << attempt.min(4)) + .min(AUTH_CONNECT_MAX_BACKOFF) +} + +fn retryable_connect_error(message: &str) -> bool { + let lower = message.to_ascii_lowercase(); + [ + "transport error", + "connection refused", + "connection was not ready", + "operation timed out", + "deadline has elapsed", + "dns error", + "broken pipe", + "connection reset", + ] + .iter() + .any(|needle| lower.contains(needle)) +} + fn prune_expired(cache: &mut HashMap>) { let now = Instant::now(); cache.retain(|_, entry| entry.expires_at > now); @@ -400,6 +454,29 @@ fn extract_token_from_metadata(metadata: &MetadataMap) -> Result )) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn retryable_connect_error_matches_transport_failures() { + assert!(retryable_connect_error("Internal error: transport error")); + assert!(retryable_connect_error("connection was not ready")); + assert!(retryable_connect_error("deadline has elapsed")); + assert!(!retryable_connect_error("permission denied")); + } + + #[test] + fn auth_connect_retry_delay_is_capped() { + assert_eq!(auth_connect_retry_delay(0), Duration::from_millis(500)); + assert_eq!(auth_connect_retry_delay(1), Duration::from_millis(1000)); + assert_eq!(auth_connect_retry_delay(2), Duration::from_millis(2000)); + assert_eq!(auth_connect_retry_delay(3), Duration::from_millis(4000)); + assert_eq!(auth_connect_retry_delay(4), Duration::from_secs(5)); + assert_eq!(auth_connect_retry_delay(8), Duration::from_secs(5)); + } +} + fn extract_token_from_headers(headers: &HeaderMap) -> Result { if let Some(auth_header) = headers.get(AUTHORIZATION) { let auth_str = auth_header diff --git a/iam/crates/iam-store/src/credential_store.rs b/iam/crates/iam-store/src/credential_store.rs index 9e2719e..abc2316 100644 --- a/iam/crates/iam-store/src/credential_store.rs +++ b/iam/crates/iam-store/src/credential_store.rs @@ -1,24 +1,25 @@ //! Credential storage (access/secret key metadata) +use std::sync::Arc; + use iam_types::{CredentialRecord, Result}; -use crate::backend::JsonStore; -use crate::{DynMetadataClient, MetadataClient}; +use crate::backend::{Backend, CasResult, JsonStore, StorageBackend}; /// Store for credentials (S3/API keys) pub struct CredentialStore { - client: DynMetadataClient, + backend: Arc, } impl JsonStore for CredentialStore { - fn client(&self) -> &dyn MetadataClient { - self.client.as_ref() + fn backend(&self) -> &Backend { + &self.backend } } impl CredentialStore { - pub fn new(client: DynMetadataClient) -> Self { - Self { client } + pub fn new(backend: Arc) -> Self { + Self { backend } } pub async fn put(&self, record: &CredentialRecord) -> Result { @@ -36,13 +37,17 @@ impl CredentialStore { principal_id: &str, limit: u32, ) -> Result> { - // scan prefix and filter by principal_id; small cardinality expected let prefix = b"iam/credentials/"; - let items = self.scan_prefix_json::(prefix, limit).await?; - Ok(items - .into_iter() - .filter(|rec| rec.principal_id == principal_id) - .collect()) + let items = self.backend.scan_prefix(prefix, limit).await?; + let mut credentials = Vec::new(); + for pair in items { + let record: CredentialRecord = serde_json::from_slice(&pair.value) + .map_err(|e| iam_types::Error::Serialization(e.to_string()))?; + if record.principal_id == principal_id { + credentials.push(record); + } + } + Ok(credentials) } pub async fn revoke(&self, access_key_id: &str) -> Result { @@ -56,13 +61,10 @@ impl CredentialStore { return Ok(false); } record.revoked = true; - match self - .cas_json(key.as_bytes(), version, &record) - .await? - { - crate::CasResult::Success(_) => Ok(true), - crate::CasResult::Conflict { .. } => Ok(false), - crate::CasResult::NotFound => Ok(false), + match self.cas_json(key.as_bytes(), version, &record).await? { + CasResult::Success(_) => Ok(true), + CasResult::Conflict { .. } => Ok(false), + CasResult::NotFound => Ok(false), } } } diff --git a/iam/crates/iam-store/src/lib.rs b/iam/crates/iam-store/src/lib.rs index 9e0b969..47a13ea 100644 --- a/iam/crates/iam-store/src/lib.rs +++ b/iam/crates/iam-store/src/lib.rs @@ -7,6 +7,7 @@ pub mod backend; pub mod binding_store; +pub mod credential_store; pub mod group_store; pub mod principal_store; pub mod role_store; @@ -14,6 +15,7 @@ pub mod token_store; pub use backend::{Backend, BackendConfig, CasResult, KvPair, StorageBackend}; pub use binding_store::BindingStore; +pub use credential_store::CredentialStore; pub use group_store::GroupStore; pub use principal_store::PrincipalStore; pub use role_store::RoleStore; diff --git a/iam/crates/iam-types/src/credential.rs b/iam/crates/iam-types/src/credential.rs index 817bb9c..e001ea9 100644 --- a/iam/crates/iam-types/src/credential.rs +++ b/iam/crates/iam-types/src/credential.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; +use crate::PrincipalKind; + /// Argon2 parameters used to hash the secret key #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Argon2Params { @@ -17,6 +19,9 @@ pub struct Argon2Params { pub struct CredentialRecord { pub access_key_id: String, pub principal_id: String, + pub principal_kind: PrincipalKind, + pub org_id: Option, + pub project_id: Option, pub created_at: u64, pub expires_at: Option, pub revoked: bool, diff --git a/iam/crates/iam-types/src/lib.rs b/iam/crates/iam-types/src/lib.rs index bffbac0..ba1dda4 100644 --- a/iam/crates/iam-types/src/lib.rs +++ b/iam/crates/iam-types/src/lib.rs @@ -10,6 +10,7 @@ //! - Error types pub mod condition; +pub mod credential; pub mod error; pub mod policy; pub mod principal; @@ -19,6 +20,7 @@ pub mod scope; pub mod token; pub use condition::{Condition, ConditionExpr}; +pub use credential::{Argon2Params, CredentialRecord}; pub use error::{Error, IamError, Result, StorageError}; pub use policy::{CreateBindingRequest, EffectivePolicy, PolicyBinding}; pub use principal::{Principal, PrincipalKind, PrincipalRef}; diff --git a/iam/proto/iam.proto b/iam/proto/iam.proto index deb7e74..200e12e 100644 --- a/iam/proto/iam.proto +++ b/iam/proto/iam.proto @@ -89,6 +89,14 @@ service IamToken { rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse); } +// IamCredential manages S3-style access/secret key credentials. +service IamCredential { + rpc CreateS3Credential(CreateS3CredentialRequest) returns (CreateS3CredentialResponse); + rpc GetSecretKey(GetSecretKeyRequest) returns (GetSecretKeyResponse); + rpc ListCredentials(ListCredentialsRequest) returns (ListCredentialsResponse); + rpc RevokeCredential(RevokeCredentialRequest) returns (RevokeCredentialResponse); +} + message IssueTokenRequest { // Principal to issue token for string principal_id = 1; @@ -162,6 +170,63 @@ message RefreshTokenResponse { uint64 expires_at = 2; } +message CreateS3CredentialRequest { + string principal_id = 1; + string description = 2; + optional uint64 expires_at = 3; + optional string org_id = 4; + optional string project_id = 5; + PrincipalKind principal_kind = 6; +} + +message CreateS3CredentialResponse { + string access_key_id = 1; + string secret_key = 2; + uint64 created_at = 3; + optional uint64 expires_at = 4; +} + +message GetSecretKeyRequest { + string access_key_id = 1; +} + +message GetSecretKeyResponse { + string secret_key = 1; + string principal_id = 2; + optional uint64 expires_at = 3; + optional string org_id = 4; + optional string project_id = 5; + PrincipalKind principal_kind = 6; +} + +message ListCredentialsRequest { + string principal_id = 1; +} + +message Credential { + string access_key_id = 1; + string principal_id = 2; + uint64 created_at = 3; + optional uint64 expires_at = 4; + bool revoked = 5; + string description = 6; + optional string org_id = 7; + optional string project_id = 8; + PrincipalKind principal_kind = 9; +} + +message ListCredentialsResponse { + repeated Credential credentials = 1; +} + +message RevokeCredentialRequest { + string access_key_id = 1; +} + +message RevokeCredentialResponse { + bool success = 1; +} + message InternalTokenClaims { string principal_id = 1; PrincipalKind principal_kind = 2; diff --git a/k8shost/Cargo.lock b/k8shost/Cargo.lock index 51ba8a3..47d8064 100644 --- a/k8shost/Cargo.lock +++ b/k8shost/Cargo.lock @@ -2,13 +2,48 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -51,9 +86,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -66,15 +101,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -101,9 +136,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -116,6 +151,18 @@ dependencies = [ "tonic-build 0.12.3", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -141,7 +188,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -152,7 +199,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -178,9 +225,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -188,9 +235,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -220,7 +267,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 1.0.2", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -231,7 +278,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "bytes", "form_urlencoded", "futures-util", @@ -253,7 +300,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -281,9 +328,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -316,6 +363,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -324,9 +377,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -340,6 +393,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -351,32 +413,33 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecheck" @@ -408,15 +471,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -476,9 +539,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -489,10 +552,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -500,9 +573,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -512,36 +585,36 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -711,9 +784,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -730,9 +813,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -756,7 +839,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -852,9 +935,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -862,6 +945,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flaredb-client" version = "0.1.0" @@ -956,9 +1045,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -971,9 +1060,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -981,15 +1070,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1009,38 +1098,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1050,7 +1139,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1066,9 +1154,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1091,6 +1179,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -1109,7 +1207,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1118,9 +1216,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1128,7 +1226,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1325,7 +1423,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "httparse", @@ -1362,13 +1460,13 @@ dependencies = [ "hyper 1.8.1", "hyper-util", "log", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -1386,14 +1484,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1402,7 +1499,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1412,7 +1509,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64 0.22.1", "iam-audit", @@ -1422,6 +1521,7 @@ dependencies = [ "iam-types", "prost 0.13.5", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1458,7 +1558,7 @@ dependencies = [ "iam-types", "jsonwebtoken", "rand 0.8.5", - "reqwest 0.12.24", + "reqwest 0.12.28", "serde", "serde_json", "sha2", @@ -1507,6 +1607,7 @@ dependencies = [ "iam-client", "iam-types", "serde_json", + "tokio", "tonic", "tracing", ] @@ -1542,9 +1643,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1612,9 +1713,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1626,9 +1727,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1678,19 +1779,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1703,9 +1813,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1727,10 +1837,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.15" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1744,9 +1863,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1893,9 +2012,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" @@ -1903,7 +2022,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "plain", "redox_syscall 0.7.3", @@ -1954,9 +2073,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2018,9 +2137,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -2043,7 +2162,7 @@ dependencies = [ "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -2162,9 +2281,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2173,10 +2292,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "ordered-multimap" @@ -2217,6 +2342,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -2241,9 +2377,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -2251,9 +2387,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -2261,22 +2397,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -2288,8 +2424,18 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", - "indexmap 2.12.1", + "fixedbitset 0.4.2", + "indexmap 2.13.0", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", + "indexmap 2.13.0", ] [[package]] @@ -2302,29 +2448,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2426,7 +2572,7 @@ dependencies = [ "plasmavmc-types", "prismnet-api", "prost 0.13.5", - "reqwest 0.12.24", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 1.0.69", @@ -2450,10 +2596,22 @@ dependencies = [ ] [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "polyval" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -2486,7 +2644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2549,9 +2707,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2584,16 +2742,16 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.12.1", "log", "multimap", "once_cell", - "petgraph", + "petgraph 0.6.5", "prettyplease", "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.111", + "syn 2.0.117", "tempfile", ] @@ -2604,16 +2762,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "once_cell", - "petgraph", + "petgraph 0.7.1", "prettyplease", "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.111", + "syn 2.0.117", "tempfile", ] @@ -2624,10 +2782,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2637,10 +2795,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2772,9 +2930,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", - "socket2 0.6.1", - "thiserror 2.0.17", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2782,9 +2940,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2792,10 +2950,10 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2810,16 +2968,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2854,7 +3012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2874,7 +3032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2883,14 +3041,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2901,7 +3059,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2910,7 +3068,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2919,14 +3077,14 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2936,9 +3094,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2947,9 +3105,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -3003,9 +3161,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -3021,7 +3179,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -3029,14 +3187,14 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -3047,7 +3205,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3055,9 +3213,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -3073,9 +3231,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -3105,9 +3263,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" dependencies = [ "arrayvec", "borsh", @@ -3127,11 +3285,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -3152,25 +3310,25 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3198,9 +3356,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -3218,9 +3376,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -3236,15 +3394,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3273,11 +3431,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3286,9 +3444,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3311,7 +3469,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3385,10 +3543,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3406,7 +3565,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -3418,9 +3577,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3443,12 +3602,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3490,17 +3649,17 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", "percent-encoding", - "rustls 0.23.35", + "rustls 0.23.37", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -3518,7 +3677,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3540,7 +3699,7 @@ dependencies = [ "sqlx-core", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.111", + "syn 2.0.117", "tokio", "url", ] @@ -3553,7 +3712,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -3577,7 +3736,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -3601,7 +3760,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", ] @@ -3648,9 +3807,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3680,7 +3839,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3712,9 +3871,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -3734,11 +3893,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -3749,18 +3908,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3815,9 +3974,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3830,9 +3989,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3840,20 +3999,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3872,15 +4031,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.37", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3889,9 +4048,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3945,12 +4104,12 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3959,19 +4118,19 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "toml_datetime 0.7.0", "toml_parser", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -3991,7 +4150,7 @@ dependencies = [ "axum 0.7.9", "base64 0.22.1", "bytes", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -4023,7 +4182,7 @@ dependencies = [ "proc-macro2", "prost-build 0.12.6", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4037,7 +4196,7 @@ dependencies = [ "prost-build 0.13.5", "prost-types 0.13.5", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4075,9 +4234,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4095,14 +4254,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -4121,9 +4280,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -4139,14 +4298,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -4165,9 +4324,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -4207,9 +4366,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -4226,6 +4385,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4234,9 +4403,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -4303,9 +4472,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -4318,9 +4487,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -4331,11 +4500,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4344,9 +4514,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4354,31 +4524,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -4406,14 +4576,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -4471,7 +4641,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4482,7 +4652,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4733,13 +4903,19 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.50.0" @@ -4752,9 +4928,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -4799,28 +4975,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4840,7 +5016,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] @@ -4880,5 +5056,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] diff --git a/lightningstor/Cargo.lock b/lightningstor/Cargo.lock index 2ff8344..49c3f26 100644 --- a/lightningstor/Cargo.lock +++ b/lightningstor/Cargo.lock @@ -2,13 +2,48 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -51,9 +86,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -66,15 +101,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -101,9 +136,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -116,6 +151,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -172,9 +219,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -182,9 +229,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -220,7 +267,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -253,6 +300,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -261,9 +314,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -276,9 +338,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -288,15 +350,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -356,9 +418,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -369,10 +431,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -380,9 +452,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -392,9 +464,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -404,24 +476,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -503,9 +575,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -522,9 +604,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -618,9 +700,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -691,9 +773,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -706,9 +788,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -716,15 +798,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -744,15 +826,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -761,21 +843,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -785,7 +867,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -801,9 +882,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -826,6 +907,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -834,9 +925,9 @@ checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -844,7 +935,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1033,7 +1124,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1051,14 +1142,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1067,7 +1157,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1077,7 +1167,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64", "iam-audit", @@ -1087,6 +1179,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1172,6 +1265,7 @@ dependencies = [ "iam-client", "iam-types", "serde_json", + "tokio", "tonic", "tracing", ] @@ -1207,9 +1301,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1343,14 +1437,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1362,9 +1465,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1402,9 +1505,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1418,9 +1521,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1449,9 +1552,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -1461,13 +1564,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1566,6 +1670,7 @@ dependencies = [ "hmac", "http", "http-body-util", + "iam-api", "iam-service-auth", "lightningstor-api", "lightningstor-distributed", @@ -1588,7 +1693,7 @@ dependencies = [ "toml", "tonic", "tonic-health", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tracing", "tracing-subscriber", @@ -1628,9 +1733,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1695,9 +1800,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -1720,7 +1825,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -1823,9 +1928,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1834,10 +1939,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking" @@ -1893,6 +2004,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1916,23 +2038,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1941,9 +2063,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1958,10 +2080,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -1999,9 +2139,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2160,7 +2300,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2169,9 +2309,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2197,16 +2337,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2264,7 +2404,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2282,7 +2422,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2300,16 +2440,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2327,9 +2467,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2339,9 +2479,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2350,15 +2490,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2382,14 +2522,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2400,7 +2540,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2414,11 +2554,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2427,9 +2567,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -2443,9 +2583,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2464,9 +2604,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2474,9 +2614,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -2492,15 +2632,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2513,11 +2653,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -2526,9 +2666,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2625,10 +2765,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2658,9 +2799,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2683,12 +2824,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2730,7 +2871,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -2793,7 +2934,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -2877,9 +3018,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2908,9 +3049,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -3011,9 +3152,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3026,9 +3167,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3036,16 +3177,16 @@ dependencies = [ "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3064,9 +3205,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3075,9 +3216,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3113,7 +3254,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -3209,9 +3350,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3225,18 +3366,18 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -3256,9 +3397,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3279,9 +3420,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3300,9 +3441,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3336,9 +3477,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3355,6 +3496,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3363,9 +3514,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3432,9 +3583,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -3447,9 +3598,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3460,11 +3611,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3473,9 +3625,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3483,9 +3635,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3496,18 +3648,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3529,14 +3681,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3856,18 +4008,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -3900,18 +4052,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/lightningstor/crates/lightningstor-distributed/src/backends/erasure_coded.rs b/lightningstor/crates/lightningstor-distributed/src/backends/erasure_coded.rs index 29e720a..b7f6921 100644 --- a/lightningstor/crates/lightningstor-distributed/src/backends/erasure_coded.rs +++ b/lightningstor/crates/lightningstor-distributed/src/backends/erasure_coded.rs @@ -10,6 +10,8 @@ use crate::node::{NodeClientTrait, NodeRegistry}; use crate::placement::{ConsistentHashSelector, NodeSelector}; use async_trait::async_trait; use bytes::Bytes; +use futures::future::BoxFuture; +use futures::stream::{FuturesUnordered, StreamExt}; use lightningstor_storage::{StorageBackend, StorageError, StorageResult}; use lightningstor_types::ObjectId; use serde::{Deserialize, Serialize}; @@ -336,7 +338,7 @@ impl ErasureCodedBackend { .map_err(|e| StorageError::Backend(e.to_string()))?; // Try to read all shards in parallel - let mut shard_futures = Vec::with_capacity(self.total_shards()); + let mut shard_futures = FuturesUnordered::new(); for shard_idx in 0..self.total_shards() { let is_parity = shard_idx >= self.data_shards; let chunk_id = ChunkId::new(object_id, chunk_index, shard_idx, is_parity); @@ -345,35 +347,73 @@ impl ErasureCodedBackend { let chunk_key = chunk_id.to_key(); shard_futures.push(async move { - // Try to read from the preferred node first - if let Ok(node) = node_selector.select_for_read(&nodes, &chunk_key).await { - if let Ok(data) = node - .get_chunk(&chunk_key, shard_idx as u32, is_parity) - .await + let preferred_id = node_selector + .select_for_read(&nodes, &chunk_key) + .await + .ok() + .map(|node| node.node_id().to_string()); + let mut readers: FuturesUnordered>> = + FuturesUnordered::new(); + + if let Some(preferred_id) = preferred_id.as_ref() { + if let Some(preferred) = nodes + .iter() + .find(|node| node.node_id() == preferred_id.as_str()) + .cloned() { - return Some(data); + let key = chunk_key.clone(); + readers.push(Box::pin(async move { + preferred + .get_chunk(&key, shard_idx as u32, is_parity) + .await + .ok() + })); } } - // Try other nodes if preferred fails for node in &nodes { - if let Ok(data) = node - .get_chunk(&chunk_key, shard_idx as u32, is_parity) - .await + if preferred_id + .as_ref() + .is_some_and(|preferred| preferred == node.node_id()) { - return Some(data); + continue; + } + let node = node.clone(); + let key = chunk_key.clone(); + readers.push(Box::pin(async move { + node.get_chunk(&key, shard_idx as u32, is_parity).await.ok() + })); + } + + while let Some(result) = readers.next().await { + if let Some(data) = result { + return (shard_idx, Some(data)); } } - None + (shard_idx, None) }); } - let shard_results: Vec> = futures::future::join_all(shard_futures).await; + let mut shard_results = vec![None; self.total_shards()]; + let mut available_count = 0usize; + + while let Some((shard_idx, shard)) = shard_futures.next().await { + if shard.is_some() { + available_count += 1; + } + shard_results[shard_idx] = shard; + + if available_count >= self.data_shards { + break; + } + + if available_count + shard_futures.len() < self.data_shards { + break; + } + } // Count available shards - let available_count = shard_results.iter().filter(|s| s.is_some()).count(); - debug!( object_id = %object_id, chunk_index, @@ -419,9 +459,9 @@ impl StorageBackend for ErasureCodedBackend { debug!(object_id = %object_id, size = original_size, "Putting object with erasure coding"); // Split data into chunks - let chunks = self.chunk_manager.split(&data); + let chunk_size = self.chunk_manager.effective_chunk_size(data.len()); + let chunks = self.chunk_manager.split_with_chunk_size(&data, chunk_size); let chunk_count = chunks.len(); - let chunk_size = self.chunk_manager.chunk_size(); // Write each chunk for (chunk_idx, chunk_data) in chunks.into_iter().enumerate() { @@ -591,24 +631,78 @@ impl StorageBackend for ErasureCodedBackend { .map_err(|e| StorageError::Backend(e.to_string()))?; // Try to read shards - let mut shard_futures = Vec::with_capacity(self.total_shards()); + let mut shard_futures = FuturesUnordered::new(); for shard_idx in 0..self.total_shards() { let is_parity = shard_idx >= self.data_shards; let key = format!("{}_{}_{}", part_key, shard_idx, if is_parity { "p" } else { "d" }); let nodes = nodes.clone(); + let node_selector = self.node_selector.clone(); shard_futures.push(async move { - for node in &nodes { - if let Ok(data) = node.get_chunk(&key, shard_idx as u32, is_parity).await { - return Some(data); + let preferred_id = node_selector + .select_for_read(&nodes, &key) + .await + .ok() + .map(|node| node.node_id().to_string()); + let mut readers: FuturesUnordered>> = + FuturesUnordered::new(); + + if let Some(preferred_id) = preferred_id.as_ref() { + if let Some(preferred) = nodes + .iter() + .find(|node| node.node_id() == preferred_id.as_str()) + .cloned() + { + let key = key.clone(); + readers.push(Box::pin(async move { + preferred + .get_chunk(&key, shard_idx as u32, is_parity) + .await + .ok() + })); } } - None + + for node in &nodes { + if preferred_id + .as_ref() + .is_some_and(|preferred| preferred == node.node_id()) + { + continue; + } + let node = node.clone(); + let key = key.clone(); + readers.push(Box::pin(async move { + node.get_chunk(&key, shard_idx as u32, is_parity).await.ok() + })); + } + + while let Some(result) = readers.next().await { + if let Some(data) = result { + return (shard_idx, Some(data)); + } + } + (shard_idx, None) }); } - let shard_results: Vec> = futures::future::join_all(shard_futures).await; - let available = shard_results.iter().filter(|s| s.is_some()).count(); + let mut shard_results = vec![None; self.total_shards()]; + let mut available = 0usize; + + while let Some((shard_idx, shard)) = shard_futures.next().await { + if shard.is_some() { + available += 1; + } + shard_results[shard_idx] = shard; + + if available >= self.data_shards { + break; + } + + if available + shard_futures.len() < self.data_shards { + break; + } + } if available < self.data_shards { return Err(StorageError::Backend(format!( @@ -674,7 +768,135 @@ impl StorageBackend for ErasureCodedBackend { mod tests { use super::*; use crate::config::{ChunkConfig, RedundancyMode}; - use crate::node::MockNodeRegistry; + use crate::node::{MockNodeClient, MockNodeRegistry, NodeError, NodeResult}; + use async_trait::async_trait; + use dashmap::DashMap; + use std::time::{Duration, Instant}; + use tokio::time::sleep; + + struct SlowReadNodeClient { + node_id: String, + endpoint: String, + delay: Duration, + chunks: DashMap>, + } + + impl SlowReadNodeClient { + fn new(node_id: impl Into, endpoint: impl Into, delay: Duration) -> Self { + Self { + node_id: node_id.into(), + endpoint: endpoint.into(), + delay, + chunks: DashMap::new(), + } + } + + fn insert_chunk(&self, chunk_id: impl Into, data: Vec) { + self.chunks.insert(chunk_id.into(), data); + } + } + + #[async_trait] + impl NodeClientTrait for SlowReadNodeClient { + fn node_id(&self) -> &str { + &self.node_id + } + + fn endpoint(&self) -> &str { + &self.endpoint + } + + async fn is_healthy(&self) -> bool { + true + } + + async fn put_chunk( + &self, + chunk_id: &str, + _shard_index: u32, + _is_parity: bool, + data: Bytes, + ) -> NodeResult<()> { + self.chunks.insert(chunk_id.to_string(), data.to_vec()); + Ok(()) + } + + async fn get_chunk( + &self, + chunk_id: &str, + _shard_index: u32, + _is_parity: bool, + ) -> NodeResult { + sleep(self.delay).await; + self.chunks + .get(chunk_id) + .map(|value| Bytes::from(value.value().clone())) + .ok_or_else(|| NodeError::NotFound(chunk_id.to_string())) + } + + async fn delete_chunk(&self, chunk_id: &str) -> NodeResult<()> { + self.chunks.remove(chunk_id); + Ok(()) + } + + async fn chunk_exists(&self, chunk_id: &str) -> NodeResult { + Ok(self.chunks.contains_key(chunk_id)) + } + + async fn chunk_size(&self, chunk_id: &str) -> NodeResult> { + Ok(self + .chunks + .get(chunk_id) + .map(|value| value.value().len() as u64)) + } + + async fn ping(&self) -> NodeResult { + Ok(Duration::from_millis(1)) + } + } + + struct FixedNodeRegistry { + nodes: Vec>, + } + + #[async_trait] + impl NodeRegistry for FixedNodeRegistry { + async fn get_all_nodes(&self) -> NodeResult>> { + Ok(self.nodes.clone()) + } + + async fn get_healthy_nodes(&self) -> NodeResult>> { + Ok(self.nodes.clone()) + } + + async fn register_node(&self, _info: crate::node::NodeInfo) -> NodeResult<()> { + Ok(()) + } + + async fn deregister_node(&self, _node_id: &str) -> NodeResult<()> { + Ok(()) + } + + async fn update_health(&self, _node_id: &str, _healthy: bool) -> NodeResult<()> { + Ok(()) + } + + async fn get_node(&self, node_id: &str) -> NodeResult>> { + Ok(self + .nodes + .iter() + .find(|node| node.node_id() == node_id) + .cloned()) + } + + async fn node_count(&self) -> usize { + self.nodes.len() + } + + async fn healthy_node_count(&self) -> usize { + self.nodes.len() + } + } fn create_ec_config(data_shards: usize, parity_shards: usize) -> DistributedConfig { DistributedConfig { @@ -858,4 +1080,162 @@ mod tests { assert_eq!(retrieved.len(), data.len()); assert_eq!(retrieved, data); } + + #[tokio::test] + async fn test_ec_backend_read_returns_after_minimum_shards() { + let config = create_ec_config(4, 2); + let mut fast_nodes = Vec::new(); + for index in 0..4 { + fast_nodes.push(Arc::new(MockNodeClient::new( + format!("fast-{index}"), + format!("http://fast-{index}:9002"), + ))); + } + let slow_a = Arc::new(SlowReadNodeClient::new( + "slow-a", + "http://slow-a:9002", + Duration::from_millis(250), + )); + let slow_b = Arc::new(SlowReadNodeClient::new( + "slow-b", + "http://slow-b:9002", + Duration::from_millis(250), + )); + + let backend = ErasureCodedBackend::new( + config, + Arc::new(FixedNodeRegistry { + nodes: vec![ + fast_nodes[0].clone() as Arc, + fast_nodes[1].clone() as Arc, + fast_nodes[2].clone() as Arc, + fast_nodes[3].clone() as Arc, + slow_a.clone() as Arc, + slow_b.clone() as Arc, + ], + }), + ) + .await + .unwrap(); + + let object_id = ObjectId::new(); + let data = Bytes::from(vec![5u8; 512]); + let metadata = ObjectMetadata::new(data.len() as u64, 1, data.len()); + let meta_key = ObjectMetadata::metadata_key(&object_id); + let shards = backend.codec.encode(&data).unwrap(); + + for fast_node in &fast_nodes { + fast_node + .put_chunk(&meta_key, 0, false, Bytes::from(metadata.to_bytes())) + .await + .unwrap(); + } + for slow_node in [&slow_a, &slow_b] { + slow_node.insert_chunk(meta_key.clone(), metadata.to_bytes()); + } + + for (shard_idx, shard_data) in shards.into_iter().enumerate() { + let is_parity = shard_idx >= backend.data_shards; + let key = ChunkId::new(&object_id, 0, shard_idx, is_parity).to_key(); + if shard_idx < 4 { + fast_nodes[shard_idx] + .put_chunk( + &key, + shard_idx as u32, + is_parity, + Bytes::from(shard_data), + ) + .await + .unwrap(); + } else if shard_idx == 4 { + slow_a.insert_chunk(key, shard_data); + } else { + slow_b.insert_chunk(key, shard_data); + } + } + + let started = Instant::now(); + let retrieved = backend.get_object(&object_id).await.unwrap(); + let elapsed = started.elapsed(); + + assert!(elapsed < Duration::from_millis(200), "elapsed={elapsed:?}"); + assert_eq!(retrieved, data); + } + + #[tokio::test] + async fn test_ec_backend_get_part_returns_after_minimum_shards() { + let config = create_ec_config(4, 2); + let mut fast_nodes = Vec::new(); + for index in 0..4 { + fast_nodes.push(Arc::new(MockNodeClient::new( + format!("fast-{index}"), + format!("http://fast-{index}:9002"), + ))); + } + let slow_a = Arc::new(SlowReadNodeClient::new( + "slow-a", + "http://slow-a:9002", + Duration::from_millis(250), + )); + let slow_b = Arc::new(SlowReadNodeClient::new( + "slow-b", + "http://slow-b:9002", + Duration::from_millis(250), + )); + + let backend = ErasureCodedBackend::new( + config, + Arc::new(FixedNodeRegistry { + nodes: vec![ + fast_nodes[0].clone() as Arc, + fast_nodes[1].clone() as Arc, + fast_nodes[2].clone() as Arc, + fast_nodes[3].clone() as Arc, + slow_a.clone() as Arc, + slow_b.clone() as Arc, + ], + }), + ) + .await + .unwrap(); + + let upload_id = "upload-latency"; + let part_number = 7; + let data = Bytes::from(vec![9u8; 512]); + let shards = backend.codec.encode(&data).unwrap(); + + for (shard_idx, shard_data) in shards.into_iter().enumerate() { + let is_parity = shard_idx >= backend.data_shards; + let key = format!( + "part_{}_{}_{}_{}", + upload_id, + part_number, + shard_idx, + if is_parity { "p" } else { "d" } + ); + + if shard_idx < 4 { + fast_nodes[shard_idx] + .put_chunk( + &key, + shard_idx as u32, + is_parity, + Bytes::from(shard_data), + ) + .await + .unwrap(); + } else if shard_idx == 4 { + slow_a.insert_chunk(key, shard_data); + } else { + slow_b.insert_chunk(key, shard_data); + } + } + + let started = Instant::now(); + let retrieved = backend.get_part(upload_id, part_number).await.unwrap(); + let elapsed = started.elapsed(); + + assert!(elapsed < Duration::from_millis(200), "elapsed={elapsed:?}"); + assert_eq!(retrieved, data); + } } diff --git a/lightningstor/crates/lightningstor-distributed/src/backends/replicated.rs b/lightningstor/crates/lightningstor-distributed/src/backends/replicated.rs index 46b2030..eb661c3 100644 --- a/lightningstor/crates/lightningstor-distributed/src/backends/replicated.rs +++ b/lightningstor/crates/lightningstor-distributed/src/backends/replicated.rs @@ -5,13 +5,15 @@ use crate::chunk::ChunkManager; use crate::config::DistributedConfig; -use crate::node::{NodeClientTrait, NodeError, NodeRegistry}; +use crate::node::{NodeClientTrait, NodeError, NodeRegistry, NodeResult}; use crate::placement::{ConsistentHashSelector, NodeSelector}; +use crate::repair::{RepairQueue, ReplicatedRepairTask}; use async_trait::async_trait; use bytes::{Bytes, BytesMut}; use futures::stream::{FuturesUnordered, StreamExt}; use lightningstor_storage::{StorageBackend, StorageError, StorageResult}; use lightningstor_types::ObjectId; +use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use tracing::{debug, error, warn}; @@ -81,6 +83,8 @@ pub struct ReplicatedBackend { read_quorum: usize, /// Write quorum (minimum replicas for successful write) write_quorum: usize, + /// Durable queue for repairing under-replicated chunks. + repair_queue: Option>, } impl ReplicatedBackend { @@ -92,6 +96,15 @@ impl ReplicatedBackend { pub async fn new( config: DistributedConfig, node_registry: Arc, + ) -> StorageResult { + Self::new_with_repair_queue(config, node_registry, None).await + } + + /// Create a replicated backend with an optional durable repair queue. + pub async fn new_with_repair_queue( + config: DistributedConfig, + node_registry: Arc, + repair_queue: Option>, ) -> StorageResult { let (replica_count, read_quorum, write_quorum) = match &config.redundancy { crate::config::RedundancyMode::Replicated { @@ -116,6 +129,7 @@ impl ReplicatedBackend { replica_count, read_quorum, write_quorum, + repair_queue, }) } @@ -134,6 +148,89 @@ impl ReplicatedBackend { self.write_quorum } + async fn finalize_pending_replica_writes( + repair_queue: Option>, + mut pending_writes: FuturesUnordered)>>, + key: String, + shard_index: u32, + mut success_count: usize, + total_replicas: usize, + reason: String, + ) { + let mut errors = Vec::new(); + + while let Some(result) = pending_writes.next().await { + match result { + Ok((_, Ok(()))) => success_count += 1, + Ok((node_id, Err(err))) => errors.push(format!("{node_id}: {err}")), + Err(join_err) => errors.push(format!("join error: {join_err}")), + } + } + + if success_count >= total_replicas { + return; + } + + if let Some(queue) = repair_queue { + queue + .enqueue_repair(ReplicatedRepairTask::new(key.clone(), shard_index, reason)) + .await; + } + + warn!( + chunk_key = %key, + shard_index, + success_count, + total_replicas, + errors = ?errors, + "Replica write completed below desired replication; repair task queued" + ); + } + + async fn finalize_pending_chunked_write_repairs( + repair_queue: Option>, + mut pending_writes: FuturesUnordered)>>, + repair_targets: Vec<(String, u32)>, + object_id: String, + mut success_count: usize, + total_replicas: usize, + reason: String, + ) { + let mut errors = Vec::new(); + + while let Some(result) = pending_writes.next().await { + match result { + Ok((_, Ok(()))) => success_count += 1, + Ok((node_id, Err(err))) => errors.push(format!("{node_id}: {err}")), + Err(join_err) => errors.push(format!("join error: {join_err}")), + } + } + + if success_count >= total_replicas { + return; + } + + if let Some(queue) = repair_queue { + for (chunk_key, shard_index) in repair_targets { + queue + .enqueue_repair(ReplicatedRepairTask::new( + chunk_key, + shard_index, + reason.clone(), + )) + .await; + } + } + + warn!( + object_id = %object_id, + success_count, + total_replicas, + errors = ?errors, + "Chunked replica write completed below desired replication; repair tasks queued" + ); + } + fn chunk_write_parallelism(&self, chunk_count: usize) -> usize { chunk_count .min( @@ -220,7 +317,13 @@ impl ReplicatedBackend { )); } - if let Ok(preferred) = self.node_selector.select_for_read(nodes, key).await { + let mut ordered_nodes = Self::ordered_read_nodes(nodes, self + .node_selector + .select_for_read(nodes, key) + .await + .ok()); + + if let Some(preferred) = ordered_nodes.first() { match preferred.get_chunk(key, shard_index, false).await { Ok(data) => return Ok(Some(data)), Err(NodeError::NotFound(_)) => {} @@ -235,7 +338,7 @@ impl ReplicatedBackend { } } - for node in nodes { + for node in ordered_nodes.drain(1..) { match node.get_chunk(key, shard_index, false).await { Ok(data) => return Ok(Some(data)), Err(NodeError::NotFound(_)) => continue, @@ -383,6 +486,21 @@ impl ReplicatedBackend { Ok((_, Ok(()))) => { success_count += 1; if success_count >= self.write_quorum { + if success_count < total_replicas { + let pending_writes = + std::mem::replace(&mut write_futures, FuturesUnordered::new()); + tokio::spawn(Self::finalize_pending_replica_writes( + self.repair_queue.clone(), + pending_writes, + key.clone(), + shard_index, + success_count, + total_replicas, + format!( + "replica write completed below desired replication after quorum ({success_count}/{total_replicas})" + ), + )); + } debug!( chunk_key = %key, success_count, @@ -427,13 +545,13 @@ impl ReplicatedBackend { } async fn write_chunked_object(&self, object_id: &ObjectId, data: Bytes) -> StorageResult<()> { - let chunk_size = self.chunk_manager.chunk_size(); - let chunk_count = self.chunk_manager.chunk_count(data.len()); + let chunk_size = self.chunk_manager.effective_chunk_size(data.len()); + let chunk_count = ChunkManager::chunk_count_for_size(data.len(), chunk_size); let metadata = ReplicatedObjectMetadata::new(data.len(), chunk_count, chunk_size); let mut requests = Vec::with_capacity(chunk_count + 1); for chunk_index in 0..chunk_count { let chunk_key = Self::object_chunk_key(object_id, chunk_index); - let (start, len) = self.chunk_manager.chunk_range(data.len(), chunk_index); + let (start, len) = ChunkManager::chunk_range_for_size(data.len(), chunk_index, chunk_size); let chunk_bytes = data.slice(start..start + len); requests.push((chunk_key, chunk_index as u32, false, chunk_bytes)); } @@ -464,6 +582,27 @@ impl ReplicatedBackend { Ok((_, Ok(()))) => { success_count += 1; if success_count >= self.write_quorum { + if success_count < total_replicas { + let repair_targets = requests + .iter() + .map(|(chunk_key, shard_index, _, _)| { + (chunk_key.clone(), *shard_index) + }) + .collect::>(); + let pending_writes = + std::mem::replace(&mut write_futures, FuturesUnordered::new()); + tokio::spawn(Self::finalize_pending_chunked_write_repairs( + self.repair_queue.clone(), + pending_writes, + repair_targets, + object_id.to_string(), + success_count, + total_replicas, + format!( + "chunked object write completed below desired replication after quorum ({success_count}/{total_replicas})" + ), + )); + } debug!( object_id = %object_id, chunk_count, @@ -509,6 +648,150 @@ impl ReplicatedBackend { ))) } + pub async fn repair_chunk(&self, task: &ReplicatedRepairTask) -> StorageResult<()> { + let healthy_nodes = self + .node_registry + .get_healthy_nodes() + .await + .map_err(|e| StorageError::Backend(e.to_string()))?; + if healthy_nodes.is_empty() { + return Err(StorageError::Backend( + "No healthy storage nodes available for repair".to_string(), + )); + } + let desired_nodes = self + .node_selector + .select_nodes_for_key(&healthy_nodes, self.replica_count, &task.key) + .await + .map_err(|e| StorageError::Backend(e.to_string()))?; + + let mut present_nodes = Vec::new(); + let mut missing_nodes = Vec::new(); + for node in desired_nodes { + match node.chunk_exists(&task.key).await { + Ok(true) => present_nodes.push(node), + Ok(false) => missing_nodes.push(node), + Err(err) => { + warn!( + chunk_key = task.key, + node_id = node.node_id(), + error = ?err, + "Failed to inspect chunk during repair; treating replica as missing" + ); + missing_nodes.push(node); + } + } + } + + if missing_nodes.is_empty() { + return Ok(()); + } + + if present_nodes.is_empty() { + let desired_node_ids = missing_nodes + .iter() + .map(|node| node.node_id().to_string()) + .collect::>(); + for node in healthy_nodes { + if desired_node_ids.contains(node.node_id()) { + continue; + } + match node.chunk_exists(&task.key).await { + Ok(true) => { + present_nodes.push(node); + break; + } + Ok(false) => {} + Err(err) => { + warn!( + chunk_key = task.key, + node_id = node.node_id(), + error = ?err, + "Failed to inspect off-placement chunk during repair" + ); + } + } + } + } + + let source = present_nodes.first().ok_or_else(|| { + StorageError::Backend(format!( + "Cannot repair {} because no healthy source replica is available", + task.key + )) + })?; + + let data = source + .get_chunk(&task.key, task.shard_index, false) + .await + .map_err(|err| { + StorageError::Backend(format!( + "Failed to load repair source for {} from {}: {}", + task.key, + source.node_id(), + err + )) + })?; + + let mut repair_futures = FuturesUnordered::new(); + for node in missing_nodes { + let node_id = node.node_id().to_string(); + let key = task.key.clone(); + let chunk = data.clone(); + let shard_index = task.shard_index; + repair_futures.push(tokio::spawn(async move { + let result = node.put_chunk(&key, shard_index, false, chunk).await; + (node_id, result) + })); + } + + let mut repaired = 0usize; + let mut errors = Vec::new(); + while let Some(result) = repair_futures.next().await { + match result { + Ok((_, Ok(()))) => repaired += 1, + Ok((node_id, Err(err))) => errors.push(format!("{node_id}: {err}")), + Err(join_err) => errors.push(format!("join error: {join_err}")), + } + } + + if errors.is_empty() { + return Ok(()); + } + + Err(StorageError::Backend(format!( + "Repair for {} only restored {} replicas: {}", + task.key, + repaired, + errors.join(", ") + ))) + } + + pub async fn chunk_exists_anywhere(&self, key: &str) -> StorageResult { + let nodes = self + .node_registry + .get_all_nodes() + .await + .map_err(|e| StorageError::Backend(e.to_string()))?; + + for node in nodes { + match node.chunk_exists(key).await { + Ok(true) => return Ok(true), + Ok(false) => {} + Err(err) => { + warn!( + chunk_key = key, + node_id = node.node_id(), + error = ?err, + "Failed to inspect chunk while probing global existence" + ); + } + } + } + + Ok(false) + } + async fn read_chunked_object( &self, object_id: &ObjectId, @@ -521,24 +804,47 @@ impl ReplicatedBackend { .map_err(|e| StorageError::Backend(e.to_string()))?; if !nodes.is_empty() { - let mut ordered_nodes = Vec::with_capacity(nodes.len()); - if let Ok(preferred) = self + let preferred = self .node_selector .select_for_read(&nodes, &Self::object_chunk_key(object_id, 0)) .await - { - ordered_nodes.push(preferred.clone()); - } - for node in nodes { - if ordered_nodes - .iter() - .all(|existing| existing.node_id() != node.node_id()) + .ok(); + let ordered_nodes = Self::ordered_read_nodes(&nodes, preferred); + + if metadata.chunk_count > 1 { + if let Some(local_node) = ordered_nodes.iter().find(|node| Self::is_local_node(node)) { - ordered_nodes.push(node); + let batch_requests: Vec<(String, u32, bool)> = (0..metadata.chunk_count) + .map(|chunk_index| { + ( + Self::object_chunk_key(object_id, chunk_index), + chunk_index as u32, + false, + ) + }) + .collect(); + match local_node.batch_get_chunks(batch_requests).await { + Ok(chunks) => { + return Self::assemble_chunked_bytes( + object_id, + metadata.original_size, + chunks, + ); + } + Err(err) => { + warn!( + object_id = %object_id, + node_id = local_node.node_id(), + error = ?err, + "Local replica batch read failed, falling back to distributed reads" + ); + } + } } } - if ordered_nodes.len() > 1 && metadata.chunk_count > 1 { + if ordered_nodes.len() > 1 && metadata.chunk_count > 1 && !Self::has_local_node(&ordered_nodes) + { match self .read_chunked_object_from_distributed_batches( object_id, @@ -783,6 +1089,74 @@ impl ReplicatedBackend { combined.truncate(original_size); Ok(combined.freeze()) } + + fn ordered_read_nodes( + nodes: &[Arc], + preferred: Option>, + ) -> Vec> { + let mut ordered = Vec::with_capacity(nodes.len()); + + if let Some(local) = nodes.iter().find(|node| Self::is_local_node(node)) { + ordered.push(local.clone()); + } + + if let Some(preferred) = preferred { + if ordered + .iter() + .all(|existing| existing.node_id() != preferred.node_id()) + { + ordered.push(preferred); + } + } + + for node in nodes { + if ordered + .iter() + .all(|existing| existing.node_id() != node.node_id()) + { + ordered.push(node.clone()); + } + } + + ordered + } + + fn has_local_node(nodes: &[Arc]) -> bool { + nodes.iter().any(Self::is_local_node) + } + + fn is_local_node(node: &Arc) -> bool { + Self::endpoint_is_local(node.endpoint()) + } + + fn endpoint_is_local(endpoint: &str) -> bool { + let authority = endpoint + .split_once("://") + .map(|(_, rest)| rest) + .unwrap_or(endpoint) + .split('/') + .next() + .unwrap_or(endpoint); + let host = if authority.starts_with('[') { + authority + .split_once(']') + .map(|(host, _)| host.trim_start_matches('[')) + .unwrap_or(authority.trim_matches(['[', ']'])) + } else { + authority + .rsplit_once(':') + .map(|(host, _)| host) + .unwrap_or(authority) + }; + + if host.eq_ignore_ascii_case("localhost") { + return true; + } + + host.parse::() + .map(|ip| ip.is_loopback()) + .unwrap_or(false) + } } #[async_trait] @@ -908,12 +1282,25 @@ mod tests { use super::*; use crate::config::RedundancyMode; use crate::node::{MockNodeRegistry, NodeError, NodeResult}; + use crate::repair::RepairQueue; use async_trait::async_trait; use dashmap::DashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::time::sleep; + #[derive(Default)] + struct CapturingRepairQueue { + tasks: DashMap, + } + + #[async_trait] + impl RepairQueue for CapturingRepairQueue { + async fn enqueue_repair(&self, task: ReplicatedRepairTask) { + self.tasks.insert(task.id.clone(), task); + } + } + struct SlowNodeClient { node_id: String, endpoint: String, @@ -1196,6 +1583,115 @@ mod tests { assert!(result.is_err()); } + #[tokio::test] + async fn test_under_replicated_write_enqueues_repair_task() { + let config = create_replicated_config(3); + let registry = Arc::new(MockNodeRegistry::with_nodes(3)); + let nodes = registry.all_mock_nodes(); + nodes[2].set_fail_puts(true); + + let repair_queue = Arc::new(CapturingRepairQueue::default()); + let backend = ReplicatedBackend::new_with_repair_queue( + config, + registry, + Some(repair_queue.clone()), + ) + .await + .unwrap(); + + let object_id = ObjectId::new(); + backend + .put_object(&object_id, Bytes::from_static(b"repair-me")) + .await + .unwrap(); + + let mut task = None; + for _ in 0..20 { + task = repair_queue + .tasks + .iter() + .next() + .map(|entry| entry.value().clone()); + if task.is_some() { + break; + } + sleep(Duration::from_millis(10)).await; + } + let task = task.expect("repair task should be queued"); + assert_eq!(task.key, ReplicatedBackend::object_key(&object_id)); + assert_eq!(task.shard_index, 0); + } + + #[tokio::test] + async fn test_repair_chunk_restores_missing_replica() { + let config = create_replicated_config(3); + let registry = Arc::new(MockNodeRegistry::with_nodes(3)); + let nodes = registry.all_mock_nodes(); + let backend = ReplicatedBackend::new(config, registry.clone()) + .await + .unwrap(); + + let object_id = ObjectId::new(); + let data = Bytes::from(vec![11u8; 128]); + backend.put_object(&object_id, data.clone()).await.unwrap(); + + let key = ReplicatedBackend::object_key(&object_id); + let mut missing = None; + for node in &nodes { + if node.chunk_exists(&key).await.unwrap() { + missing = Some(node.clone()); + break; + } + } + let missing = missing.expect("at least one replica should exist"); + missing.delete_chunk(&key).await.unwrap(); + assert!(!missing.chunk_exists(&key).await.unwrap()); + + let task = ReplicatedRepairTask::new(key.clone(), 0, "test"); + backend.repair_chunk(&task).await.unwrap(); + assert!(missing.chunk_exists(&key).await.unwrap()); + } + + #[tokio::test] + async fn test_repair_chunk_can_source_from_off_placement_replica() { + let config = create_replicated_config(2); + let registry = Arc::new(MockNodeRegistry::with_nodes(3)); + let nodes = registry.all_mock_nodes(); + let backend = ReplicatedBackend::new(config, registry.clone()) + .await + .unwrap(); + + let object_id = ObjectId::new(); + let data = Bytes::from(vec![23u8; 128]); + backend.put_object(&object_id, data.clone()).await.unwrap(); + + let key = ReplicatedBackend::object_key(&object_id); + let desired_nodes = backend.select_replica_nodes_for_key(&key).await.unwrap(); + assert_eq!(desired_nodes.len(), 2); + let off_placement = nodes + .iter() + .find(|node| { + desired_nodes + .iter() + .all(|desired| desired.node_id() != node.node_id()) + }) + .cloned() + .expect("off-placement node should exist"); + + let source_bytes = desired_nodes[0].get_chunk(&key, 0, false).await.unwrap(); + off_placement.put_chunk(&key, 0, false, source_bytes).await.unwrap(); + for node in &desired_nodes { + node.delete_chunk(&key).await.unwrap(); + assert!(!node.chunk_exists(&key).await.unwrap()); + } + + let task = ReplicatedRepairTask::new(key.clone(), 0, "off-placement-source"); + backend.repair_chunk(&task).await.unwrap(); + for node in &desired_nodes { + assert!(node.chunk_exists(&key).await.unwrap()); + } + } + #[tokio::test] async fn test_replicated_backend_returns_after_quorum_without_waiting_for_slow_replica() { let config = create_replicated_config(3); @@ -1333,6 +1829,43 @@ mod tests { .is_none()); } + #[tokio::test] + async fn test_replicated_backend_prefers_local_replica_for_chunked_reads() { + let mut config = create_replicated_config(3); + config.chunk.chunk_size = 64; + let local = Arc::new(crate::node::MockNodeClient::new( + "local", + "http://127.0.0.1:9002", + )); + let slow_a = Arc::new(SlowNodeClient::new( + "slow-a", + "http://slow-a:9002", + Duration::from_millis(250), + )); + let slow_b = Arc::new(SlowNodeClient::new( + "slow-b", + "http://slow-b:9002", + Duration::from_millis(250), + )); + let registry = Arc::new(FixedNodeRegistry { + nodes: vec![slow_a.clone(), slow_b.clone(), local.clone()], + }); + + let backend = ReplicatedBackend::new(config, registry).await.unwrap(); + let object_id = ObjectId::new(); + let data = Bytes::from(vec![5u8; 256]); + + backend.put_object(&object_id, data.clone()).await.unwrap(); + + let started = Instant::now(); + let retrieved = backend.get_object(&object_id).await.unwrap(); + let elapsed = started.elapsed(); + + assert_eq!(retrieved, data); + assert!(elapsed < Duration::from_millis(150), "elapsed={elapsed:?}"); + assert!(local.get_count() >= 4); + } + #[tokio::test] async fn test_replicated_backend_object_size() { let config = create_replicated_config(3); diff --git a/lightningstor/crates/lightningstor-distributed/src/chunk/mod.rs b/lightningstor/crates/lightningstor-distributed/src/chunk/mod.rs index 7e328fe..9dbd322 100644 --- a/lightningstor/crates/lightningstor-distributed/src/chunk/mod.rs +++ b/lightningstor/crates/lightningstor-distributed/src/chunk/mod.rs @@ -5,6 +5,8 @@ use crate::config::ChunkConfig; +const TARGET_CHUNK_COUNT_PER_OBJECT: usize = 8; + /// Manages chunk operations for large objects #[derive(Debug, Clone)] pub struct ChunkManager { @@ -27,18 +29,42 @@ impl ChunkManager { self.config.chunk_size } + /// Choose the effective chunk size for an object of the given size. + /// + /// Small objects keep the configured default chunk size. Larger objects + /// scale up to keep per-object chunk counts bounded without exceeding the + /// configured maximum. + pub fn effective_chunk_size(&self, total_size: usize) -> usize { + if total_size == 0 { + return self.config.chunk_size; + } + + let min_chunk_size = self.config.min_chunk_size.min(self.config.chunk_size).max(1); + let max_chunk_size = self.config.max_chunk_size.max(self.config.chunk_size); + let required = total_size.div_ceil(TARGET_CHUNK_COUNT_PER_OBJECT); + let alignment = min_chunk_size; + let aligned_required = required.div_ceil(alignment) * alignment; + + aligned_required + .max(self.config.chunk_size) + .clamp(min_chunk_size, max_chunk_size) + } + /// Split data into chunks /// /// Returns a vector of chunks. Each chunk is at most `chunk_size` bytes, /// except the last chunk which may be smaller. pub fn split(&self, data: &[u8]) -> Vec> { + self.split_with_chunk_size(data, self.config.chunk_size) + } + + /// Split data into chunks using an explicit chunk size. + pub fn split_with_chunk_size(&self, data: &[u8], chunk_size: usize) -> Vec> { if data.is_empty() { return vec![vec![]]; } - data.chunks(self.config.chunk_size) - .map(|c| c.to_vec()) - .collect() + data.chunks(chunk_size).map(|c| c.to_vec()).collect() } /// Reassemble chunks into original data @@ -50,21 +76,33 @@ impl ChunkManager { /// Calculate the number of chunks for a given data size pub fn chunk_count(&self, size: usize) -> usize { + Self::chunk_count_for_size(size, self.config.chunk_size) + } + + pub fn chunk_count_for_size(size: usize, chunk_size: usize) -> usize { if size == 0 { return 1; } - (size + self.config.chunk_size - 1) / self.config.chunk_size + size.div_ceil(chunk_size) } /// Calculate the size of a specific chunk /// /// Returns the size of the chunk at the given index for data of the given total size. pub fn chunk_size_at(&self, total_size: usize, chunk_index: usize) -> usize { - let full_chunks = total_size / self.config.chunk_size; - let remainder = total_size % self.config.chunk_size; + Self::chunk_size_at_for_size(total_size, chunk_index, self.config.chunk_size) + } + + pub fn chunk_size_at_for_size( + total_size: usize, + chunk_index: usize, + chunk_size: usize, + ) -> usize { + let full_chunks = total_size / chunk_size; + let remainder = total_size % chunk_size; if chunk_index < full_chunks { - self.config.chunk_size + chunk_size } else if chunk_index == full_chunks && remainder > 0 { remainder } else { @@ -76,8 +114,16 @@ impl ChunkManager { /// /// Returns (start_offset, length) for the chunk at the given index. pub fn chunk_range(&self, total_size: usize, chunk_index: usize) -> (usize, usize) { - let start = chunk_index * self.config.chunk_size; - let length = self.chunk_size_at(total_size, chunk_index); + Self::chunk_range_for_size(total_size, chunk_index, self.config.chunk_size) + } + + pub fn chunk_range_for_size( + total_size: usize, + chunk_index: usize, + chunk_size: usize, + ) -> (usize, usize) { + let start = chunk_index * chunk_size; + let length = Self::chunk_size_at_for_size(total_size, chunk_index, chunk_size); (start, length) } } @@ -257,6 +303,15 @@ mod tests { assert_eq!(manager.chunk_range(2500, 2), (2048, 452)); } + #[test] + fn test_effective_chunk_size_scales_large_objects_up_to_target_chunk_count() { + let manager = ChunkManager::default(); + + assert_eq!(manager.effective_chunk_size(4 * 1024 * 1024), 8 * 1024 * 1024); + assert_eq!(manager.effective_chunk_size(256 * 1024 * 1024), 32 * 1024 * 1024); + assert_eq!(manager.effective_chunk_size(1024 * 1024 * 1024), 64 * 1024 * 1024); + } + #[test] fn test_chunk_id_to_key() { let id = ChunkId::data_shard("obj123", 0, 2); diff --git a/lightningstor/crates/lightningstor-distributed/src/lib.rs b/lightningstor/crates/lightningstor-distributed/src/lib.rs index 51a1acb..b2a2eb0 100644 --- a/lightningstor/crates/lightningstor-distributed/src/lib.rs +++ b/lightningstor/crates/lightningstor-distributed/src/lib.rs @@ -65,12 +65,14 @@ pub mod config; pub mod erasure; pub mod node; pub mod placement; +pub mod repair; // Re-export commonly used types pub use backends::{ErasureCodedBackend, ReplicatedBackend}; pub use config::{BucketStorageConfig, ChunkConfig, DistributedConfig, RedundancyMode}; pub use node::{MockNodeClient, MockNodeRegistry, NodeRegistry, StaticNodeRegistry}; pub use placement::{ConsistentHashSelector, NodeSelector, RandomSelector, RoundRobinSelector}; +pub use repair::{RepairQueue, ReplicatedRepairTask}; #[cfg(test)] mod tests { diff --git a/lightningstor/crates/lightningstor-distributed/src/repair.rs b/lightningstor/crates/lightningstor-distributed/src/repair.rs new file mode 100644 index 0000000..a93ae91 --- /dev/null +++ b/lightningstor/crates/lightningstor-distributed/src/repair.rs @@ -0,0 +1,58 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ReplicatedRepairTask { + pub id: String, + pub key: String, + pub shard_index: u32, + pub reason: String, + pub enqueued_at_millis: u64, + #[serde(default)] + pub attempt_count: u32, + #[serde(default)] + pub last_error: Option, + #[serde(default)] + pub next_attempt_after_millis: u64, +} + +impl ReplicatedRepairTask { + pub fn new(key: impl Into, shard_index: u32, reason: impl Into) -> Self { + let key = key.into(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + Self { + id: format!("replicated::{key}::{shard_index}"), + key, + shard_index, + reason: reason.into(), + enqueued_at_millis: now, + attempt_count: 0, + last_error: None, + next_attempt_after_millis: now, + } + } + + pub fn schedule_retry(&mut self, error: impl Into, backoff_millis: u64) { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + self.attempt_count = self.attempt_count.saturating_add(1); + self.last_error = Some(error.into()); + self.next_attempt_after_millis = now.saturating_add(backoff_millis); + } + + pub fn is_due(&self, now_millis: u64) -> bool { + now_millis >= self.next_attempt_after_millis + } +} + +#[async_trait] +pub trait RepairQueue: Send + Sync { + async fn enqueue_repair(&self, task: ReplicatedRepairTask); +} + diff --git a/lightningstor/crates/lightningstor-node/src/storage.rs b/lightningstor/crates/lightningstor-node/src/storage.rs index 79929b0..e813309 100644 --- a/lightningstor/crates/lightningstor-node/src/storage.rs +++ b/lightningstor/crates/lightningstor-node/src/storage.rs @@ -1,13 +1,18 @@ //! Local chunk storage use dashmap::DashMap; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use thiserror::Error; use tokio::fs; use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex; use tracing::debug; +const WRITE_LOCK_STRIPES: usize = 256; + /// Errors from chunk storage operations #[derive(Debug, Error)] pub enum StorageError { @@ -45,6 +50,12 @@ pub struct LocalChunkStore { /// Whether writes should be flushed before they are acknowledged. sync_on_write: bool, + + /// Monotonic nonce for per-write temporary paths. + temp_file_nonce: AtomicU64, + + /// Striped per-chunk write/delete locks to keep same-key updates coherent. + write_locks: Vec>, } impl LocalChunkStore { @@ -65,6 +76,8 @@ impl LocalChunkStore { max_capacity, chunk_count: AtomicU64::new(0), sync_on_write, + temp_file_nonce: AtomicU64::new(0), + write_locks: (0..WRITE_LOCK_STRIPES).map(|_| Mutex::new(())).collect(), }; // Scan existing chunks @@ -91,7 +104,7 @@ impl LocalChunkStore { if metadata.is_file() { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - if name.ends_with(".tmp") { + if name.ends_with(".tmp") || name.starts_with(".tmp.") { continue; } @@ -131,6 +144,25 @@ impl LocalChunkStore { self.data_dir.join(safe_id) } + fn temporary_chunk_path(&self, path: &std::path::Path) -> PathBuf { + let nonce = self.temp_file_nonce.fetch_add(1, Ordering::Relaxed); + let pid = std::process::id(); + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("chunk"); + path.parent() + .unwrap_or(&self.data_dir) + .join(format!(".tmp.{file_name}.{pid}.{nonce}")) + } + + fn write_lock(&self, chunk_id: &str) -> &Mutex<()> { + let mut hasher = DefaultHasher::new(); + chunk_id.hash(&mut hasher); + let slot = (hasher.finish() as usize) % self.write_locks.len().max(1); + &self.write_locks[slot] + } + async fn resolve_existing_chunk_path(&self, chunk_id: &str) -> StorageResult { if let Some(path) = self.chunk_paths.get(chunk_id) { return Ok(path.clone()); @@ -154,6 +186,7 @@ impl LocalChunkStore { /// Store a chunk pub async fn put(&self, chunk_id: &str, data: &[u8]) -> StorageResult { + let _guard = self.write_lock(chunk_id).lock().await; let size = data.len() as u64; // Check if replacing existing chunk @@ -169,7 +202,7 @@ impl LocalChunkStore { } let path = self.chunk_path(chunk_id); - let temp_path = path.with_extension(".tmp"); + let temp_path = self.temporary_chunk_path(&path); if let Some(parent) = path.parent() { // Multipart uploads fan out concurrent writes into the same shard // directory. Create the parent path unconditionally so no writer can @@ -217,6 +250,7 @@ impl LocalChunkStore { /// Delete a chunk pub async fn delete(&self, chunk_id: &str) -> StorageResult<()> { + let _guard = self.write_lock(chunk_id).lock().await; if let Some((_, size)) = self.chunk_sizes.remove(chunk_id) { let path = match self.chunk_paths.remove(chunk_id) { Some((_, path)) => path, @@ -421,4 +455,34 @@ mod tests { assert_eq!(store.chunk_count(), 16); } + + #[tokio::test] + async fn test_concurrent_rewrites_same_chunk_use_unique_temp_paths() { + let (store, _temp) = create_test_store().await; + let store = Arc::new(store); + let barrier = Arc::new(Barrier::new(9)); + let mut tasks = Vec::new(); + + for idx in 0..8u8 { + let store = Arc::clone(&store); + let barrier = Arc::clone(&barrier); + tasks.push(tokio::spawn(async move { + let payload = vec![idx; 2048]; + barrier.wait().await; + store.put("shared-chunk", &payload).await.unwrap(); + payload + })); + } + + barrier.wait().await; + + let mut expected_payloads = Vec::new(); + for task in tasks { + expected_payloads.push(task.await.unwrap()); + } + + let stored = store.get("shared-chunk").await.unwrap(); + assert!(expected_payloads.iter().any(|payload| payload == &stored)); + assert_eq!(store.chunk_count(), 1); + } } diff --git a/lightningstor/crates/lightningstor-server/Cargo.toml b/lightningstor/crates/lightningstor-server/Cargo.toml index 7dfbb21..2ca79b5 100644 --- a/lightningstor/crates/lightningstor-server/Cargo.toml +++ b/lightningstor/crates/lightningstor-server/Cargo.toml @@ -17,6 +17,7 @@ lightningstor-distributed = { workspace = true } lightningstor-storage = { workspace = true } chainfire-client = { path = "../../../chainfire/chainfire-client" } flaredb-client = { path = "../../../flaredb/crates/flaredb-client" } +iam-api = { path = "../../../iam/crates/iam-api" } iam-service-auth = { path = "../../../iam/crates/iam-service-auth" } tonic = { workspace = true } tonic-health = { workspace = true } diff --git a/lightningstor/crates/lightningstor-server/src/lib.rs b/lightningstor/crates/lightningstor-server/src/lib.rs index df7e0e5..8afe3fb 100644 --- a/lightningstor/crates/lightningstor-server/src/lib.rs +++ b/lightningstor/crates/lightningstor-server/src/lib.rs @@ -9,8 +9,11 @@ mod bucket_service; pub mod config; pub mod metadata; mod object_service; +pub mod repair; pub mod s3; +pub mod tenant; pub use bucket_service::BucketServiceImpl; pub use config::ServerConfig; pub use object_service::ObjectServiceImpl; +pub use repair::{MetadataRepairQueue, spawn_replicated_repair_worker}; diff --git a/lightningstor/crates/lightningstor-server/src/main.rs b/lightningstor/crates/lightningstor-server/src/main.rs index 3ca84ec..f55d8d5 100644 --- a/lightningstor/crates/lightningstor-server/src/main.rs +++ b/lightningstor/crates/lightningstor-server/src/main.rs @@ -5,11 +5,13 @@ use clap::Parser; use iam_service_auth::AuthService; use lightningstor_api::{BucketServiceServer, ObjectServiceServer}; use lightningstor_distributed::{ - DistributedConfig, ErasureCodedBackend, RedundancyMode, ReplicatedBackend, StaticNodeRegistry, + DistributedConfig, ErasureCodedBackend, RedundancyMode, ReplicatedBackend, RepairQueue, + StaticNodeRegistry, }; use lightningstor_server::{ config::{MetadataBackend, ObjectStorageBackend}, metadata::MetadataStore, + repair::{spawn_replicated_repair_worker, MetadataRepairQueue}, s3, BucketServiceImpl, ObjectServiceImpl, ServerConfig, }; use lightningstor_storage::{LocalFsBackend, StorageBackend}; @@ -28,6 +30,12 @@ const OBJECT_GRPC_INITIAL_STREAM_WINDOW: u32 = 64 * 1024 * 1024; const OBJECT_GRPC_INITIAL_CONNECTION_WINDOW: u32 = 512 * 1024 * 1024; const OBJECT_GRPC_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30); const OBJECT_GRPC_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10); +const REPLICATED_REPAIR_SCAN_INTERVAL: Duration = Duration::from_secs(5); + +struct StorageRuntime { + backend: Arc, + repair_worker: Option>, +} /// LightningStor object storage server #[derive(Parser, Debug)] @@ -148,8 +156,6 @@ async fn main() -> Result<(), Box> { metrics_addr ); - let storage = create_storage_backend(&config).await?; - if let Some(endpoint) = &config.chainfire_endpoint { tracing::info!(" Cluster coordination: ChainFire @ {}", endpoint); let endpoint = endpoint.clone(); @@ -204,6 +210,10 @@ async fn main() -> Result<(), Box> { } }; + let storage_runtime = create_storage_backend(&config, metadata.clone()).await?; + let storage = storage_runtime.backend.clone(); + let _repair_worker = storage_runtime.repair_worker; + // Initialize IAM authentication service tracing::info!( "Connecting to IAM server at {}", @@ -253,7 +263,11 @@ async fn main() -> Result<(), Box> { let s3_addr: SocketAddr = config.s3_addr; // Start S3 HTTP server with shared state - let s3_router = s3::create_router_with_state(storage.clone(), metadata.clone()); + let s3_router = s3::create_router_with_auth( + storage.clone(), + metadata.clone(), + Some(config.auth.iam_server_addr.clone()), + ); let s3_server = tokio::spawn(async move { tracing::info!("S3 HTTP server listening on {}", s3_addr); let listener = tokio::net::TcpListener::bind(s3_addr).await.unwrap(); @@ -422,24 +436,27 @@ async fn register_chainfire_membership( async fn create_storage_backend( config: &ServerConfig, -) -> Result, Box> { + metadata: Arc, +) -> Result> { match config.object_storage_backend { ObjectStorageBackend::LocalFs => { tracing::info!("Object storage backend: local_fs"); - Ok(Arc::new( - LocalFsBackend::new(&config.data_dir, config.sync_on_write).await?, - )) + Ok(StorageRuntime { + backend: Arc::new(LocalFsBackend::new(&config.data_dir, config.sync_on_write).await?), + repair_worker: None, + }) } ObjectStorageBackend::Distributed => { tracing::info!("Object storage backend: distributed"); - create_distributed_storage_backend(&config.distributed).await + create_distributed_storage_backend(&config.distributed, metadata).await } } } async fn create_distributed_storage_backend( config: &DistributedConfig, -) -> Result, Box> { + metadata: Arc, +) -> Result> { let endpoints: Vec = config .node_endpoints .iter() @@ -501,9 +518,25 @@ async fn create_distributed_storage_backend( write_quorum, "Using replicated LightningStor storage backend" ); - Ok(Arc::new( - ReplicatedBackend::new(config.clone(), registry).await?, - )) + let repair_queue: Arc = + Arc::new(MetadataRepairQueue::new(metadata.clone())); + let backend = Arc::new( + ReplicatedBackend::new_with_repair_queue( + config.clone(), + registry, + Some(repair_queue), + ) + .await?, + ); + let repair_worker = Some(spawn_replicated_repair_worker( + metadata, + backend.clone(), + REPLICATED_REPAIR_SCAN_INTERVAL, + )); + Ok(StorageRuntime { + backend, + repair_worker, + }) } RedundancyMode::ErasureCoded { data_shards, @@ -514,9 +547,10 @@ async fn create_distributed_storage_backend( parity_shards, "Using erasure-coded LightningStor storage backend" ); - Ok(Arc::new( - ErasureCodedBackend::new(config.clone(), registry).await?, - )) + Ok(StorageRuntime { + backend: Arc::new(ErasureCodedBackend::new(config.clone(), registry).await?), + repair_worker: None, + }) } RedundancyMode::None => Err(std::io::Error::other( "distributed object storage does not support redundancy.type=none; use object_storage_backend=local_fs instead", diff --git a/lightningstor/crates/lightningstor-server/src/metadata.rs b/lightningstor/crates/lightningstor-server/src/metadata.rs index a956afc..e44d993 100644 --- a/lightningstor/crates/lightningstor-server/src/metadata.rs +++ b/lightningstor/crates/lightningstor-server/src/metadata.rs @@ -2,6 +2,7 @@ use dashmap::DashMap; use flaredb_client::RdbClient; +use lightningstor_distributed::ReplicatedRepairTask; use lightningstor_types::{Bucket, BucketId, MultipartUpload, Object, ObjectId, Result}; use serde_json; use sqlx::pool::PoolOptions; @@ -215,6 +216,12 @@ impl MetadataStore { end_key } + fn exclusive_scan_start(key: &[u8]) -> Vec { + let mut next = key.to_vec(); + next.push(0); + next + } + fn flaredb_client_for_key<'a>( clients: &'a [Arc>], key: &[u8], @@ -422,6 +429,56 @@ impl MetadataStore { Ok(results) } + async fn flaredb_scan_page( + clients: &[Arc>], + prefix: &[u8], + start_after: Option<&[u8]>, + limit: u32, + ) -> Result<(Vec<(String, String)>, bool)> { + let end_key = Self::prefix_end(prefix); + let start_key = start_after + .map(Self::exclusive_scan_start) + .unwrap_or_else(|| prefix.to_vec()); + let fetch_limit = limit.saturating_add(1).max(1); + let client = Self::flaredb_scan_client(clients); + let (mut items, next) = match { + let mut c = client.lock().await; + c.raw_scan(start_key.clone(), end_key.clone(), fetch_limit).await + } { + Ok((keys, values, next)) => { + let items = keys + .into_iter() + .zip(values.into_iter()) + .map(|(key, value)| { + ( + String::from_utf8_lossy(&key).to_string(), + String::from_utf8_lossy(&value).to_string(), + ) + }) + .collect::>(); + (items, next) + } + Err(status) if Self::flaredb_requires_strong(&status) => { + Self::flaredb_scan_strong(client, &start_key, &end_key, fetch_limit).await? + } + Err(error) => { + return Err(lightningstor_types::Error::StorageError(format!( + "FlareDB scan failed: {}", + error + ))); + } + }; + + let has_more = if items.len() > limit as usize { + items.truncate(limit as usize); + true + } else { + next.is_some() + }; + + Ok((items, has_more)) + } + async fn flaredb_has_prefix(clients: &[Arc>], prefix: &[u8]) -> Result { let end_key = Self::prefix_end(prefix); let client = Self::flaredb_scan_client(clients); @@ -613,11 +670,146 @@ impl MetadataStore { results.push((entry.key().clone(), entry.value().clone())); } } + results.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); Ok(results) } } } + async fn get_prefix_page( + &self, + prefix: &str, + start_after: Option<&str>, + limit: u32, + ) -> Result<(Vec<(String, String)>, bool)> { + if limit == 0 { + return Ok((Vec::new(), false)); + } + + match &self.backend { + StorageBackend::FlareDB(client) => { + Self::flaredb_scan_page( + client, + prefix.as_bytes(), + start_after.map(str::as_bytes), + limit, + ) + .await + } + StorageBackend::Sql(sql) => { + let prefix_end = String::from_utf8(Self::prefix_end(prefix.as_bytes())).map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "Failed to encode prefix end: {}", + e + )) + })?; + let fetch_limit = (limit.saturating_add(1)) as i64; + match sql { + SqlStorageBackend::Postgres(pool) => { + let rows: Vec<(String, String)> = if let Some(after) = start_after { + sqlx::query_as( + "SELECT key, value FROM metadata_kv + WHERE key >= $1 AND key < $2 AND key > $3 + ORDER BY key + LIMIT $4", + ) + .bind(prefix) + .bind(&prefix_end) + .bind(after) + .bind(fetch_limit) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "Postgres paged scan failed: {}", + e + )) + })? + } else { + sqlx::query_as( + "SELECT key, value FROM metadata_kv + WHERE key >= $1 AND key < $2 + ORDER BY key + LIMIT $3", + ) + .bind(prefix) + .bind(&prefix_end) + .bind(fetch_limit) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "Postgres paged scan failed: {}", + e + )) + })? + }; + let has_more = rows.len() > limit as usize; + let items = rows.into_iter().take(limit as usize).collect(); + Ok((items, has_more)) + } + SqlStorageBackend::Sqlite(pool) => { + let rows: Vec<(String, String)> = if let Some(after) = start_after { + sqlx::query_as( + "SELECT key, value FROM metadata_kv + WHERE key >= ?1 AND key < ?2 AND key > ?3 + ORDER BY key + LIMIT ?4", + ) + .bind(prefix) + .bind(&prefix_end) + .bind(after) + .bind(fetch_limit) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "SQLite paged scan failed: {}", + e + )) + })? + } else { + sqlx::query_as( + "SELECT key, value FROM metadata_kv + WHERE key >= ?1 AND key < ?2 + ORDER BY key + LIMIT ?3", + ) + .bind(prefix) + .bind(&prefix_end) + .bind(fetch_limit) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "SQLite paged scan failed: {}", + e + )) + })? + }; + let has_more = rows.len() > limit as usize; + let items = rows.into_iter().take(limit as usize).collect(); + Ok((items, has_more)) + } + } + } + StorageBackend::InMemory(map) => { + let mut rows: Vec<(String, String)> = map + .iter() + .filter(|entry| entry.key().starts_with(prefix)) + .map(|entry| (entry.key().clone(), entry.value().clone())) + .collect(); + rows.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); + if let Some(after) = start_after { + rows.retain(|(key, _)| key.as_str() > after); + } + let has_more = rows.len() > limit as usize; + let items = rows.into_iter().take(limit as usize).collect(); + Ok((items, has_more)) + } + } + } + /// Internal: check if any key exists with a prefix async fn has_prefix(&self, prefix: &str) -> Result { match &self.backend { @@ -708,10 +900,64 @@ impl MetadataStore { "/lightningstor/multipart/uploads/" } + fn multipart_bucket_key(bucket_id: &str, object_key: &str, upload_id: &str) -> String { + format!( + "/lightningstor/multipart/by-bucket/{}/{}/{}", + bucket_id, object_key, upload_id + ) + } + + fn multipart_bucket_prefix(bucket_id: &BucketId, prefix: &str) -> String { + format!("/lightningstor/multipart/by-bucket/{}/{}", bucket_id, prefix) + } + fn multipart_object_key(object_id: &ObjectId) -> String { format!("/lightningstor/multipart/objects/{}", object_id) } + fn replicated_repair_task_key(task_id: &str) -> String { + format!("/lightningstor/repair/replicated/{}", task_id) + } + + fn replicated_repair_task_prefix() -> &'static str { + "/lightningstor/repair/replicated/" + } + + pub async fn save_replicated_repair_task(&self, task: &ReplicatedRepairTask) -> Result<()> { + let key = Self::replicated_repair_task_key(&task.id); + let value = serde_json::to_string(task).map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "Failed to serialize replicated repair task: {}", + e + )) + })?; + self.put(&key, &value).await + } + + pub async fn list_replicated_repair_tasks( + &self, + limit: u32, + ) -> Result> { + let (items, _) = self + .get_prefix_page(Self::replicated_repair_task_prefix(), None, limit) + .await?; + let mut tasks = Vec::new(); + for (_, value) in items { + let task: ReplicatedRepairTask = serde_json::from_str(&value).map_err(|e| { + lightningstor_types::Error::StorageError(format!( + "Failed to deserialize replicated repair task: {}", + e + )) + })?; + tasks.push(task); + } + Ok(tasks) + } + + pub async fn delete_replicated_repair_task(&self, task_id: &str) -> Result<()> { + self.delete_key(&Self::replicated_repair_task_key(task_id)).await + } + /// Save bucket metadata pub async fn save_bucket(&self, bucket: &Bucket) -> Result<()> { let key = Self::bucket_key(&bucket.org_id, &bucket.project_id, bucket.name.as_str()); @@ -900,6 +1146,13 @@ impl MetadataStore { prefix: &str, max_keys: u32, ) -> Result> { + if max_keys > 0 { + return self + .list_objects_page(bucket_id, prefix, None, max_keys) + .await + .map(|(objects, _)| objects); + } + let prefix_key = Self::object_prefix(bucket_id, prefix); let items = self.get_prefix(&prefix_key).await?; @@ -921,6 +1174,34 @@ impl MetadataStore { Ok(objects) } + pub async fn list_objects_page( + &self, + bucket_id: &BucketId, + prefix: &str, + start_after_key: Option<&str>, + max_keys: u32, + ) -> Result<(Vec, bool)> { + if max_keys == 0 { + return Ok((Vec::new(), false)); + } + + let prefix_key = Self::object_prefix(bucket_id, prefix); + let start_after_storage_key = + start_after_key.map(|key| Self::object_key(bucket_id, key, None)); + let (items, has_more) = self + .get_prefix_page(&prefix_key, start_after_storage_key.as_deref(), max_keys) + .await?; + + let mut objects = Vec::new(); + for (_, value) in items { + if let Ok(object) = serde_json::from_str::(&value) { + objects.push(object); + } + } + + Ok((objects, has_more)) + } + pub async fn save_multipart_upload(&self, upload: &MultipartUpload) -> Result<()> { let key = Self::multipart_upload_key(upload.upload_id.as_str()); let value = serde_json::to_string(upload).map_err(|e| { @@ -929,7 +1210,16 @@ impl MetadataStore { e )) })?; - self.put(&key, &value).await + self.put(&key, &value).await?; + self.put( + &Self::multipart_bucket_key( + &upload.bucket_id, + upload.key.as_str(), + upload.upload_id.as_str(), + ), + &value, + ) + .await } pub async fn load_multipart_upload(&self, upload_id: &str) -> Result> { @@ -948,6 +1238,14 @@ impl MetadataStore { } pub async fn delete_multipart_upload(&self, upload_id: &str) -> Result<()> { + if let Some(upload) = self.load_multipart_upload(upload_id).await? { + self.delete_key(&Self::multipart_bucket_key( + &upload.bucket_id, + upload.key.as_str(), + upload.upload_id.as_str(), + )) + .await?; + } self.delete_key(&Self::multipart_upload_key(upload_id)).await } @@ -957,14 +1255,30 @@ impl MetadataStore { prefix: &str, max_uploads: u32, ) -> Result> { - let items = self.get_prefix(Self::multipart_upload_prefix()).await?; + let index_prefix = Self::multipart_bucket_prefix(bucket_id, prefix); + let items = if max_uploads > 0 { + self.get_prefix_page(&index_prefix, None, max_uploads) + .await? + .0 + } else { + self.get_prefix(&index_prefix).await? + }; let mut uploads = Vec::new(); for (_, value) in items { if let Ok(upload) = serde_json::from_str::(&value) { - if upload.bucket_id == bucket_id.to_string() - && upload.key.as_str().starts_with(prefix) - { - uploads.push(upload); + uploads.push(upload); + } + } + + if uploads.is_empty() { + let fallback_items = self.get_prefix(Self::multipart_upload_prefix()).await?; + for (_, value) in fallback_items { + if let Ok(upload) = serde_json::from_str::(&value) { + if upload.bucket_id == bucket_id.to_string() + && upload.key.as_str().starts_with(prefix) + { + uploads.push(upload); + } } } } @@ -1033,6 +1347,7 @@ fn normalize_transport_addr(endpoint: &str) -> String { #[cfg(test)] mod tests { use super::*; + use lightningstor_distributed::ReplicatedRepairTask; use lightningstor_types::{BucketName, ETag, ObjectKey}; #[tokio::test] @@ -1119,4 +1434,123 @@ mod tests { .is_none() ); } + + #[tokio::test] + async fn list_objects_page_honors_start_after_and_has_more() { + let store = MetadataStore::new_in_memory(); + let bucket = Bucket::new( + BucketName::new("paged-bucket").unwrap(), + "org-a", + "project-a", + "default", + ); + store.save_bucket(&bucket).await.unwrap(); + + for key in ["a.txt", "b.txt", "c.txt"] { + let mut object = Object::new( + bucket.id.to_string(), + ObjectKey::new(key).unwrap(), + ETag::from_md5(&[7u8; 16]), + 128, + Some("text/plain".to_string()), + ); + object.version = lightningstor_types::ObjectVersion::null(); + store.save_object(&object).await.unwrap(); + } + + let (first_page, first_has_more) = store + .list_objects_page(&bucket.id, "", None, 2) + .await + .unwrap(); + assert_eq!( + first_page + .iter() + .map(|object| object.key.as_str().to_string()) + .collect::>(), + vec!["a.txt".to_string(), "b.txt".to_string()] + ); + assert!(first_has_more); + + let (second_page, second_has_more) = store + .list_objects_page(&bucket.id, "", Some("b.txt"), 2) + .await + .unwrap(); + assert_eq!( + second_page + .iter() + .map(|object| object.key.as_str().to_string()) + .collect::>(), + vec!["c.txt".to_string()] + ); + assert!(!second_has_more); + } + + #[tokio::test] + async fn list_multipart_uploads_uses_bucket_prefix_index() { + let store = MetadataStore::new_in_memory(); + let bucket = Bucket::new( + BucketName::new("multipart-bucket").unwrap(), + "org-a", + "project-a", + "default", + ); + store.save_bucket(&bucket).await.unwrap(); + + let upload_a = MultipartUpload::new(bucket.id.to_string(), ObjectKey::new("a/one.bin").unwrap()); + let upload_b = MultipartUpload::new(bucket.id.to_string(), ObjectKey::new("a/two.bin").unwrap()); + let other_bucket = Bucket::new( + BucketName::new("other-bucket").unwrap(), + "org-a", + "project-a", + "default", + ); + store.save_bucket(&other_bucket).await.unwrap(); + let upload_other = + MultipartUpload::new(other_bucket.id.to_string(), ObjectKey::new("a/three.bin").unwrap()); + + store.save_multipart_upload(&upload_a).await.unwrap(); + store.save_multipart_upload(&upload_b).await.unwrap(); + store.save_multipart_upload(&upload_other).await.unwrap(); + + let uploads = store + .list_multipart_uploads(&bucket.id, "a/", 10) + .await + .unwrap(); + assert_eq!(uploads.len(), 2); + assert_eq!( + uploads + .iter() + .map(|upload| upload.key.as_str().to_string()) + .collect::>(), + vec!["a/one.bin".to_string(), "a/two.bin".to_string()] + ); + } + + #[tokio::test] + async fn replicated_repair_tasks_round_trip() { + let store = MetadataStore::new_in_memory(); + let mut task = ReplicatedRepairTask::new("obj_abc", 0, "quorum write"); + store.save_replicated_repair_task(&task).await.unwrap(); + + let tasks = store.list_replicated_repair_tasks(10).await.unwrap(); + assert_eq!(tasks.len(), 1); + assert_eq!(tasks[0].key, "obj_abc"); + + task.schedule_retry("transient failure", 5_000); + store.save_replicated_repair_task(&task).await.unwrap(); + + let tasks = store.list_replicated_repair_tasks(10).await.unwrap(); + assert_eq!(tasks[0].attempt_count, 1); + assert_eq!(tasks[0].last_error.as_deref(), Some("transient failure")); + + store + .delete_replicated_repair_task(&task.id) + .await + .unwrap(); + assert!(store + .list_replicated_repair_tasks(10) + .await + .unwrap() + .is_empty()); + } } diff --git a/lightningstor/crates/lightningstor-server/src/object_service.rs b/lightningstor/crates/lightningstor-server/src/object_service.rs index 68874d1..d323ef6 100644 --- a/lightningstor/crates/lightningstor-server/src/object_service.rs +++ b/lightningstor/crates/lightningstor-server/src/object_service.rs @@ -155,6 +155,10 @@ impl ObjectServiceImpl { .await .map_err(|e| Status::internal(format!("Failed to delete multipart part: {}", e)))?; } + self.storage + .delete_upload_parts(upload.upload_id.as_str()) + .await + .map_err(|e| Status::internal(format!("Failed to clean multipart upload: {}", e)))?; Ok(()) } @@ -465,17 +469,15 @@ impl ObjectService for ObjectServiceImpl { let (start, end) = Self::resolve_range(object.size as usize, req.range_start, req.range_end); - if object.etag.is_multipart() { - if let Some(upload) = self - .metadata - .load_object_multipart_upload(&object.id) - .await - .map_err(Self::to_status)? - { - return Ok(Response::new( - self.multipart_object_stream(&object, upload, start, end), - )); - } + if let Some(upload) = self + .metadata + .load_object_multipart_upload(&object.id) + .await + .map_err(Self::to_status)? + { + return Ok(Response::new( + self.multipart_object_stream(&object, upload, start, end), + )); } let data = self @@ -524,28 +526,21 @@ impl ObjectService for ObjectServiceImpl { .map_err(Self::to_status)? .ok_or_else(|| Status::not_found(format!("Object {} not found", req.key)))?; - if object.etag.is_multipart() { - if let Some(upload) = self - .metadata - .load_object_multipart_upload(&object.id) + if let Some(upload) = self + .metadata + .load_object_multipart_upload(&object.id) + .await + .map_err(Self::to_status)? + { + self.delete_multipart_parts(&upload).await?; + self.metadata + .delete_object_multipart_upload(&object.id) .await - .map_err(Self::to_status)? - { - self.delete_multipart_parts(&upload).await?; - self.metadata - .delete_object_multipart_upload(&object.id) - .await - .map_err(Self::to_status)?; - self.metadata - .delete_multipart_upload(upload.upload_id.as_str()) - .await - .map_err(Self::to_status)?; - } else { - self.storage - .delete_object(&object.id) - .await - .map_err(|e| Status::internal(format!("Failed to delete object: {}", e)))?; - } + .map_err(Self::to_status)?; + self.metadata + .delete_multipart_upload(upload.upload_id.as_str()) + .await + .map_err(Self::to_status)?; } else { self.storage .delete_object(&object.id) diff --git a/lightningstor/crates/lightningstor-server/src/repair.rs b/lightningstor/crates/lightningstor-server/src/repair.rs new file mode 100644 index 0000000..21c2277 --- /dev/null +++ b/lightningstor/crates/lightningstor-server/src/repair.rs @@ -0,0 +1,182 @@ +use crate::metadata::MetadataStore; +use async_trait::async_trait; +use lightningstor_distributed::{RepairQueue, ReplicatedBackend, ReplicatedRepairTask}; +use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use tracing::{debug, warn}; + +const REPAIR_SCAN_LIMIT: u32 = 256; +const REPAIR_BACKOFF_BASE_MILLIS: u64 = 1_000; +const REPAIR_BACKOFF_MAX_MILLIS: u64 = 60_000; +const ORPHAN_REPAIR_DROP_ATTEMPTS: u32 = 8; + +pub struct MetadataRepairQueue { + metadata: Arc, +} + +impl MetadataRepairQueue { + pub fn new(metadata: Arc) -> Self { + Self { metadata } + } +} + +#[async_trait] +impl RepairQueue for MetadataRepairQueue { + async fn enqueue_repair(&self, task: ReplicatedRepairTask) { + if let Err(error) = self.metadata.save_replicated_repair_task(&task).await { + warn!( + task_id = task.id, + chunk_key = task.key, + error = %error, + "failed to persist replicated repair task" + ); + } + } +} + +pub fn spawn_replicated_repair_worker( + metadata: Arc, + backend: Arc, + interval: Duration, +) -> JoinHandle<()> { + tokio::spawn(async move { + loop { + if let Err(error) = process_replicated_repair_queue(&metadata, &backend).await { + if replicated_repair_queue_transiently_unready(&error) { + debug!(error = %error, "replicated repair queue pass deferred until metadata becomes ready"); + } else { + warn!(error = %error, "replicated repair queue pass failed"); + } + } + sleep(interval).await; + } + }) +} + +async fn process_replicated_repair_queue( + metadata: &MetadataStore, + backend: &ReplicatedBackend, +) -> Result<(), lightningstor_types::Error> { + let now = unix_time_millis(); + let tasks = metadata + .list_replicated_repair_tasks(REPAIR_SCAN_LIMIT) + .await?; + for mut task in tasks { + if !task.is_due(now) { + continue; + } + match backend.repair_chunk(&task).await { + Ok(()) => { + metadata.delete_replicated_repair_task(&task.id).await?; + debug!( + task_id = task.id, + chunk_key = task.key, + "repaired replicated chunk" + ); + } + Err(error) => { + if task.attempt_count >= ORPHAN_REPAIR_DROP_ATTEMPTS { + match backend.chunk_exists_anywhere(&task.key).await { + Ok(false) => { + warn!( + task_id = task.id, + chunk_key = task.key, + attempts = task.attempt_count, + "dropping orphan replicated repair task with no remaining source replica" + ); + metadata.delete_replicated_repair_task(&task.id).await?; + continue; + } + Ok(true) => {} + Err(probe_error) => { + warn!( + task_id = task.id, + chunk_key = task.key, + error = %probe_error, + "failed to probe global chunk existence while evaluating orphan repair task" + ); + } + } + } + let backoff = repair_backoff_millis(task.attempt_count); + task.schedule_retry(error.to_string(), backoff); + metadata.save_replicated_repair_task(&task).await?; + warn!( + task_id = task.id, + chunk_key = task.key, + attempts = task.attempt_count, + backoff_millis = backoff, + error = %error, + "replicated chunk repair failed" + ); + } + } + } + Ok(()) +} + +fn unix_time_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 +} + +fn repair_backoff_millis(attempt_count: u32) -> u64 { + let exponent = attempt_count.min(6); + let multiplier = 1u64 << exponent; + (REPAIR_BACKOFF_BASE_MILLIS.saturating_mul(multiplier)).min(REPAIR_BACKOFF_MAX_MILLIS) +} + +fn replicated_repair_queue_transiently_unready(error: &lightningstor_types::Error) -> bool { + let rendered = error.to_string().to_ascii_lowercase(); + let transient = rendered.contains("region not found") + || rendered.contains("status: notfound") + || rendered.contains("metadata backend not ready") + || rendered.contains("notleader"); + if transient { + return true; + } + + match error { + lightningstor_types::Error::StorageError(message) + | lightningstor_types::Error::Internal(message) => { + let message = message.to_ascii_lowercase(); + message.contains("region not found") + || message.contains("status: notfound") + || message.contains("metadata backend not ready") + || message.contains("notleader") + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::replicated_repair_queue_transiently_unready; + + #[test] + fn treats_region_not_found_as_transient_startup_state() { + let error = lightningstor_types::Error::StorageError( + "FlareDB scan failed: status: NotFound, message: \"region not found\"".to_string(), + ); + assert!(replicated_repair_queue_transiently_unready(&error)); + } + + #[test] + fn treats_wrapped_storage_error_rendering_as_transient_startup_state() { + let error = lightningstor_types::Error::StorageError( + "FlareDB scan failed: status: NotFound, message: \"region not found\", details: [], metadata: MetadataMap { headers: {} }".to_string(), + ); + assert!(replicated_repair_queue_transiently_unready(&error)); + } + + #[test] + fn keeps_real_repair_failures_as_warnings() { + let error = + lightningstor_types::Error::StorageError("replication checksum mismatch".to_string()); + assert!(!replicated_repair_queue_transiently_unready(&error)); + } +} diff --git a/lightningstor/crates/lightningstor-server/src/s3/auth.rs b/lightningstor/crates/lightningstor-server/src/s3/auth.rs index 3d1f781..67cee5b 100644 --- a/lightningstor/crates/lightningstor-server/src/s3/auth.rs +++ b/lightningstor/crates/lightningstor-server/src/s3/auth.rs @@ -10,13 +10,17 @@ use axum::{ middleware::Next, response::{IntoResponse, Response}, }; +use crate::tenant::TenantContext; use hmac::{Hmac, Mac}; +use iam_api::proto::{iam_credential_client::IamCredentialClient, GetSecretKeyRequest}; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; +use tonic::transport::Channel; use tracing::{debug, warn}; use url::form_urlencoded; +use std::time::{Duration as StdDuration, Instant}; type HmacSha256 = Hmac; const DEFAULT_MAX_AUTH_BODY_BYTES: usize = 1024 * 1024 * 1024; @@ -27,6 +31,13 @@ pub(crate) struct VerifiedBodyBytes(pub Bytes); #[derive(Clone, Debug)] pub(crate) struct VerifiedPayloadHash(pub String); +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct VerifiedTenantContext(pub TenantContext); + +fn should_buffer_auth_body(payload_hash_header: Option<&str>) -> bool { + payload_hash_header.is_none() +} + /// SigV4 authentication state #[derive(Clone)] pub struct AuthState { @@ -40,21 +51,73 @@ pub struct AuthState { aws_service: String, } -/// Placeholder IAM client (will integrate with real IAM later) pub struct IamClient { - // Stores access_key_id -> secret_key mapping - credentials: std::collections::HashMap, + mode: IamClientMode, + credential_cache: Arc>>, + cache_ttl: StdDuration, +} + +enum IamClientMode { + Env { + credentials: std::collections::HashMap, + }, + Grpc { + endpoint: String, + channel: Arc>>, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ResolvedCredential { + pub secret_key: String, + pub principal_id: String, + pub org_id: Option, + pub project_id: Option, +} + +struct CachedCredential { + credential: ResolvedCredential, + cached_at: Instant, } impl IamClient { - /// Create a new IamClient loading credentials from environment variables for MVP. + /// Create a new IAM client. If an endpoint is supplied, use the IAM gRPC API. + pub fn new(iam_endpoint: Option) -> Self { + let cache_ttl = std::env::var("LIGHTNINGSTOR_S3_IAM_CACHE_TTL_SECS") + .ok() + .and_then(|value| value.parse::().ok()) + .map(StdDuration::from_secs) + .unwrap_or_else(|| StdDuration::from_secs(30)); + + if let Some(endpoint) = iam_endpoint + .map(|value| normalize_iam_endpoint(&value)) + .filter(|value| !value.is_empty()) + { + return Self { + mode: IamClientMode::Grpc { + endpoint, + channel: Arc::new(Mutex::new(None)), + }, + credential_cache: Arc::new(RwLock::new(HashMap::new())), + cache_ttl, + }; + } + + Self { + mode: IamClientMode::Env { + credentials: Self::load_env_credentials(), + }, + credential_cache: Arc::new(RwLock::new(HashMap::new())), + cache_ttl, + } + } + + /// Load credentials from environment variables for fallback/testing. /// /// Supports two formats: /// 1. Single credential: S3_ACCESS_KEY_ID + S3_SECRET_KEY /// 2. Multiple credentials: S3_CREDENTIALS="key1:secret1,key2:secret2,..." - /// - /// TODO: Replace with proper IAM gRPC integration (see T060) - pub fn new() -> Self { + fn load_env_credentials() -> std::collections::HashMap { let mut credentials = std::collections::HashMap::new(); // Option 1: Multiple credentials via S3_CREDENTIALS @@ -87,28 +150,160 @@ impl IamClient { warn!("Set S3_CREDENTIALS or S3_ACCESS_KEY_ID/S3_SECRET_KEY to enable access."); } - Self { credentials } + credentials } - /// Validate access key and return secret key - pub async fn get_secret_key(&self, access_key_id: &str) -> Result { - self.credentials - .get(access_key_id) - .cloned() - .ok_or_else(|| "Access key ID not found".to_string()) + #[cfg(test)] + fn env_credentials(&self) -> Option<&std::collections::HashMap> { + match &self.mode { + IamClientMode::Env { credentials } => Some(credentials), + IamClientMode::Grpc { .. } => None, + } + } + + fn env_default_tenant() -> (Option, Option) { + let org_id = std::env::var("S3_TENANT_ORG_ID") + .ok() + .or_else(|| std::env::var("S3_ORG_ID").ok()) + .or_else(|| Some("default".to_string())); + let project_id = std::env::var("S3_TENANT_PROJECT_ID") + .ok() + .or_else(|| std::env::var("S3_PROJECT_ID").ok()) + .or_else(|| Some("default".to_string())); + (org_id, project_id) + } + + /// Validate access key and resolve the credential context. + pub async fn get_credential(&self, access_key_id: &str) -> Result { + match &self.mode { + IamClientMode::Env { credentials } => { + let secret_key = credentials + .get(access_key_id) + .cloned() + .ok_or_else(|| "Access key ID not found".to_string())?; + let (org_id, project_id) = Self::env_default_tenant(); + Ok(ResolvedCredential { + secret_key, + principal_id: access_key_id.to_string(), + org_id, + project_id, + }) + } + IamClientMode::Grpc { endpoint, channel } => { + if let Some(credential) = self.cached_credential(access_key_id).await { + return Ok(credential); + } + + let response = self + .grpc_get_secret_key(endpoint, channel, access_key_id) + .await?; + let response = response.into_inner(); + let credential = ResolvedCredential { + secret_key: response.secret_key, + principal_id: response.principal_id, + org_id: response.org_id, + project_id: response.project_id, + }; + self.cache_credential(access_key_id, &credential).await; + Ok(credential) + } + } + } + + async fn cached_credential(&self, access_key_id: &str) -> Option { + let cache = self.credential_cache.read().await; + cache.get(access_key_id).and_then(|entry| { + if entry.cached_at.elapsed() <= self.cache_ttl { + Some(entry.credential.clone()) + } else { + None + } + }) + } + + async fn cache_credential(&self, access_key_id: &str, credential: &ResolvedCredential) { + let mut cache = self.credential_cache.write().await; + cache.insert( + access_key_id.to_string(), + CachedCredential { + credential: credential.clone(), + cached_at: Instant::now(), + }, + ); + } + + async fn grpc_channel( + endpoint: &str, + channel: &Arc>>, + ) -> Result { + let mut cached = channel.lock().await; + if let Some(existing) = cached.as_ref() { + return Ok(existing.clone()); + } + + let created = Channel::from_shared(endpoint.to_string()) + .map_err(|e| format!("failed to parse IAM credential endpoint: {}", e))? + .connect() + .await + .map_err(|e| format!("failed to connect to IAM credential service: {}", e))?; + *cached = Some(created.clone()); + Ok(created) + } + + async fn invalidate_grpc_channel(channel: &Arc>>) { + let mut cached = channel.lock().await; + *cached = None; + } + + async fn grpc_get_secret_key( + &self, + endpoint: &str, + channel: &Arc>>, + access_key_id: &str, + ) -> Result, String> { + for attempt in 0..2 { + let grpc_channel = Self::grpc_channel(endpoint, channel).await?; + let mut client = IamCredentialClient::new(grpc_channel); + match client + .get_secret_key(GetSecretKeyRequest { + access_key_id: access_key_id.to_string(), + }) + .await + { + Ok(response) => return Ok(response), + Err(status) + if attempt == 0 + && matches!( + status.code(), + tonic::Code::Unavailable + | tonic::Code::Cancelled + | tonic::Code::Unknown + | tonic::Code::DeadlineExceeded + | tonic::Code::Internal + ) => + { + Self::invalidate_grpc_channel(channel).await; + } + Err(status) => return Err(status.message().to_string()), + } + } + + Err("IAM credential lookup exhausted retries".to_string()) + } +} + +fn normalize_iam_endpoint(endpoint: &str) -> String { + if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + endpoint.to_string() + } else { + format!("http://{}", endpoint) } } impl AuthState { /// Create new auth state with IAM integration pub fn new(iam_endpoint: Option) -> Self { - let iam_client = if let Some(_endpoint) = iam_endpoint { - // TODO: Connect to real IAM gRPC service - // For now, if an endpoint is provided, we still use our env var based client - Some(Arc::new(RwLock::new(IamClient::new()))) - } else { - Some(Arc::new(RwLock::new(IamClient::new()))) - }; + let iam_client = Some(Arc::new(RwLock::new(IamClient::new(iam_endpoint)))); Self { iam_client, @@ -198,9 +393,9 @@ pub async fn sigv4_auth_middleware( }; // Get secret key from IAM (or use dummy for MVP) - let secret_key = if let Some(ref iam) = auth_state.iam_client { - match iam.read().await.get_secret_key(&access_key_id).await { - Ok(key) => key, + let credential = if let Some(ref iam) = auth_state.iam_client { + match iam.read().await.get_credential(&access_key_id).await { + Ok(credential) => credential, Err(e) => { warn!("IAM credential validation failed: {}", e); return error_response( @@ -211,18 +406,22 @@ pub async fn sigv4_auth_middleware( } } } else { - // This case should ideally not be hit with the current IamClient::new() logic - // but kept for safety. debug!("No IAM integration, using dummy secret key if IamClient wasn't initialized."); - "dummy_secret_key_for_mvp".to_string() + ResolvedCredential { + secret_key: "dummy_secret_key_for_mvp".to_string(), + principal_id: access_key_id.clone(), + org_id: Some("default".to_string()), + project_id: Some("default".to_string()), + } }; + let secret_key = credential.secret_key.as_str(); let payload_hash_header = headers .get("x-amz-content-sha256") .and_then(|value| value.to_str().ok()) .filter(|value| !value.is_empty()) .map(str::to_string); - let should_buffer_body = !matches!(payload_hash_header.as_deref(), Some(hash) if hash != "UNSIGNED-PAYLOAD"); + let should_buffer_body = should_buffer_auth_body(payload_hash_header.as_deref()); let body_bytes = if should_buffer_body { let max_body_bytes = std::env::var("S3_MAX_AUTH_BODY_BYTES") @@ -282,7 +481,7 @@ pub async fn sigv4_auth_middleware( ); let expected_signature = match compute_sigv4_signature( - &secret_key, + secret_key, &method, &uri, &headers, @@ -310,6 +509,21 @@ pub async fn sigv4_auth_middleware( ); } + match (credential.org_id, credential.project_id) { + (Some(org_id), Some(project_id)) => { + request + .extensions_mut() + .insert(VerifiedTenantContext(TenantContext { org_id, project_id })); + } + _ => { + return error_response( + StatusCode::FORBIDDEN, + "AccessDenied", + "S3 credential is missing tenant scope", + ); + } + } + // Auth successful debug!("SigV4 auth successful for access_key={}", access_key_id); next.run(request).await @@ -558,6 +772,97 @@ fn error_response(status: StatusCode, code: &str, message: &str) -> Response { mod tests { use super::*; use axum::http::HeaderValue; + use iam_api::proto::{ + iam_credential_server::{IamCredential, IamCredentialServer}, + CreateS3CredentialRequest, CreateS3CredentialResponse, Credential, GetSecretKeyResponse, + ListCredentialsRequest, ListCredentialsResponse, RevokeCredentialRequest, + RevokeCredentialResponse, + }; + use std::collections::HashMap; + use std::net::SocketAddr; + use std::sync::{atomic::{AtomicUsize, Ordering}, Mutex}; + use tokio::net::TcpListener; + use tokio::time::{sleep, Duration}; + use tonic::{Request as TonicRequest, Response as TonicResponse, Status}; + use tonic::transport::Server; + + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + #[derive(Clone, Default)] + struct MockIamCredentialService { + secrets: Arc>, + get_secret_calls: Arc, + } + + #[tonic::async_trait] + impl IamCredential for MockIamCredentialService { + async fn create_s3_credential( + &self, + _request: TonicRequest, + ) -> Result, Status> { + Err(Status::unimplemented("not needed in test")) + } + + async fn get_secret_key( + &self, + request: TonicRequest, + ) -> Result, Status> { + let access_key_id = request.into_inner().access_key_id; + self.get_secret_calls.fetch_add(1, Ordering::SeqCst); + let Some(secret_key) = self.secrets.get(&access_key_id) else { + return Err(Status::not_found("access key not found")); + }; + Ok(TonicResponse::new(GetSecretKeyResponse { + secret_key: secret_key.clone(), + principal_id: "test-principal".to_string(), + expires_at: None, + org_id: Some("test-org".to_string()), + project_id: Some("test-project".to_string()), + principal_kind: iam_api::proto::PrincipalKind::ServiceAccount as i32, + })) + } + + async fn list_credentials( + &self, + _request: TonicRequest, + ) -> Result, Status> { + Ok(TonicResponse::new(ListCredentialsResponse { + credentials: Vec::::new(), + })) + } + + async fn revoke_credential( + &self, + _request: TonicRequest, + ) -> Result, Status> { + Ok(TonicResponse::new(RevokeCredentialResponse { success: true })) + } + } + + async fn start_mock_iam(secrets: HashMap) -> (SocketAddr, Arc) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let get_secret_calls = Arc::new(AtomicUsize::new(0)); + let service = MockIamCredentialService { + secrets: Arc::new(secrets), + get_secret_calls: get_secret_calls.clone(), + }; + drop(listener); + tokio::spawn(async move { + Server::builder() + .add_service(IamCredentialServer::new(service)) + .serve(addr) + .await + .unwrap(); + }); + for _ in 0..20 { + if tokio::net::TcpStream::connect(addr).await.is_ok() { + return (addr, get_secret_calls); + } + sleep(Duration::from_millis(25)).await; + } + panic!("mock IAM server did not start on {}", addr); + } #[tokio::test] async fn test_parse_auth_header() { @@ -657,6 +962,13 @@ mod tests { assert_eq!(hashed_payload, "signed-payload-hash"); } + #[test] + fn test_should_buffer_auth_body_only_when_hash_header_missing() { + assert!(should_buffer_auth_body(None)); + assert!(!should_buffer_auth_body(Some("signed-payload-hash"))); + assert!(!should_buffer_auth_body(Some("UNSIGNED-PAYLOAD"))); + } + #[test] fn test_build_string_to_sign() { let amz_date = "20231201T000000Z"; @@ -677,34 +989,77 @@ mod tests { #[test] fn test_iam_client_multi_credentials() { + let _guard = ENV_LOCK.lock().unwrap(); // Test parsing S3_CREDENTIALS format std::env::set_var("S3_CREDENTIALS", "key1:secret1,key2:secret2,key3:secret3"); - let client = IamClient::new(); + let client = IamClient::new(None); + let credentials = client.env_credentials().unwrap(); - assert_eq!(client.credentials.len(), 3); - assert_eq!(client.credentials.get("key1"), Some(&"secret1".to_string())); - assert_eq!(client.credentials.get("key2"), Some(&"secret2".to_string())); - assert_eq!(client.credentials.get("key3"), Some(&"secret3".to_string())); + assert_eq!(credentials.len(), 3); + assert_eq!(credentials.get("key1"), Some(&"secret1".to_string())); + assert_eq!(credentials.get("key2"), Some(&"secret2".to_string())); + assert_eq!(credentials.get("key3"), Some(&"secret3".to_string())); std::env::remove_var("S3_CREDENTIALS"); } #[test] fn test_iam_client_single_credentials() { + let _guard = ENV_LOCK.lock().unwrap(); // Test legacy S3_ACCESS_KEY_ID/S3_SECRET_KEY format std::env::remove_var("S3_CREDENTIALS"); std::env::set_var("S3_ACCESS_KEY_ID", "test_key"); std::env::set_var("S3_SECRET_KEY", "test_secret"); - let client = IamClient::new(); + let client = IamClient::new(None); + let credentials = client.env_credentials().unwrap(); - assert_eq!(client.credentials.len(), 1); - assert_eq!(client.credentials.get("test_key"), Some(&"test_secret".to_string())); + assert_eq!(credentials.len(), 1); + assert_eq!(credentials.get("test_key"), Some(&"test_secret".to_string())); std::env::remove_var("S3_ACCESS_KEY_ID"); std::env::remove_var("S3_SECRET_KEY"); } + #[tokio::test] + async fn test_iam_client_grpc_lookup() { + let (addr, _calls) = start_mock_iam(HashMap::from([( + "grpc_key".to_string(), + "grpc_secret".to_string(), + )])) + .await; + let client = IamClient::new(Some(addr.to_string())); + + let credential = client.get_credential("grpc_key").await.unwrap(); + assert_eq!(credential.secret_key, "grpc_secret"); + assert_eq!(credential.org_id.as_deref(), Some("test-org")); + assert_eq!(credential.project_id.as_deref(), Some("test-project")); + assert_eq!( + client.get_credential("missing").await.unwrap_err(), + "access key not found" + ); + } + + #[tokio::test] + async fn test_iam_client_grpc_cache_reuses_secret() { + let (addr, calls) = start_mock_iam(HashMap::from([( + "grpc_key".to_string(), + "grpc_secret".to_string(), + )])) + .await; + let client = IamClient::new(Some(addr.to_string())); + + assert_eq!( + client.get_credential("grpc_key").await.unwrap().secret_key, + "grpc_secret" + ); + assert_eq!( + client.get_credential("grpc_key").await.unwrap().secret_key, + "grpc_secret" + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); + } + #[test] fn test_complete_sigv4_signature() { // Test with AWS example credentials (from AWS docs) @@ -1039,18 +1394,20 @@ mod tests { #[test] fn test_security_credential_lookup_unknown_key() { + let _guard = ENV_LOCK.lock().unwrap(); // Test that unknown access keys return the correct result std::env::remove_var("S3_CREDENTIALS"); std::env::set_var("S3_ACCESS_KEY_ID", "known_key"); std::env::set_var("S3_SECRET_KEY", "known_secret"); - let client = IamClient::new(); + let client = IamClient::new(None); + let credentials = client.env_credentials().unwrap(); // Known key should be found in credentials map - assert_eq!(client.credentials.get("known_key"), Some(&"known_secret".to_string())); + assert_eq!(credentials.get("known_key"), Some(&"known_secret".to_string())); // Unknown key should not be found - assert_eq!(client.credentials.get("unknown_key"), None); + assert_eq!(credentials.get("unknown_key"), None); std::env::remove_var("S3_ACCESS_KEY_ID"); std::env::remove_var("S3_SECRET_KEY"); @@ -1058,33 +1415,36 @@ mod tests { #[test] fn test_security_empty_credentials() { + let _guard = ENV_LOCK.lock().unwrap(); // Test that IamClient keeps credentials empty when none provided std::env::remove_var("S3_CREDENTIALS"); std::env::remove_var("S3_ACCESS_KEY_ID"); std::env::remove_var("S3_SECRET_KEY"); - let client = IamClient::new(); + let client = IamClient::new(None); // No credentials configured - assert!(client.credentials.is_empty()); + assert!(client.env_credentials().unwrap().is_empty()); } #[test] fn test_security_malformed_s3_credentials_env() { + let _guard = ENV_LOCK.lock().unwrap(); // Test that malformed S3_CREDENTIALS are handled gracefully // Missing colon separator std::env::set_var("S3_CREDENTIALS", "key1_secret1,key2:secret2"); - let client = IamClient::new(); + let client = IamClient::new(None); + let credentials = client.env_credentials().unwrap(); // Should only parse the valid pair (key2:secret2) - assert_eq!(client.credentials.len(), 1); - assert!(client.credentials.contains_key("key2")); + assert_eq!(credentials.len(), 1); + assert!(credentials.contains_key("key2")); // Empty pairs std::env::set_var("S3_CREDENTIALS", "key1:secret1,,key2:secret2"); - let client2 = IamClient::new(); + let client2 = IamClient::new(None); // Should parse both valid pairs, skip empty - assert_eq!(client2.credentials.len(), 2); + assert_eq!(client2.env_credentials().unwrap().len(), 2); std::env::remove_var("S3_CREDENTIALS"); } diff --git a/lightningstor/crates/lightningstor-server/src/s3/mod.rs b/lightningstor/crates/lightningstor-server/src/s3/mod.rs index 037f1c7..4018331 100644 --- a/lightningstor/crates/lightningstor-server/src/s3/mod.rs +++ b/lightningstor/crates/lightningstor-server/src/s3/mod.rs @@ -7,4 +7,4 @@ mod router; mod xml; pub use auth::{AuthState, sigv4_auth_middleware}; -pub use router::{create_router, create_router_with_state}; +pub use router::{create_router, create_router_with_auth, create_router_with_state}; diff --git a/lightningstor/crates/lightningstor-server/src/s3/router.rs b/lightningstor/crates/lightningstor-server/src/s3/router.rs index 226bd95..5001d5f 100644 --- a/lightningstor/crates/lightningstor-server/src/s3/router.rs +++ b/lightningstor/crates/lightningstor-server/src/s3/router.rs @@ -2,24 +2,36 @@ use axum::{ body::{Body, Bytes}, - extract::{State, Request}, - http::{HeaderMap, StatusCode, Method}, + extract::{Request, State}, + http::{HeaderMap, Method, StatusCode}, middleware, response::{IntoResponse, Response}, Router, }; +use bytes::BytesMut; +use chrono::Utc; +use futures::{stream, stream::FuturesUnordered, StreamExt}; use http_body_util::BodyExt; use md5::{Digest, Md5}; use serde::Deserialize; +use std::io; use sha2::Sha256; use std::sync::Arc; +use tokio::task::JoinHandle; use crate::metadata::MetadataStore; +use crate::tenant::TenantContext; use lightningstor_storage::StorageBackend; -use lightningstor_types::{Bucket, BucketName, Object, ObjectKey, ObjectMetadata, ObjectVersion}; +use lightningstor_types::{ + Bucket, BucketName, MultipartUpload, Object, ObjectKey, ObjectMetadata, ObjectVersion, Part, + PartNumber, +}; -use super::auth::{AuthState, VerifiedBodyBytes, VerifiedPayloadHash}; -use super::xml::{BucketEntry, ErrorResponse, ListAllMyBucketsResult, ListBucketResult, ListBucketResultV2, ObjectEntry}; +use super::auth::{AuthState, VerifiedBodyBytes, VerifiedPayloadHash, VerifiedTenantContext}; +use super::xml::{ + BucketEntry, ErrorResponse, ListAllMyBucketsResult, ListBucketResult, ListBucketResultV2, + ObjectEntry, +}; /// S3 API state #[derive(Clone)] @@ -28,6 +40,12 @@ pub struct S3State { pub metadata: Arc, } +// Keep streamed single-PUT parts aligned with the distributed backend's +// large-object chunking so GET does not degrade into many small serial reads. +const DEFAULT_STREAMING_PUT_THRESHOLD_BYTES: usize = 16 * 1024 * 1024; +const DEFAULT_INLINE_PUT_MAX_BYTES: usize = 128 * 1024 * 1024; +const DEFAULT_MULTIPART_PUT_CONCURRENCY: usize = 4; + impl S3State { pub fn new(storage: Arc, metadata: Arc) -> Self { Self { storage, metadata } @@ -39,7 +57,7 @@ pub fn create_router_with_state( storage: Arc, metadata: Arc, ) -> Router { - create_router_with_auth(storage, metadata, None) + create_router_with_auth_state(storage, metadata, Arc::new(AuthState::new(None))) } /// Create the S3-compatible HTTP router with auth and storage backends @@ -47,9 +65,16 @@ pub fn create_router_with_auth( storage: Arc, metadata: Arc, iam_endpoint: Option, +) -> Router { + create_router_with_auth_state(storage, metadata, Arc::new(AuthState::new(iam_endpoint))) +} + +fn create_router_with_auth_state( + storage: Arc, + metadata: Arc, + auth_state: Arc, ) -> Router { let state = Arc::new(S3State::new(storage, metadata)); - let auth_state = Arc::new(AuthState::new(iam_endpoint)); Router::new() // Catch-all route for ALL operations (including root /) @@ -57,9 +82,7 @@ pub fn create_router_with_auth( .fallback(dispatch_global) .layer(middleware::from_fn(move |request, next| { let auth_state = auth_state.clone(); - async move { - super::auth::sigv4_auth_middleware(auth_state, request, next).await - } + async move { super::auth::sigv4_auth_middleware(auth_state, request, next).await } })) .with_state(state) } @@ -67,10 +90,13 @@ pub fn create_router_with_auth( /// Create a router without state (for backwards compatibility, returns stub responses) pub fn create_router() -> Router { // Create a minimal router that returns NotImplemented for all operations - Router::new() - .fallback(|| async { - error_response(StatusCode::SERVICE_UNAVAILABLE, "ServiceUnavailable", "Storage not configured") - }) + Router::new().fallback(|| async { + error_response( + StatusCode::SERVICE_UNAVAILABLE, + "ServiceUnavailable", + "Storage not configured", + ) + }) } /// Query parameters for ListObjects @@ -81,6 +107,7 @@ pub struct ListObjectsQuery { delimiter: Option, #[serde(rename = "max-keys")] max_keys: Option, + marker: Option, #[serde(rename = "start-after")] start_after: Option, #[serde(rename = "continuation-token")] @@ -89,22 +116,66 @@ pub struct ListObjectsQuery { list_type: Option, } +fn request_tenant(extensions: &axum::http::Extensions) -> TenantContext { + extensions + .get::() + .map(|tenant| tenant.0.clone()) + .unwrap_or_else(|| TenantContext { + org_id: "default".to_string(), + project_id: "default".to_string(), + }) +} + +fn streaming_put_threshold_bytes() -> usize { + std::env::var("LIGHTNINGSTOR_S3_STREAMING_PUT_THRESHOLD_BYTES") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_STREAMING_PUT_THRESHOLD_BYTES) +} + +fn inline_put_max_bytes() -> usize { + std::env::var("LIGHTNINGSTOR_S3_INLINE_PUT_MAX_BYTES") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_INLINE_PUT_MAX_BYTES) +} + +fn multipart_put_concurrency() -> usize { + std::env::var("LIGHTNINGSTOR_S3_MULTIPART_PUT_CONCURRENCY") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(DEFAULT_MULTIPART_PUT_CONCURRENCY) +} + +fn request_content_length(headers: &HeaderMap) -> Option { + headers + .get("content-length") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()) +} + // ============================================================================= // Global Dispatcher // ============================================================================= -async fn dispatch_global( - State(state): State>, - request: Request, -) -> Response { +async fn dispatch_global(State(state): State>, request: Request) -> Response { let full_path = request.uri().path().trim_start_matches('/').to_string(); - + // Check if path is effectively root (empty) if full_path.is_empty() { if request.method() == Method::GET { - return list_buckets(State(state)).await.into_response(); + let tenant = request_tenant(request.extensions()); + return list_buckets(state, tenant).await.into_response(); } else { - return error_response(StatusCode::METHOD_NOT_ALLOWED, "MethodNotAllowed", "Method not allowed on root").into_response(); + return error_response( + StatusCode::METHOD_NOT_ALLOWED, + "MethodNotAllowed", + "Method not allowed on root", + ) + .into_response(); } } @@ -128,39 +199,61 @@ async fn dispatch_global( .extensions .get::() .map(|payload| payload.0.clone()); + let tenant = request_tenant(&parts.extensions); // Dispatch based on method and key presence if method == Method::PUT { if key.is_empty() { - create_bucket(state, bucket).await.into_response() + create_bucket(state, tenant, bucket).await.into_response() } else { - put_object(state, bucket, key, headers, body, verified_body, verified_payload_hash) - .await - .into_response() + put_object( + state, + tenant, + bucket, + key, + headers, + body, + verified_body, + verified_payload_hash, + ) + .await + .into_response() } } else if method == Method::GET { if key.is_empty() { // Parse query params let query_str = uri.query().unwrap_or(""); - let params: ListObjectsQuery = serde_urlencoded::from_str(query_str).unwrap_or_default(); - list_objects(state, bucket, params).await.into_response() + let params: ListObjectsQuery = + serde_urlencoded::from_str(query_str).unwrap_or_default(); + list_objects(state, tenant, bucket, params) + .await + .into_response() } else { - get_object(state, bucket, key).await.into_response() + get_object(state, tenant, bucket, key).await.into_response() } } else if method == Method::DELETE { if key.is_empty() { - delete_bucket(state, bucket).await.into_response() + delete_bucket(state, tenant, bucket).await.into_response() } else { - delete_object(state, bucket, key).await.into_response() + delete_object(state, tenant, bucket, key) + .await + .into_response() } } else if method == Method::HEAD { if key.is_empty() { - head_bucket(state, bucket).await.into_response() + head_bucket(state, tenant, bucket).await.into_response() } else { - head_object(state, bucket, key).await.into_response() + head_object(state, tenant, bucket, key) + .await + .into_response() } } else { - error_response(StatusCode::METHOD_NOT_ALLOWED, "MethodNotAllowed", "Method not allowed").into_response() + error_response( + StatusCode::METHOD_NOT_ALLOWED, + "MethodNotAllowed", + "Method not allowed", + ) + .into_response() } } @@ -168,10 +261,12 @@ async fn dispatch_global( // Service Operations // ============================================================================= -async fn list_buckets(State(state): State>) -> impl IntoResponse { - let org_id = "default"; - - match state.metadata.list_buckets(org_id, None).await { +async fn list_buckets(state: Arc, tenant: TenantContext) -> impl IntoResponse { + match state + .metadata + .list_buckets(&tenant.org_id, Some(&tenant.project_id)) + .await + { Ok(buckets) => { let bucket_entries: Vec = buckets .iter() @@ -183,10 +278,12 @@ async fn list_buckets(State(state): State>) -> impl IntoResponse { let result = ListAllMyBucketsResult { owner: super::xml::Owner { - id: org_id.to_string(), - display_name: org_id.to_string(), + id: tenant.org_id.clone(), + display_name: tenant.project_id.clone(), + }, + buckets: super::xml::Buckets { + bucket: bucket_entries, }, - buckets: super::xml::Buckets { bucket: bucket_entries }, }; match super::xml::to_xml(&result) { @@ -195,10 +292,18 @@ async fn list_buckets(State(state): State>) -> impl IntoResponse { .header("Content-Type", "application/xml") .body(Body::from(xml)) .unwrap(), - Err(_) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", "Failed to serialize response"), + Err(_) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + "Failed to serialize response", + ), } } - Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Err(e) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ), } } @@ -208,12 +313,10 @@ async fn list_buckets(State(state): State>) -> impl IntoResponse { async fn create_bucket( state: Arc, + tenant: TenantContext, bucket: String, ) -> impl IntoResponse { tracing::info!(bucket = %bucket, "CreateBucket request"); - - let org_id = "default"; - let project_id = "default"; let region = "default"; // Validate bucket name @@ -223,18 +326,30 @@ async fn create_bucket( }; // Check if bucket already exists - match state.metadata.load_bucket(org_id, project_id, &bucket).await { + match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(_)) => { - return error_response(StatusCode::CONFLICT, "BucketAlreadyExists", "Bucket already exists"); + return error_response( + StatusCode::CONFLICT, + "BucketAlreadyExists", + "Bucket already exists", + ); } Ok(None) => {} // Bucket does not exist, proceed Err(e) => { - return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()); + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ); } } // Create bucket - let new_bucket = Bucket::new(bucket_name, org_id, project_id, region); + let new_bucket = Bucket::new(bucket_name, &tenant.org_id, &tenant.project_id, region); match state.metadata.save_bucket(&new_bucket).await { Ok(_) => { @@ -245,24 +360,42 @@ async fn create_bucket( .body(Body::empty()) .unwrap() } - Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Err(e) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ), } } async fn delete_bucket( state: Arc, + tenant: TenantContext, bucket: String, ) -> impl IntoResponse { tracing::info!(bucket = %bucket, "DeleteBucket request"); - let org_id = "default"; - let project_id = "default"; - // Load bucket - let bucket_obj = match state.metadata.load_bucket(org_id, project_id, &bucket).await { + let bucket_obj = match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(b)) => b, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; // Ensure bucket is empty before deleting to avoid data loss @@ -293,29 +426,41 @@ async fn delete_bucket( .body(Body::empty()) .unwrap() } - Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Err(e) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ), } } async fn head_bucket( state: Arc, + tenant: TenantContext, bucket: String, ) -> impl IntoResponse { tracing::info!(bucket = %bucket, "HeadBucket request"); - let org_id = "default"; - let project_id = "default"; - - match state.metadata.load_bucket(org_id, project_id, &bucket).await { - Ok(Some(b)) => { - Response::builder() - .status(StatusCode::OK) - .header("x-amz-bucket-region", &b.region) - .body(Body::empty()) - .unwrap() - } - Ok(None) => error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { + Ok(Some(b)) => Response::builder() + .status(StatusCode::OK) + .header("x-amz-bucket-region", &b.region) + .body(Body::empty()) + .unwrap(), + Ok(None) => error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ), + Err(e) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ), } } @@ -375,28 +520,55 @@ fn extract_user_metadata(headers: &HeaderMap) -> std::collections::HashMap, + tenant: TenantContext, bucket: String, params: ListObjectsQuery, ) -> impl IntoResponse { tracing::info!(bucket = %bucket, ?params, "ListObjects request"); - let org_id = "default"; - let project_id = "default"; - // Load bucket to verify it exists - let bucket_obj = match state.metadata.load_bucket(org_id, project_id, &bucket).await { + let bucket_obj = match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(b)) => b, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; let prefix = params.prefix.unwrap_or_default(); let max_keys = params.max_keys.unwrap_or(1000); let delimiter = params.delimiter.as_deref(); + let is_v2 = params.list_type.as_deref() == Some("2"); + let page_marker = if is_v2 { + params + .continuation_token + .clone() + .or_else(|| params.start_after.clone()) + } else { + params.marker.clone().or_else(|| params.start_after.clone()) + }; // List objects - match state.metadata.list_objects(&bucket_obj.id, &prefix, max_keys).await { - Ok(objects) => { + match state + .metadata + .list_objects_page(&bucket_obj.id, &prefix, page_marker.as_deref(), max_keys) + .await + { + Ok((objects, has_more)) => { let contents: Vec = objects .iter() .filter(|o| !o.is_delete_marker) @@ -417,19 +589,15 @@ async fn list_objects( }; let key_count = (filtered_contents.len() + common_prefixes.len()) as u32; - let is_truncated = contents.len() >= max_keys as usize; - - // Check if this is ListObjectsV2 (list-type=2) - let is_v2 = params.list_type.as_deref() == Some("2"); + let is_truncated = has_more; + let next_token = if is_truncated { + objects.last().map(|object| object.key.as_str().to_string()) + } else { + None + }; let xml = if is_v2 { // ListObjectsV2 response - let next_token = if is_truncated { - contents.last().map(|o| o.key.clone()) - } else { - None - }; - let result = ListBucketResultV2 { name: bucket, prefix: prefix.clone(), @@ -450,9 +618,11 @@ async fn list_objects( let result = ListBucketResult { name: bucket, prefix, + marker: params.marker, delimiter: params.delimiter, max_keys, is_truncated, + next_marker: next_token, contents: filtered_contents, common_prefixes, }; @@ -466,10 +636,18 @@ async fn list_objects( .header("Content-Type", "application/xml") .body(Body::from(xml)) .unwrap(), - Err(_) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", "Failed to serialize response"), + Err(_) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + "Failed to serialize response", + ), } } - Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Err(e) => error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ), } } @@ -479,6 +657,7 @@ async fn list_objects( async fn put_object( state: Arc, + tenant: TenantContext, bucket: String, key: String, headers: HeaderMap, @@ -488,14 +667,27 @@ async fn put_object( ) -> impl IntoResponse { tracing::debug!(bucket = %bucket, key = %key, "PutObject request"); - let org_id = "default"; - let project_id = "default"; - // Load bucket - let bucket_obj = match state.metadata.load_bucket(org_id, project_id, &bucket).await { + let bucket_obj = match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(b)) => b, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; // Validate object key @@ -504,34 +696,6 @@ async fn put_object( Err(e) => return error_response(StatusCode::BAD_REQUEST, "InvalidArgument", e), }; - // Read body - let body_bytes = match verified_body { - Some(body_bytes) => body_bytes, - None => match body.collect().await { - Ok(collected) => collected.to_bytes(), - Err(e) => { - return error_response(StatusCode::BAD_REQUEST, "InvalidRequest", &e.to_string()) - } - }, - }; - - let body_len = body_bytes.len() as u64; - let (actual_payload_hash, etag) = match verified_payload_hash.as_deref() { - Some(expected_payload_hash) if expected_payload_hash != "UNSIGNED-PAYLOAD" => { - (expected_payload_hash.to_string(), calculate_etag(&body_bytes)) - } - _ => calculate_payload_hashes(&body_bytes), - }; - if let Some(expected_payload_hash) = verified_payload_hash { - if expected_payload_hash != "UNSIGNED-PAYLOAD" && actual_payload_hash != expected_payload_hash { - return error_response( - StatusCode::FORBIDDEN, - "SignatureDoesNotMatch", - "x-amz-content-sha256 does not match the request body", - ); - } - } - // Extract content type from headers let content_type = headers .get("content-type") @@ -542,13 +706,85 @@ async fn put_object( let user_metadata = extract_user_metadata(&headers); let metadata = ObjectMetadata { content_type: content_type.clone(), - content_encoding: headers.get("content-encoding").and_then(|v| v.to_str().ok()).map(|s| s.to_string()), - content_disposition: headers.get("content-disposition").and_then(|v| v.to_str().ok()).map(|s| s.to_string()), - content_language: headers.get("content-language").and_then(|v| v.to_str().ok()).map(|s| s.to_string()), - cache_control: headers.get("cache-control").and_then(|v| v.to_str().ok()).map(|s| s.to_string()), + content_encoding: headers + .get("content-encoding") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()), + content_disposition: headers + .get("content-disposition") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()), + content_language: headers + .get("content-language") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()), + cache_control: headers + .get("cache-control") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()), user_metadata, }; + let content_length = request_content_length(&headers); + let (body_len, etag, multipart_upload, inline_body) = match verified_body { + Some(body_bytes) => { + let body_len = body_bytes.len() as u64; + let (actual_payload_hash, etag) = match verified_payload_hash.as_deref() { + Some(expected_payload_hash) if expected_payload_hash != "UNSIGNED-PAYLOAD" => ( + expected_payload_hash.to_string(), + calculate_etag(&body_bytes), + ), + _ => calculate_payload_hashes(&body_bytes), + }; + if let Some(expected_payload_hash) = verified_payload_hash { + if expected_payload_hash != "UNSIGNED-PAYLOAD" + && actual_payload_hash != expected_payload_hash + { + return error_response( + StatusCode::FORBIDDEN, + "SignatureDoesNotMatch", + "x-amz-content-sha256 does not match the request body", + ); + } + } + + (body_len, etag, None, Some(body_bytes)) + } + None => { + let prepared = if let Some(content_length) = + content_length.filter(|content_length| *content_length <= inline_put_max_bytes()) + { + read_inline_put_body( + body, + verified_payload_hash.as_deref(), + inline_put_max_bytes(), + Some(content_length), + ) + .await + } else { + stream_put_body( + &state, + &bucket_obj.id.to_string(), + object_key.clone(), + metadata.clone(), + body, + verified_payload_hash.as_deref(), + ) + .await + }; + + match prepared { + Ok(prepared) => ( + prepared.body_len, + prepared.etag, + prepared.multipart_upload, + prepared.inline_body, + ), + Err(response) => return response, + } + } + }; + // Create object let mut object = Object::new( bucket_obj.id.to_string(), @@ -564,14 +800,49 @@ async fn put_object( object.version = ObjectVersion::new(); } - // Save object data to storage backend - if let Err(e) = state.storage.put_object(&object.id, body_bytes).await { - return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &format!("Failed to store object: {}", e)); + let has_inline_body = inline_body.is_some(); + if let Some(body_bytes) = inline_body { + if let Err(e) = state.storage.put_object(&object.id, body_bytes).await { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &format!("Failed to store object: {}", e), + ); + } + } + + if let Some(upload) = &multipart_upload { + if let Err(e) = state + .metadata + .save_object_multipart_upload(&object.id, upload) + .await + { + let _ = delete_multipart_parts(&state, upload).await; + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &format!("Failed to save multipart manifest: {}", e), + ); + } } // Save object metadata if let Err(e) = state.metadata.save_object(&object).await { - return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()); + if has_inline_body { + let _ = state.storage.delete_object(&object.id).await; + } + if let Some(upload) = &multipart_upload { + let _ = state + .metadata + .delete_object_multipart_upload(&object.id) + .await; + let _ = delete_multipart_parts(&state, upload).await; + } + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ); } tracing::debug!(bucket = %bucket, key = %key, etag = %etag.as_str(), "Object stored successfully"); @@ -592,6 +863,452 @@ fn calculate_payload_hashes(body: &[u8]) -> (String, lightningstor_types::ETag) (sha256_hex, calculate_etag(body)) } +struct PreparedPutBody { + body_len: u64, + etag: lightningstor_types::ETag, + inline_body: Option, + multipart_upload: Option, +} + +async fn read_inline_put_body( + mut body: Body, + expected_payload_hash: Option<&str>, + max_bytes: usize, + expected_len: Option, +) -> Result> { + let verify_payload_hash = expected_payload_hash + .filter(|expected_payload_hash| *expected_payload_hash != "UNSIGNED-PAYLOAD"); + let initial_capacity = expected_len.unwrap_or(0).min(max_bytes); + let mut buffered = BytesMut::with_capacity(initial_capacity); + let mut full_md5 = Md5::new(); + let mut full_sha256 = verify_payload_hash.map(|_| Sha256::new()); + let mut body_len = 0usize; + + while let Some(frame) = body.frame().await { + let frame = match frame { + Ok(frame) => frame, + Err(e) => { + return Err(error_response( + StatusCode::BAD_REQUEST, + "InvalidRequest", + &e.to_string(), + )); + } + }; + let Ok(data) = frame.into_data() else { + continue; + }; + if data.is_empty() { + continue; + } + body_len = body_len.saturating_add(data.len()); + if body_len > max_bytes { + return Err(error_response( + StatusCode::PAYLOAD_TOO_LARGE, + "EntityTooLarge", + "request body exceeded the inline upload limit", + )); + } + full_md5.update(&data); + if let Some(full_sha256) = full_sha256.as_mut() { + full_sha256.update(&data); + } + buffered.extend_from_slice(&data); + } + + if let (Some(expected_payload_hash), Some(full_sha256)) = (verify_payload_hash, full_sha256) { + let actual_payload_hash = hex::encode(full_sha256.finalize()); + if expected_payload_hash != actual_payload_hash { + return Err(error_response( + StatusCode::FORBIDDEN, + "SignatureDoesNotMatch", + "x-amz-content-sha256 does not match the request body", + )); + } + } + + let md5_hash: [u8; 16] = full_md5.finalize().into(); + Ok(PreparedPutBody { + body_len: body_len as u64, + etag: lightningstor_types::ETag::from_md5(&md5_hash), + inline_body: Some(buffered.freeze()), + multipart_upload: None, + }) +} + +type StreamingPartUploadHandle = JoinHandle>>; + +async fn stream_put_body( + state: &Arc, + bucket_id: &str, + object_key: ObjectKey, + metadata: ObjectMetadata, + mut body: Body, + expected_payload_hash: Option<&str>, +) -> Result> { + let verify_payload_hash = expected_payload_hash + .filter(|expected_payload_hash| *expected_payload_hash != "UNSIGNED-PAYLOAD"); + let threshold = streaming_put_threshold_bytes(); + let mut buffered = BytesMut::with_capacity(threshold); + let mut full_md5 = Some(Md5::new()); + let mut full_sha256 = verify_payload_hash.map(|_| Sha256::new()); + let mut body_len = 0u64; + let mut next_part_number = 1u32; + let mut multipart_upload: Option = None; + let mut scheduled_part_numbers = Vec::new(); + let mut completed_parts = Vec::new(); + let mut in_flight_uploads = FuturesUnordered::new(); + let max_in_flight_uploads = multipart_put_concurrency(); + + while let Some(frame) = body.frame().await { + let frame = match frame { + Ok(frame) => frame, + Err(e) => { + if let Some(upload) = &multipart_upload { + cleanup_incomplete_streaming_upload( + state, + upload.upload_id.as_str(), + &scheduled_part_numbers, + &mut in_flight_uploads, + ) + .await; + } + return Err(error_response( + StatusCode::BAD_REQUEST, + "InvalidRequest", + &e.to_string(), + )); + } + }; + let Ok(data) = frame.into_data() else { + continue; + }; + if data.is_empty() { + continue; + } + body_len += data.len() as u64; + if let Some(full_md5) = full_md5.as_mut() { + full_md5.update(&data); + } + if let Some(full_sha256) = full_sha256.as_mut() { + full_sha256.update(&data); + } + buffered.extend_from_slice(&data); + + while buffered.len() >= threshold { + let chunk = buffered.split_to(threshold).freeze(); + if let Err(response) = queue_streaming_part_upload( + state, + &mut multipart_upload, + bucket_id, + &object_key, + &metadata, + &mut next_part_number, + chunk, + &mut scheduled_part_numbers, + &mut in_flight_uploads, + ) { + if let Some(upload) = &multipart_upload { + cleanup_incomplete_streaming_upload( + state, + upload.upload_id.as_str(), + &scheduled_part_numbers, + &mut in_flight_uploads, + ) + .await; + } + return Err(response); + } + full_md5 = None; + + if in_flight_uploads.len() >= max_in_flight_uploads { + if let Err(response) = + collect_next_streaming_part(&mut in_flight_uploads, &mut completed_parts).await + { + if let Some(upload) = &multipart_upload { + cleanup_incomplete_streaming_upload( + state, + upload.upload_id.as_str(), + &scheduled_part_numbers, + &mut in_flight_uploads, + ) + .await; + } + return Err(response); + } + } + } + } + + if let (Some(expected_payload_hash), Some(full_sha256)) = (verify_payload_hash, full_sha256) { + let actual_payload_hash = hex::encode(full_sha256.finalize()); + if expected_payload_hash != actual_payload_hash { + if let Some(upload) = &multipart_upload { + cleanup_incomplete_streaming_upload( + state, + upload.upload_id.as_str(), + &scheduled_part_numbers, + &mut in_flight_uploads, + ) + .await; + } + return Err(error_response( + StatusCode::FORBIDDEN, + "SignatureDoesNotMatch", + "x-amz-content-sha256 does not match the request body", + )); + } + } + + if multipart_upload.is_some() { + if !buffered.is_empty() { + let chunk = buffered.freeze(); + if let Err(response) = queue_streaming_part_upload( + state, + &mut multipart_upload, + bucket_id, + &object_key, + &metadata, + &mut next_part_number, + chunk, + &mut scheduled_part_numbers, + &mut in_flight_uploads, + ) { + if let Some(upload) = &multipart_upload { + cleanup_incomplete_streaming_upload( + state, + upload.upload_id.as_str(), + &scheduled_part_numbers, + &mut in_flight_uploads, + ) + .await; + } + return Err(response); + } + } + + if let Err(response) = + collect_all_streaming_parts(&mut in_flight_uploads, &mut completed_parts).await + { + if let Some(upload) = &multipart_upload { + cleanup_incomplete_streaming_upload( + state, + upload.upload_id.as_str(), + &scheduled_part_numbers, + &mut in_flight_uploads, + ) + .await; + } + return Err(response); + } + + completed_parts.sort_by_key(|part| part.part_number.as_u32()); + let multipart_etags = completed_parts + .iter() + .map(|part| part.etag.clone()) + .collect::>(); + let etag = lightningstor_types::ETag::multipart(&multipart_etags, completed_parts.len()); + if let Some(upload) = multipart_upload.as_mut() { + upload.parts = completed_parts; + } + + return Ok(PreparedPutBody { + body_len, + etag, + inline_body: None, + multipart_upload, + }); + } + + let md5_hash: [u8; 16] = full_md5 + .expect("inline PUT bodies retain the aggregate MD5") + .finalize() + .into(); + let etag = lightningstor_types::ETag::from_md5(&md5_hash); + + Ok(PreparedPutBody { + body_len, + etag, + inline_body: Some(buffered.freeze()), + multipart_upload: None, + }) +} + +fn queue_streaming_part_upload( + state: &Arc, + upload: &mut Option, + bucket_id: &str, + object_key: &ObjectKey, + metadata: &ObjectMetadata, + next_part_number: &mut u32, + chunk: Bytes, + scheduled_part_numbers: &mut Vec, + in_flight_uploads: &mut FuturesUnordered, +) -> Result<(), Response> { + let upload = upload.get_or_insert_with(|| { + let mut upload = MultipartUpload::new(bucket_id.to_string(), object_key.clone()); + upload.metadata = metadata.clone(); + upload + }); + + let part_number = match PartNumber::new(*next_part_number) { + Ok(part_number) => part_number, + Err(e) => { + return Err(error_response(StatusCode::BAD_REQUEST, "InvalidRequest", e)); + } + }; + *next_part_number += 1; + scheduled_part_numbers.push(part_number.as_u32()); + in_flight_uploads.push(start_streaming_part_upload( + Arc::clone(state), + upload.upload_id.to_string(), + part_number, + chunk, + )); + Ok(()) +} + +fn start_streaming_part_upload( + state: Arc, + upload_id: String, + part_number: PartNumber, + chunk: Bytes, +) -> StreamingPartUploadHandle { + tokio::spawn(async move { + let part_etag = calculate_etag(&chunk); + let part_size = chunk.len() as u64; + state + .storage + .put_part(upload_id.as_str(), part_number.as_u32(), chunk) + .await + .map_err(|e| { + error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &format!("Failed to store object part: {}", e), + ) + })?; + Ok(Part { + part_number, + etag: part_etag, + size: part_size, + last_modified: Utc::now(), + }) + }) +} + +async fn collect_next_streaming_part( + in_flight_uploads: &mut FuturesUnordered, + completed_parts: &mut Vec, +) -> Result<(), Response> { + match in_flight_uploads.next().await { + Some(Ok(Ok(part))) => { + completed_parts.push(part); + Ok(()) + } + Some(Ok(Err(response))) => Err(response), + Some(Err(join_error)) => Err(error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &format!("Multipart upload task failed: {}", join_error), + )), + None => Ok(()), + } +} + +async fn collect_all_streaming_parts( + in_flight_uploads: &mut FuturesUnordered, + completed_parts: &mut Vec, +) -> Result<(), Response> { + while !in_flight_uploads.is_empty() { + collect_next_streaming_part(in_flight_uploads, completed_parts).await?; + } + Ok(()) +} + +async fn cleanup_incomplete_streaming_upload( + state: &Arc, + upload_id: &str, + scheduled_part_numbers: &[u32], + in_flight_uploads: &mut FuturesUnordered, +) { + while let Some(_result) = in_flight_uploads.next().await {} + for part_number in scheduled_part_numbers { + let _ = state.storage.delete_part(upload_id, *part_number).await; + } + let _ = state.storage.delete_upload_parts(upload_id).await; +} + +fn multipart_object_body(state: Arc, object: &Object, upload: MultipartUpload) -> Body { + let storage = Arc::clone(&state.storage); + let object_size = object.size; + let object_id = object.id; + let stream = stream::try_unfold( + (storage, upload, 0usize, 0u64), + move |(storage, upload, next_part_index, consumed)| async move { + if consumed >= object_size { + return Ok::<_, io::Error>(None); + } + + let mut idx = next_part_index; + let mut offset = consumed; + while idx < upload.parts.len() { + let part = &upload.parts[idx]; + let part_start = offset; + let part_end = part_start + part.size; + idx += 1; + offset = part_end; + + if object_size <= part_start { + break; + } + + let bytes = storage + .get_part(upload.upload_id.as_str(), part.part_number.as_u32()) + .await + .map_err(|e| { + io::Error::other(format!( + "failed to retrieve multipart object {object_id} part {}: {e}", + part.part_number.as_u32() + )) + })?; + let body_end = (object_size.min(part_end) - part_start) as usize; + if body_end > bytes.len() { + return Err(io::Error::other(format!( + "multipart object {object_id} part {} is inconsistent: stored={} expected={}", + part.part_number.as_u32(), + bytes.len(), + body_end + ))); + } + + return Ok(Some((bytes.slice(0..body_end), (storage, upload, idx, offset)))); + } + + Ok(None) + }, + ); + + Body::from_stream(stream) +} + +async fn delete_multipart_parts( + state: &Arc, + upload: &MultipartUpload, +) -> lightningstor_types::Result<()> { + for part in &upload.parts { + state + .storage + .delete_part(upload.upload_id.as_str(), part.part_number.as_u32()) + .await?; + } + state + .storage + .delete_upload_parts(upload.upload_id.as_str()) + .await?; + Ok(()) +} + fn calculate_etag(body: &[u8]) -> lightningstor_types::ETag { let mut md5 = Md5::new(); md5.update(body); @@ -601,41 +1318,92 @@ fn calculate_etag(body: &[u8]) -> lightningstor_types::ETag { async fn get_object( state: Arc, + tenant: TenantContext, bucket: String, key: String, ) -> impl IntoResponse { tracing::debug!(bucket = %bucket, key = %key, "GetObject request"); - let org_id = "default"; - let project_id = "default"; - // Load bucket - let bucket_obj = match state.metadata.load_bucket(org_id, project_id, &bucket).await { + let bucket_obj = match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(b)) => b, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; // Load object metadata let object = match state.metadata.load_object(&bucket_obj.id, &key, None).await { Ok(Some(o)) => o, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchKey", "The specified key does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchKey", + "The specified key does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; if object.is_delete_marker { - return error_response(StatusCode::NOT_FOUND, "NoSuchKey", "The specified key does not exist"); + return error_response( + StatusCode::NOT_FOUND, + "NoSuchKey", + "The specified key does not exist", + ); } - // Get object data from storage backend - let data = match state.storage.get_object(&object.id).await { - Ok(d) => d, - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &format!("Failed to retrieve object: {}", e)), + let multipart_upload = match state.metadata.load_object_multipart_upload(&object.id).await { + Ok(upload) => upload, + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } + }; + let (body, content_length) = if let Some(upload) = multipart_upload { + (multipart_object_body(Arc::clone(&state), &object, upload), object.size as usize) + } else { + let data = match state.storage.get_object(&object.id).await { + Ok(data) => data, + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &format!("Failed to retrieve object: {}", e), + ) + } + }; + let len = data.len(); + (Body::from(data), len) }; let mut response = Response::builder() .status(StatusCode::OK) - .header("Content-Length", data.len()) + .header("Content-Length", content_length) .header("ETag", format!("\"{}\"", object.etag.as_str())) .header("Last-Modified", object.last_modified.to_rfc2822()) .header("x-amz-version-id", object.version.as_str()); @@ -656,24 +1424,38 @@ async fn get_object( response = response.header("Cache-Control", cc); } - response.body(Body::from(data)).unwrap() + response.body(body).unwrap() } async fn delete_object( state: Arc, + tenant: TenantContext, bucket: String, key: String, ) -> impl IntoResponse { tracing::debug!(bucket = %bucket, key = %key, "DeleteObject request"); - let org_id = "default"; - let project_id = "default"; - // Load bucket - let bucket_obj = match state.metadata.load_bucket(org_id, project_id, &bucket).await { + let bucket_obj = match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(b)) => b, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; // Load object metadata @@ -686,18 +1468,53 @@ async fn delete_object( .body(Body::empty()) .unwrap(); } - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; - // Delete from storage backend - if let Err(e) = state.storage.delete_object(&object.id).await { - tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete object data"); - // Continue to delete metadata even if storage delete fails + match state + .metadata + .load_object_multipart_upload(&object.id) + .await + { + Ok(Some(upload)) => { + if let Err(e) = delete_multipart_parts(&state, &upload).await { + tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete multipart object parts"); + } + if let Err(e) = state + .metadata + .delete_object_multipart_upload(&object.id) + .await + { + tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete multipart manifest"); + } + } + Ok(None) => { + if let Err(e) = state.storage.delete_object(&object.id).await { + tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to delete object data"); + } + } + Err(e) => { + tracing::warn!(bucket = %bucket, key = %key, error = %e, "Failed to inspect multipart manifest during delete"); + } } // Delete from metadata store - if let Err(e) = state.metadata.delete_object(&bucket_obj.id, &key, None).await { - return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()); + if let Err(e) = state + .metadata + .delete_object(&bucket_obj.id, &key, None) + .await + { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ); } tracing::debug!(bucket = %bucket, key = %key, "Object deleted successfully"); @@ -711,30 +1528,60 @@ async fn delete_object( async fn head_object( state: Arc, + tenant: TenantContext, bucket: String, key: String, ) -> impl IntoResponse { tracing::debug!(bucket = %bucket, key = %key, "HeadObject request"); - let org_id = "default"; - let project_id = "default"; - // Load bucket - let bucket_obj = match state.metadata.load_bucket(org_id, project_id, &bucket).await { + let bucket_obj = match state + .metadata + .load_bucket(&tenant.org_id, &tenant.project_id, &bucket) + .await + { Ok(Some(b)) => b, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchBucket", "The specified bucket does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchBucket", + "The specified bucket does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; // Load object metadata let object = match state.metadata.load_object(&bucket_obj.id, &key, None).await { Ok(Some(o)) => o, - Ok(None) => return error_response(StatusCode::NOT_FOUND, "NoSuchKey", "The specified key does not exist"), - Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", &e.to_string()), + Ok(None) => { + return error_response( + StatusCode::NOT_FOUND, + "NoSuchKey", + "The specified key does not exist", + ) + } + Err(e) => { + return error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalError", + &e.to_string(), + ) + } }; if object.is_delete_marker { - return error_response(StatusCode::NOT_FOUND, "NoSuchKey", "The specified key does not exist"); + return error_response( + StatusCode::NOT_FOUND, + "NoSuchKey", + "The specified key does not exist", + ); } let mut response = Response::builder() @@ -781,3 +1628,682 @@ fn error_response(status: StatusCode, code: &str, message: &str) -> Response (Router, Arc) { + let tempdir = tempdir().unwrap(); + let storage = Arc::new(LocalFsBackend::new(tempdir.path(), false).await.unwrap()); + let metadata = Arc::new(MetadataStore::new_in_memory()); + let router = create_router_with_auth_state( + storage, + metadata.clone(), + Arc::new(AuthState::disabled()), + ); + std::mem::forget(tempdir); + (router, metadata) + } + + async fn test_router() -> Router { + test_router_with_metadata().await.0 + } + + fn request_with_tenant( + method: Method, + uri: &str, + body: Body, + org_id: &str, + project_id: &str, + ) -> Request { + let mut request = Request::builder() + .method(method) + .uri(uri) + .body(body) + .unwrap(); + request + .extensions_mut() + .insert(VerifiedTenantContext(TenantContext { + org_id: org_id.to_string(), + project_id: project_id.to_string(), + })); + request + } + + #[tokio::test] + async fn bucket_and_object_roundtrip_via_router() { + let router = test_router().await; + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/test-bucket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/test-bucket/folder/hello.txt") + .header("content-type", "text/plain") + .header("x-amz-meta-owner", "qa") + .body(Body::from("hello world")) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert!(response.headers().contains_key("ETag")); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::GET) + .uri("/test-bucket?list-type=2&delimiter=/") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let list_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + let list_xml = String::from_utf8(list_body.to_vec()).unwrap(); + assert!(list_xml.contains("folder/")); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::HEAD) + .uri("/test-bucket/folder/hello.txt") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers().get("Content-Length").unwrap(), "11"); + assert_eq!( + response.headers().get("Content-Type").unwrap(), + "text/plain" + ); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::GET) + .uri("/test-bucket/folder/hello.txt") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + assert_eq!(body.as_ref(), b"hello world"); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::DELETE) + .uri("/test-bucket/folder/hello.txt") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NO_CONTENT); + + let response = router + .oneshot( + Request::builder() + .method(Method::DELETE) + .uri("/test-bucket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NO_CONTENT); + } + + #[tokio::test] + async fn list_objects_v2_paginates_with_continuation_token() { + let router = test_router().await; + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/paged-bucket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + for key in ["a.txt", "b.txt", "c.txt"] { + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(format!("/paged-bucket/{key}")) + .header("content-type", "text/plain") + .body(Body::from(key.to_string())) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::GET) + .uri("/paged-bucket?list-type=2&max-keys=2") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let first_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + let first_xml = String::from_utf8(first_body.to_vec()).unwrap(); + assert!(first_xml.contains("a.txt")); + assert!(first_xml.contains("b.txt")); + assert!(first_xml.contains("true")); + assert!(first_xml.contains("b.txt")); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::GET) + .uri("/paged-bucket?list-type=2&max-keys=2&continuation-token=b.txt") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let second_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + let second_xml = String::from_utf8(second_body.to_vec()).unwrap(); + assert!(!second_xml.contains("a.txt")); + assert!(!second_xml.contains("b.txt")); + assert!(second_xml.contains("c.txt")); + assert!(second_xml.contains("false")); + } + + #[tokio::test] + async fn identical_bucket_names_are_isolated_per_tenant() { + let router = test_router().await; + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::PUT, + "/shared-bucket", + Body::empty(), + "org-a", + "project-a", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::PUT, + "/shared-bucket", + Body::empty(), + "org-b", + "project-b", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::PUT, + "/shared-bucket/object.txt", + Body::from("from-a"), + "org-a", + "project-a", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::PUT, + "/shared-bucket/object.txt", + Body::from("from-b"), + "org-b", + "project-b", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::GET, + "/shared-bucket/object.txt", + Body::empty(), + "org-a", + "project-a", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + assert_eq!(body.as_ref(), b"from-a"); + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::GET, + "/shared-bucket/object.txt", + Body::empty(), + "org-b", + "project-b", + )) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + assert_eq!(body.as_ref(), b"from-b"); + + let response = router + .clone() + .oneshot(request_with_tenant( + Method::GET, + "/", + Body::empty(), + "org-a", + "project-a", + )) + .await + .unwrap(); + let list_body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); + let list_xml = String::from_utf8(list_body.to_vec()).unwrap(); + assert_eq!(list_xml.matches("shared-bucket").count(), 1); + } + + #[tokio::test] + async fn large_put_uses_streamed_multipart_manifest() { + let (router, metadata) = test_router_with_metadata().await; + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/stream-bucket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = vec![b'x'; DEFAULT_STREAMING_PUT_THRESHOLD_BYTES + 1024]; + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/stream-bucket/large.bin") + .header("content-type", "application/octet-stream") + .body(Body::from(body.clone())) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let bucket = metadata + .load_bucket("default", "default", "stream-bucket") + .await + .unwrap() + .unwrap(); + let bucket_id = BucketId::from_str(&bucket.id.to_string()).unwrap(); + let object = metadata + .load_object(&bucket_id, "large.bin", None) + .await + .unwrap() + .unwrap(); + let manifest = metadata + .load_object_multipart_upload(&object.id) + .await + .unwrap(); + assert!(manifest.is_some()); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::GET) + .uri("/stream-bucket/large.bin") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let fetched = to_bytes(response.into_body(), (body.len() * 2) as usize) + .await + .unwrap(); + assert_eq!(fetched.as_ref(), body.as_slice()); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::DELETE) + .uri("/stream-bucket/large.bin") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NO_CONTENT); + + let manifest = metadata + .load_object_multipart_upload(&object.id) + .await + .unwrap(); + assert!(manifest.is_none()); + } + + struct DelayedMultipartStorage { + _tempdir: TempDir, + inline_objects: Mutex>, + multipart_parts: Mutex>, + put_part_delay: Duration, + in_flight_put_parts: AtomicUsize, + max_in_flight_put_parts: AtomicUsize, + } + + impl DelayedMultipartStorage { + fn new(put_part_delay: Duration) -> Self { + Self { + _tempdir: tempdir().unwrap(), + inline_objects: Mutex::new(HashMap::new()), + multipart_parts: Mutex::new(HashMap::new()), + put_part_delay, + in_flight_put_parts: AtomicUsize::new(0), + max_in_flight_put_parts: AtomicUsize::new(0), + } + } + + fn max_in_flight_put_parts(&self) -> usize { + self.max_in_flight_put_parts.load(Ordering::SeqCst) + } + + fn note_put_part_started(&self) { + let current = self.in_flight_put_parts.fetch_add(1, Ordering::SeqCst) + 1; + self.max_in_flight_put_parts + .fetch_max(current, Ordering::SeqCst); + } + + fn note_put_part_finished(&self) { + self.in_flight_put_parts.fetch_sub(1, Ordering::SeqCst); + } + } + + #[async_trait] + impl StorageBackend for DelayedMultipartStorage { + async fn put_object( + &self, + object_id: &lightningstor_types::ObjectId, + data: Bytes, + ) -> StorageResult<()> { + self.inline_objects + .lock() + .await + .insert(object_id.to_string(), data); + Ok(()) + } + + async fn get_object( + &self, + object_id: &lightningstor_types::ObjectId, + ) -> StorageResult { + self.inline_objects + .lock() + .await + .get(&object_id.to_string()) + .cloned() + .ok_or_else(|| StorageError::NotFound(*object_id)) + } + + async fn delete_object( + &self, + object_id: &lightningstor_types::ObjectId, + ) -> StorageResult<()> { + self.inline_objects + .lock() + .await + .remove(&object_id.to_string()); + Ok(()) + } + + async fn object_exists( + &self, + object_id: &lightningstor_types::ObjectId, + ) -> StorageResult { + Ok(self + .inline_objects + .lock() + .await + .contains_key(&object_id.to_string())) + } + + async fn object_size( + &self, + object_id: &lightningstor_types::ObjectId, + ) -> StorageResult { + self.inline_objects + .lock() + .await + .get(&object_id.to_string()) + .map(|bytes| bytes.len() as u64) + .ok_or_else(|| StorageError::NotFound(*object_id)) + } + + async fn put_part( + &self, + upload_id: &str, + part_number: u32, + data: Bytes, + ) -> StorageResult<()> { + self.note_put_part_started(); + sleep(self.put_part_delay).await; + self.multipart_parts + .lock() + .await + .insert((upload_id.to_string(), part_number), data); + self.note_put_part_finished(); + Ok(()) + } + + async fn get_part(&self, upload_id: &str, part_number: u32) -> StorageResult { + self.multipart_parts + .lock() + .await + .get(&(upload_id.to_string(), part_number)) + .cloned() + .ok_or_else(|| { + StorageError::Backend(format!( + "missing multipart part {upload_id}:{part_number}" + )) + }) + } + + async fn delete_part(&self, upload_id: &str, part_number: u32) -> StorageResult<()> { + self.multipart_parts + .lock() + .await + .remove(&(upload_id.to_string(), part_number)); + Ok(()) + } + + async fn delete_upload_parts(&self, upload_id: &str) -> StorageResult<()> { + self.multipart_parts + .lock() + .await + .retain(|(existing_upload_id, _), _| existing_upload_id != upload_id); + Ok(()) + } + } + + #[tokio::test] + async fn large_put_streams_multipart_parts_with_parallel_uploads() { + let storage = Arc::new(DelayedMultipartStorage::new(Duration::from_millis(25))); + let metadata = Arc::new(MetadataStore::new_in_memory()); + let router = create_router_with_auth_state( + storage.clone(), + metadata, + Arc::new(AuthState::disabled()), + ); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/parallel-stream-bucket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = vec![b'x'; (DEFAULT_STREAMING_PUT_THRESHOLD_BYTES * 2) + 4096]; + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/parallel-stream-bucket/large.bin") + .header("content-type", "application/octet-stream") + .body(Body::from(body)) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let etag = response + .headers() + .get("ETag") + .and_then(|value| value.to_str().ok()) + .unwrap(); + assert!( + etag.contains('-'), + "expected multipart ETag for streamed upload, got {etag}" + ); + assert!( + storage.max_in_flight_put_parts() >= 2, + "expected multipart uploads to overlap, saw max concurrency {}", + storage.max_in_flight_put_parts() + ); + } + + #[tokio::test] + async fn moderate_put_with_content_length_stays_inline() { + let storage = Arc::new(DelayedMultipartStorage::new(Duration::from_millis(25))); + let metadata = Arc::new(MetadataStore::new_in_memory()); + let router = create_router_with_auth_state( + storage.clone(), + metadata.clone(), + Arc::new(AuthState::disabled()), + ); + + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/inline-bucket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = vec![b'y'; DEFAULT_STREAMING_PUT_THRESHOLD_BYTES + 4096]; + let response = router + .clone() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri("/inline-bucket/moderate.bin") + .header("content-type", "application/octet-stream") + .header("content-length", body.len().to_string()) + .body(Body::from(body.clone())) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + assert_eq!(storage.max_in_flight_put_parts(), 0); + + let bucket = metadata + .load_bucket("default", "default", "inline-bucket") + .await + .unwrap() + .unwrap(); + let bucket_id = BucketId::from_str(&bucket.id.to_string()).unwrap(); + let object = metadata + .load_object(&bucket_id, "moderate.bin", None) + .await + .unwrap() + .unwrap(); + assert!(metadata + .load_object_multipart_upload(&object.id) + .await + .unwrap() + .is_none()); + let stored = storage.get_object(&object.id).await.unwrap(); + assert_eq!(stored.as_ref(), body.as_slice()); + } +} diff --git a/lightningstor/crates/lightningstor-server/src/s3/xml.rs b/lightningstor/crates/lightningstor-server/src/s3/xml.rs index ee61665..0521496 100644 --- a/lightningstor/crates/lightningstor-server/src/s3/xml.rs +++ b/lightningstor/crates/lightningstor-server/src/s3/xml.rs @@ -66,6 +66,9 @@ pub struct ListBucketResult { pub name: String, #[serde(rename = "Prefix")] pub prefix: String, + #[serde(rename = "Marker")] + #[serde(skip_serializing_if = "Option::is_none")] + pub marker: Option, #[serde(rename = "Delimiter")] #[serde(skip_serializing_if = "Option::is_none")] pub delimiter: Option, @@ -73,6 +76,9 @@ pub struct ListBucketResult { pub max_keys: u32, #[serde(rename = "IsTruncated")] pub is_truncated: bool, + #[serde(rename = "NextMarker")] + #[serde(skip_serializing_if = "Option::is_none")] + pub next_marker: Option, #[serde(rename = "Contents", default)] pub contents: Vec, #[serde(rename = "CommonPrefixes", default)] diff --git a/lightningstor/crates/lightningstor-server/src/tenant.rs b/lightningstor/crates/lightningstor-server/src/tenant.rs index 6e00b46..ccc28d2 100644 --- a/lightningstor/crates/lightningstor-server/src/tenant.rs +++ b/lightningstor/crates/lightningstor-server/src/tenant.rs @@ -1,6 +1,6 @@ use tonic::{metadata::MetadataMap, Status}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TenantContext { pub org_id: String, pub project_id: String, diff --git a/nix/ci/flake.lock b/nix/ci/flake.lock index 65b6ed5..5a9868a 100644 --- a/nix/ci/flake.lock +++ b/nix/ci/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "photoncloud", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765326679, + "narHash": "sha256-fTLX9kDwLr9Y0rH/nG+h1XG5UU+jBcy0PFYn5eneRX8=", + "owner": "nix-community", + "repo": "disko", + "rev": "d64e5cdca35b5fad7c504f615357a7afe6d9c49e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -18,6 +39,43 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-nos": { + "inputs": { + "nixpkgs": [ + "photoncloud", + "nixpkgs" + ] + }, + "locked": { + "path": "./nix-nos", + "type": "path" + }, + "original": { + "path": "./nix-nos", + "type": "path" + }, + "parent": [ + "photoncloud" + ] + }, "nixpkgs": { "locked": { "lastModified": 1765186076, @@ -34,14 +92,71 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "photoncloud": { + "inputs": { + "disko": "disko", + "flake-utils": "flake-utils_2", + "nix-nos": "nix-nos", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay", + "systems": "systems_3" + }, + "locked": { + "path": "../..", + "type": "path" + }, + "original": { + "path": "../..", + "type": "path" + }, + "parent": [] + }, "root": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "photoncloud": "photoncloud", + "rust-overlay": "rust-overlay_2" } }, "rust-overlay": { + "inputs": { + "nixpkgs": [ + "photoncloud", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765465581, + "narHash": "sha256-fCXT0aZXmTalM3NPCTedVs9xb0egBG5BOZkcrYo5PGE=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "99cc5667eece98bb35dcf35f7e511031a8b7a125", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { "inputs": { "nixpkgs": [ "nixpkgs" @@ -75,6 +190,35 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "id": "systems", + "type": "indirect" + } } }, "root": "root", diff --git a/nix/ci/flake.nix b/nix/ci/flake.nix index f89b564..607eb2f 100644 --- a/nix/ci/flake.nix +++ b/nix/ci/flake.nix @@ -5,6 +5,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + photoncloud.url = "path:../.."; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -12,7 +13,7 @@ }; }; - outputs = { self, nixpkgs, flake-utils, rust-overlay }: + outputs = { self, nixpkgs, flake-utils, photoncloud, rust-overlay }: flake-utils.lib.eachDefaultSystem (system: let overlays = [ (import rust-overlay) ]; @@ -201,7 +202,7 @@ if [[ "$no_logs" == "0" ]]; then local out - out="$logdir/shared_${crate}.$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | tr -cd 'a-z0-9_').log" + out="$logdir/shared_''${crate}.$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | tr -cd 'a-z0-9_').log" (cd "$repo_root" && bash -c "$cmd") 2>&1 | tee "$out" else (cd "$repo_root" && bash -c "$cmd") @@ -291,6 +292,11 @@ ${gate}/bin/photoncloud-gate --tier 0 --no-logs touch $out/ok ''; + checks.deployer-vm-smoke = photoncloud.checks.${system}.deployer-vm-smoke; + checks.deployer-vm-rollback = photoncloud.checks.${system}.deployer-vm-rollback; + checks.deployer-bootstrap-e2e = photoncloud.checks.${system}.deployer-bootstrap-e2e; + checks.host-lifecycle-e2e = photoncloud.checks.${system}.host-lifecycle-e2e; + checks.fleet-scheduler-e2e = photoncloud.checks.${system}.fleet-scheduler-e2e; devShells.default = pkgs.mkShell { name = "photoncloud-ci-dev"; diff --git a/nix/images/deployer-vm-smoke-target.nix b/nix/images/deployer-vm-smoke-target.nix new file mode 100644 index 0000000..3c87cfc --- /dev/null +++ b/nix/images/deployer-vm-smoke-target.nix @@ -0,0 +1,67 @@ +{ lib, modulesPath, ... }: + +{ + imports = [ + "${modulesPath}/virtualisation/qemu-vm.nix" + "${modulesPath}/testing/test-instrumentation.nix" + ]; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + networking.hostName = "worker"; + networking.firewall.enable = false; + networking.useDHCP = lib.mkForce false; + networking.dhcpcd.enable = lib.mkForce false; + systemd.network = { + enable = true; + networks."10-eth0" = { + matchConfig.Name = "eth0"; + networkConfig.DHCP = "yes"; + linkConfig.RequiredForOnline = "routable"; + }; + networks."20-eth1" = { + matchConfig.Name = "eth1"; + address = [ "192.168.1.2/24" ]; + linkConfig.RequiredForOnline = "routable"; + }; + }; + + nix.registry = lib.mkForce { }; + nix.nixPath = lib.mkForce [ ]; + nix.channel.enable = false; + nix.settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + flake-registry = ""; + }; + nixpkgs.flake = { + source = lib.mkForce null; + setFlakeRegistry = lib.mkForce false; + setNixPath = lib.mkForce false; + }; + + system.switch.enable = lib.mkForce true; + system.nixos.label = lib.mkForce "vm-smoke-target"; + system.nixos.version = lib.mkForce "vm-smoke-target"; + system.nixos.versionSuffix = lib.mkForce "-vm-smoke-target"; + environment.etc."photon-vm-smoke-target".text = "vm-smoke-target\n"; + + documentation.enable = false; + documentation.nixos.enable = false; + documentation.man.enable = false; + documentation.info.enable = false; + documentation.doc.enable = false; + + system.stateVersion = "24.11"; +} diff --git a/nix/modules/cluster-config-lib.nix b/nix/modules/cluster-config-lib.nix index da07e8f..f3e0c77 100644 --- a/nix/modules/cluster-config-lib.nix +++ b/nix/modules/cluster-config-lib.nix @@ -33,6 +33,12 @@ let mkDesiredSystemType = types: types.submodule { options = { + deploymentId = mkOption { + type = types.nullOr types.str; + default = null; + description = "Optional host deployment identifier owning this desired system"; + }; + nixosConfiguration = mkOption { type = types.nullOr types.str; default = null; @@ -62,9 +68,122 @@ let default = null; description = "Whether nix-agent should roll back when the health check fails"; }; + + drainBeforeApply = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the controller should drain the node before issuing this desired system"; + }; }; }; + mkHostDeploymentSelectorType = types: types.submodule { + options = { + nodeIds = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Explicit node IDs targeted by the deployment"; + }; + + roles = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Node roles targeted by the deployment"; + }; + + pools = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Node pools targeted by the deployment"; + }; + + nodeClasses = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Node classes targeted by the deployment"; + }; + + matchLabels = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Label selectors applied to target nodes"; + }; + }; + }; + + mkHostDeploymentType = types: + let + selectorType = mkHostDeploymentSelectorType types; + in types.submodule { + options = { + selector = mkOption { + type = selectorType; + default = { }; + description = "Node selector used by the host deployment"; + }; + + nixosConfiguration = mkOption { + type = types.nullOr types.str; + default = null; + description = "Name of the nixosConfigurations output to roll out"; + }; + + flakeRef = mkOption { + type = types.nullOr types.str; + default = null; + description = "Explicit flake reference used during rollout"; + }; + + batchSize = mkOption { + type = types.nullOr types.int; + default = null; + description = "Maximum number of nodes started per reconciliation wave"; + }; + + maxUnavailable = mkOption { + type = types.nullOr types.int; + default = null; + description = "Maximum number of unavailable nodes allowed during rollout"; + }; + + healthCheckCommand = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Health check command executed by nix-agent after activation"; + }; + + switchAction = mkOption { + type = types.nullOr types.str; + default = null; + description = "switch-to-configuration action used by nix-agent"; + }; + + rollbackOnFailure = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether nodes should roll back when rollout health checks fail"; + }; + + drainBeforeApply = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the controller should drain a node before applying the rollout"; + }; + + rebootPolicy = mkOption { + type = types.nullOr types.str; + default = null; + description = "Operator-facing reboot policy associated with the rollout"; + }; + + paused = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the rollout should start in a paused state"; + }; + }; + }; + mkNodeType = types: let installPlanType = mkInstallPlanType types; @@ -159,6 +278,30 @@ let default = null; description = "Desired deployer node lifecycle state"; }; + + commissionState = mkOption { + type = types.nullOr (types.enum [ "discovered" "commissioning" "commissioned" ]); + default = null; + description = "Optional commissioning state exported into deployer cluster state"; + }; + + installState = mkOption { + type = types.nullOr (types.enum [ "pending" "installing" "installed" "failed" "reinstall_requested" ]); + default = null; + description = "Optional install lifecycle state exported into deployer cluster state"; + }; + + powerState = mkOption { + type = types.nullOr (types.enum [ "on" "off" "cycling" "unknown" ]); + default = null; + description = "Optional external power-management state associated with the node"; + }; + + bmcRef = mkOption { + type = types.nullOr types.str; + default = null; + description = "Optional BMC / Redfish reference associated with the node"; + }; }; }; @@ -339,7 +482,10 @@ let mkDesiredSystem = nodeName: desiredSystem: let rendered = - optionalAttrs (desiredSystem != null && desiredSystem.nixosConfiguration != null) { + optionalAttrs (desiredSystem != null && desiredSystem.deploymentId != null) { + deployment_id = desiredSystem.deploymentId; + } + // optionalAttrs (desiredSystem != null && desiredSystem.nixosConfiguration != null) { nixos_configuration = desiredSystem.nixosConfiguration; } // optionalAttrs (desiredSystem != null && desiredSystem.flakeRef != null) { @@ -353,12 +499,60 @@ let } // optionalAttrs (desiredSystem != null && desiredSystem.rollbackOnFailure != null) { rollback_on_failure = desiredSystem.rollbackOnFailure; + } + // optionalAttrs (desiredSystem != null && desiredSystem.drainBeforeApply != null) { + drain_before_apply = desiredSystem.drainBeforeApply; }; in if desiredSystem == null || rendered == { } then null else { node_id = nodeName; } // rendered; + mkHostDeploymentSelector = selector: + { + node_ids = selector.nodeIds or [ ]; + roles = selector.roles or [ ]; + pools = selector.pools or [ ]; + node_classes = selector.nodeClasses or [ ]; + match_labels = selector.matchLabels or { }; + }; + + mkDeployerHostDeploymentSpec = name: deployment: + { + inherit name; + selector = mkHostDeploymentSelector deployment.selector; + } + // optionalAttrs (deployment.nixosConfiguration != null) { + nixos_configuration = deployment.nixosConfiguration; + } + // optionalAttrs (deployment.flakeRef != null) { + flake_ref = deployment.flakeRef; + } + // optionalAttrs (deployment.batchSize != null) { + batch_size = deployment.batchSize; + } + // optionalAttrs (deployment.maxUnavailable != null) { + max_unavailable = deployment.maxUnavailable; + } + // optionalAttrs (deployment.healthCheckCommand != [ ]) { + health_check_command = deployment.healthCheckCommand; + } + // optionalAttrs (deployment.switchAction != null) { + switch_action = deployment.switchAction; + } + // optionalAttrs (deployment.rollbackOnFailure != null) { + rollback_on_failure = deployment.rollbackOnFailure; + } + // optionalAttrs (deployment.drainBeforeApply != null) { + drain_before_apply = deployment.drainBeforeApply; + } + // optionalAttrs (deployment.rebootPolicy != null) { + reboot_policy = deployment.rebootPolicy; + } + // optionalAttrs (deployment.paused != null) { + paused = deployment.paused; + }; + mkDeployerNodeSpec = nodeName: node: { node_id = nodeName; @@ -390,6 +584,18 @@ let } // optionalAttrs (node.state != null) { state = node.state; + } + // optionalAttrs (node.commissionState != null) { + commission_state = node.commissionState; + } + // optionalAttrs (node.installState != null) { + install_state = node.installState; + } + // optionalAttrs (node.powerState != null) { + power_state = node.powerState; + } + // optionalAttrs (node.bmcRef != null) { + bmc_ref = node.bmcRef; }; mkDeployerNodeClassSpec = name: nodeClass: @@ -522,6 +728,7 @@ let nodeClasses = deployer.nodeClasses or { }; pools = deployer.pools or { }; enrollmentRules = deployer.enrollmentRules or { }; + hostDeployments = deployer.hostDeployments or { }; in { cluster = { cluster_id = clusterId; @@ -532,6 +739,7 @@ let node_classes = map (name: mkDeployerNodeClassSpec name nodeClasses.${name}) (attrNames nodeClasses); pools = map (name: mkDeployerPoolSpec name pools.${name}) (attrNames pools); enrollment_rules = map (name: mkDeployerEnrollmentRuleSpec name enrollmentRules.${name}) (attrNames enrollmentRules); + host_deployments = map (name: mkDeployerHostDeploymentSpec name hostDeployments.${name}) (attrNames hostDeployments); services = [ ]; instances = [ ]; mtls_policies = [ ]; @@ -541,6 +749,8 @@ in inherit mkInstallPlanType mkDesiredSystemType + mkHostDeploymentSelectorType + mkHostDeploymentType mkNodeType mkNodeClassType mkNodePoolType diff --git a/nix/modules/coronafs.nix b/nix/modules/coronafs.nix index f30e64a..ae6ff2b 100644 --- a/nix/modules/coronafs.nix +++ b/nix/modules/coronafs.nix @@ -2,30 +2,112 @@ let cfg = config.services.coronafs; + chainfireEnabled = lib.hasAttrByPath [ "services" "chainfire" "enable" ] config && config.services.chainfire.enable; + chainfireApiUrls = + if cfg.chainfireApiUrl != null then + lib.filter (item: item != "") (map lib.strings.trim (lib.splitString "," cfg.chainfireApiUrl)) + else + [ ]; + effectiveChainfireApiUrl = + if cfg.chainfireApiUrl != null then cfg.chainfireApiUrl + else if chainfireEnabled then "http://127.0.0.1:${toString config.services.chainfire.httpPort}" + else null; + localChainfireApiUrl = + lib.any + (url: + lib.hasPrefix "http://127.0.0.1:" url + || lib.hasPrefix "http://localhost:" url + ) + ( + if effectiveChainfireApiUrl == null then + [ ] + else if cfg.chainfireApiUrl != null then + chainfireApiUrls + else + [ effectiveChainfireApiUrl ] + ); + waitForChainfire = + pkgs.writeShellScript "coronafs-wait-for-chainfire" '' + set -eu + deadline=$((SECONDS + 60)) + urls='${lib.concatStringsSep " " ( + if effectiveChainfireApiUrl == null then + [ ] + else if cfg.chainfireApiUrl != null then + chainfireApiUrls + else + [ effectiveChainfireApiUrl ] + )}' + while true; do + for url in $urls; do + if curl -fsS "$url/health" >/dev/null 2>&1; then + exit 0 + fi + done + if [ "$SECONDS" -ge "$deadline" ]; then + echo "timed out waiting for ChainFire at ${if effectiveChainfireApiUrl == null then "(none)" else effectiveChainfireApiUrl}" >&2 + exit 1 + fi + sleep 1 + done + ''; tomlFormat = pkgs.formats.toml { }; - coronafsConfigFile = tomlFormat.generate "coronafs.toml" { - listen_addr = "0.0.0.0:${toString cfg.port}"; - advertise_host = cfg.advertiseHost; - data_dir = toString cfg.dataDir; - export_bind_addr = cfg.exportBindAddr; - export_base_port = cfg.exportBasePort; - export_port_count = cfg.exportPortCount; - export_shared_clients = cfg.exportSharedClients; - export_cache_mode = cfg.exportCacheMode; - export_aio_mode = cfg.exportAioMode; - export_discard_mode = cfg.exportDiscardMode; - export_detect_zeroes_mode = cfg.exportDetectZeroesMode; - preallocate = cfg.preallocate; - sync_on_write = cfg.syncOnWrite; - qemu_nbd_path = "${pkgs.qemu}/bin/qemu-nbd"; - qemu_img_path = "${pkgs.qemu}/bin/qemu-img"; - log_level = "info"; - }; + coronafsConfigFile = tomlFormat.generate "coronafs.toml" ( + { + mode = cfg.mode; + metadata_backend = cfg.metadataBackend; + chainfire_key_prefix = cfg.chainfireKeyPrefix; + listen_addr = "0.0.0.0:${toString cfg.port}"; + advertise_host = cfg.advertiseHost; + data_dir = toString cfg.dataDir; + export_bind_addr = cfg.exportBindAddr; + export_base_port = cfg.exportBasePort; + export_port_count = cfg.exportPortCount; + export_shared_clients = cfg.exportSharedClients; + export_cache_mode = cfg.exportCacheMode; + export_aio_mode = cfg.exportAioMode; + export_discard_mode = cfg.exportDiscardMode; + export_detect_zeroes_mode = cfg.exportDetectZeroesMode; + preallocate = cfg.preallocate; + sync_on_write = cfg.syncOnWrite; + qemu_nbd_path = "${pkgs.qemu}/bin/qemu-nbd"; + qemu_img_path = "${pkgs.qemu}/bin/qemu-img"; + log_level = "info"; + } + // lib.optionalAttrs (effectiveChainfireApiUrl != null) { + chainfire_api_url = effectiveChainfireApiUrl; + } + ); in { options.services.coronafs = { enable = lib.mkEnableOption "CoronaFS block volume service"; + mode = lib.mkOption { + type = lib.types.enum [ "combined" "controller" "node" ]; + default = "combined"; + description = "CoronaFS operating mode: combined compatibility mode, controller-only API, or node-local export mode."; + }; + + metadataBackend = lib.mkOption { + type = lib.types.enum [ "filesystem" "chainfire" ]; + default = "filesystem"; + description = "Metadata backend for CoronaFS volume metadata. Use chainfire on controller nodes to replicate volume metadata."; + }; + + chainfireApiUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Optional ChainFire HTTP API URL used when metadataBackend = chainfire. Comma-separated endpoints are allowed for failover."; + example = "http://127.0.0.1:8081"; + }; + + chainfireKeyPrefix = lib.mkOption { + type = lib.types.str; + default = "/coronafs/volumes"; + description = "ChainFire key prefix used to store CoronaFS metadata when metadataBackend = chainfire."; + }; + port = lib.mkOption { type = lib.types.port; default = 50088; @@ -71,7 +153,7 @@ in exportAioMode = lib.mkOption { type = lib.types.enum [ "native" "io_uring" "threads" ]; - default = "io_uring"; + default = "threads"; description = "qemu-nbd AIO mode for CoronaFS exports."; }; @@ -113,11 +195,22 @@ in }; config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.metadataBackend != "chainfire" || effectiveChainfireApiUrl != null; + message = "services.coronafs.metadataBackend = \"chainfire\" requires services.coronafs.chainfireApiUrl or a local services.chainfire instance."; + } + ]; + users.users.coronafs = { isSystemUser = true; group = "coronafs"; description = "CoronaFS service user"; home = cfg.dataDir; + extraGroups = + lib.optional + (lib.hasAttrByPath [ "services" "plasmavmc" "enable" ] config && config.services.plasmavmc.enable) + "plasmavmc"; }; users.groups.coronafs = { }; @@ -125,8 +218,9 @@ in systemd.services.coronafs = { description = "CoronaFS Block Volume Service"; wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - path = [ pkgs.qemu pkgs.util-linux pkgs.procps pkgs.coreutils ]; + after = [ "network.target" ] ++ lib.optionals chainfireEnabled [ "chainfire.service" ]; + wants = lib.optionals chainfireEnabled [ "chainfire.service" ]; + path = [ pkgs.qemu pkgs.util-linux pkgs.procps pkgs.coreutils pkgs.curl ]; serviceConfig = { Type = "simple"; @@ -138,13 +232,14 @@ in StateDirectory = "coronafs"; StateDirectoryMode = "0750"; ReadWritePaths = [ cfg.dataDir ]; + ExecStartPre = lib.optionals (cfg.metadataBackend == "chainfire" && localChainfireApiUrl) [ waitForChainfire ]; ExecStart = "${cfg.package}/bin/coronafs-server --config ${coronafsConfigFile}"; }; }; systemd.tmpfiles.rules = [ "d ${toString cfg.dataDir} 0750 coronafs coronafs -" - "d ${toString cfg.dataDir}/volumes 0750 coronafs coronafs -" + "d ${toString cfg.dataDir}/volumes 2770 coronafs coronafs -" "d ${toString cfg.dataDir}/metadata 0750 coronafs coronafs -" "d ${toString cfg.dataDir}/pids 0750 coronafs coronafs -" ]; diff --git a/nix/modules/deployer.nix b/nix/modules/deployer.nix index 07af739..247ee4d 100644 --- a/nix/modules/deployer.nix +++ b/nix/modules/deployer.nix @@ -3,6 +3,23 @@ let cfg = config.services.deployer; tomlFormat = pkgs.formats.toml { }; + usesLocalChainfire = + builtins.any + ( + endpoint: + lib.hasPrefix "http://127.0.0.1:" endpoint + || lib.hasPrefix "http://localhost:" endpoint + || lib.hasPrefix "http://[::1]:" endpoint + ) + cfg.chainfireEndpoints; + localChainfireDeps = + lib.optionals + ( + usesLocalChainfire + && lib.hasAttrByPath [ "services" "chainfire" "enable" ] config + && config.services.chainfire.enable + ) + [ "chainfire.service" ]; generatedConfig = { bind_addr = cfg.bindAddr; chainfire = { @@ -226,7 +243,9 @@ in systemd.services.deployer = { description = "PlasmaCloud Deployer Server"; wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; + wants = [ "network-online.target" ] ++ localChainfireDeps; + after = [ "network-online.target" ] ++ localChainfireDeps; + requires = localChainfireDeps; environment = {} // lib.optionalAttrs (cfg.bootstrapToken != null) { diff --git a/nix/modules/first-boot-automation.nix b/nix/modules/first-boot-automation.nix index 6aef770..8f1fb89 100644 --- a/nix/modules/first-boot-automation.nix +++ b/nix/modules/first-boot-automation.nix @@ -285,7 +285,7 @@ in healthUrl = "http://localhost:8082/health"; # Health endpoint on admin port leaderUrlKey = "flaredb_leader_url"; defaultLeaderUrl = "http://localhost:8082"; - joinPath = null; + joinPath = "/admin/member/add"; port = cfg.flaredbPort; description = "FlareDB"; } // { diff --git a/nix/modules/lightningstor.nix b/nix/modules/lightningstor.nix index 0a5172c..e06dda3 100644 --- a/nix/modules/lightningstor.nix +++ b/nix/modules/lightningstor.nix @@ -297,6 +297,30 @@ in description = "Prometheus metrics port for lightningstor-node."; }; + s3StreamingPutThresholdBytes = lib.mkOption { + type = lib.types.int; + default = 64 * 1024 * 1024; + description = "Streaming PUT multipart threshold for the S3 frontend."; + }; + + s3InlinePutMaxBytes = lib.mkOption { + type = lib.types.int; + default = 128 * 1024 * 1024; + description = "Maximum inline single-PUT size for the S3 frontend."; + }; + + s3MultipartPutConcurrency = lib.mkOption { + type = lib.types.int; + default = 4; + description = "Maximum in-flight multipart PUT part uploads."; + }; + + s3MultipartFetchConcurrency = lib.mkOption { + type = lib.types.int; + default = 4; + description = "Maximum concurrent multipart GET part fetches."; + }; + databaseUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -369,6 +393,14 @@ in environment = { RUST_LOG = "info"; + LIGHTNINGSTOR_S3_STREAMING_PUT_THRESHOLD_BYTES = + toString cfg.s3StreamingPutThresholdBytes; + LIGHTNINGSTOR_S3_INLINE_PUT_MAX_BYTES = + toString cfg.s3InlinePutMaxBytes; + LIGHTNINGSTOR_S3_MULTIPART_PUT_CONCURRENCY = + toString cfg.s3MultipartPutConcurrency; + LIGHTNINGSTOR_S3_MULTIPART_FETCH_CONCURRENCY = + toString cfg.s3MultipartFetchConcurrency; }; }; }; diff --git a/nix/modules/plasmacloud-cluster.nix b/nix/modules/plasmacloud-cluster.nix index ab5bc28..86bb4d5 100644 --- a/nix/modules/plasmacloud-cluster.nix +++ b/nix/modules/plasmacloud-cluster.nix @@ -9,6 +9,7 @@ let nodeClassType = clusterConfigLib.mkNodeClassType types; nodePoolType = clusterConfigLib.mkNodePoolType types; enrollmentRuleType = clusterConfigLib.mkEnrollmentRuleType types; + hostDeploymentType = clusterConfigLib.mkHostDeploymentType types; jsonFormat = pkgs.formats.json { }; # Generate cluster-config.json for the current node @@ -98,6 +99,12 @@ in { default = { }; description = "Deployer auto-enrollment rules derived from Nix"; }; + + hostDeployments = mkOption { + type = types.attrsOf hostDeploymentType; + default = { }; + description = "Declarative host rollout objects derived from Nix"; + }; }; generated = { @@ -173,6 +180,16 @@ in { ) (attrNames cfg.deployer.enrollmentRules); message = "All deployer enrollment rules must reference existing pools and node classes"; } + { + assertion = all (deploymentName: + let + deployment = cfg.deployer.hostDeployments.${deploymentName}; + in + all (pool: cfg.deployer.pools ? "${pool}") deployment.selector.pools + && all (nodeClass: cfg.deployer.nodeClasses ? "${nodeClass}") deployment.selector.nodeClasses + ) (attrNames cfg.deployer.hostDeployments); + message = "All deployer host deployments must reference existing pools and node classes"; + } ]; # Generate cluster-config.json for first-boot-automation diff --git a/nix/modules/plasmavmc.nix b/nix/modules/plasmavmc.nix index 050e108..5573adc 100644 --- a/nix/modules/plasmavmc.nix +++ b/nix/modules/plasmavmc.nix @@ -2,11 +2,30 @@ let cfg = config.services.plasmavmc; + localIamDeps = lib.optional (config.services.iam.enable or false) "iam.service"; + localIamHealthUrl = + if config.services.iam.enable or false + then "http://127.0.0.1:${toString config.services.iam.httpPort}/health" + else null; + remoteIamEndpoint = + if !(config.services.iam.enable or false) && cfg.iamAddr != null + then cfg.iamAddr + else null; coronafsEnabled = lib.hasAttrByPath [ "services" "coronafs" "enable" ] config && config.services.coronafs.enable; coronafsDataDir = if coronafsEnabled && lib.hasAttrByPath [ "services" "coronafs" "dataDir" ] config then toString config.services.coronafs.dataDir else null; + effectiveCoronafsControllerEndpoint = + if cfg.coronafsControllerEndpoint != null then cfg.coronafsControllerEndpoint + else if cfg.coronafsEndpoint != null then cfg.coronafsEndpoint + else if coronafsEnabled then "http://127.0.0.1:${toString config.services.coronafs.port}" + else null; + effectiveCoronafsNodeEndpoint = + if cfg.coronafsNodeEndpoint != null then cfg.coronafsNodeEndpoint + else if coronafsEnabled then "http://127.0.0.1:${toString config.services.coronafs.port}" + else if cfg.coronafsEndpoint != null then cfg.coronafsEndpoint + else null; tomlFormat = pkgs.formats.toml { }; plasmavmcConfigFile = tomlFormat.generate "plasmavmc.toml" { addr = "0.0.0.0:${toString cfg.port}"; @@ -94,10 +113,41 @@ in coronafsEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; - description = "CoronaFS HTTP endpoint used to provision and export managed VM volumes."; + description = "Deprecated combined CoronaFS HTTP endpoint used to provision and export managed VM volumes."; example = "http://10.0.0.11:50088"; }; + coronafsControllerEndpoint = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "CoronaFS controller HTTP endpoint used to provision and resize managed VM volumes. Comma-separated endpoints are allowed for client-side failover."; + example = "http://10.0.0.11:50088"; + }; + + coronafsNodeEndpoint = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "CoronaFS node-local HTTP endpoint used to resolve local paths and exports for attached VM volumes. Comma-separated endpoints are allowed for client-side failover."; + example = "http://127.0.0.1:50088"; + }; + + coronafsNodeLocalAttach = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable writable VM attachment through node-local CoronaFS materialization. + This requires services.plasmavmc.sharedLiveMigration = false because migrations use cold relocate plus flush-back. + ''; + }; + + experimentalCoronafsNodeLocalAttach = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Deprecated alias for services.plasmavmc.coronafsNodeLocalAttach. + ''; + }; + managedVolumeRoot = lib.mkOption { type = lib.types.path; default = "/var/lib/plasmavmc/managed-volumes"; @@ -173,6 +223,24 @@ in }; config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !((cfg.coronafsNodeLocalAttach || cfg.experimentalCoronafsNodeLocalAttach) && cfg.sharedLiveMigration); + message = '' + services.plasmavmc.coronafsNodeLocalAttach requires services.plasmavmc.sharedLiveMigration = false + because writable node-local CoronaFS attachment uses cold relocate plus flush-back instead of shared-storage live migration. + ''; + } + ]; + + warnings = + lib.optional (cfg.coronafsEndpoint != null) '' + services.plasmavmc.coronafsEndpoint is deprecated; use services.plasmavmc.coronafsControllerEndpoint and services.plasmavmc.coronafsNodeEndpoint. + '' + ++ lib.optional (cfg.experimentalCoronafsNodeLocalAttach) '' + services.plasmavmc.experimentalCoronafsNodeLocalAttach is deprecated; use services.plasmavmc.coronafsNodeLocalAttach. + ''; + # Create system user users.users.plasmavmc = { isSystemUser = true; @@ -188,9 +256,35 @@ in systemd.services.plasmavmc = { description = "PlasmaVMC Virtual Machine Compute Service"; wantedBy = [ "multi-user.target" ]; - after = [ "network.target" "prismnet.service" "flaredb.service" "chainfire.service" ]; - wants = [ "prismnet.service" "flaredb.service" "chainfire.service" ]; - path = [ pkgs.qemu pkgs.coreutils ]; + after = [ "network-online.target" "prismnet.service" "flaredb.service" "chainfire.service" ] ++ localIamDeps; + wants = [ "network-online.target" "prismnet.service" "flaredb.service" "chainfire.service" ] ++ localIamDeps; + path = [ pkgs.qemu pkgs.coreutils pkgs.curl ]; + preStart = + lib.optionalString (localIamHealthUrl != null) '' + for _ in $(seq 1 90); do + if curl -fsS ${lib.escapeShellArg localIamHealthUrl} >/dev/null 2>&1; then + exit 0 + fi + sleep 1 + done + echo "plasmavmc: timed out waiting for local IAM health at ${localIamHealthUrl}" >&2 + exit 1 + '' + + lib.optionalString (remoteIamEndpoint != null) '' + endpoint=${lib.escapeShellArg remoteIamEndpoint} + endpoint="''${endpoint#http://}" + endpoint="''${endpoint#https://}" + host="''${endpoint%:*}" + port="''${endpoint##*:}" + for _ in $(${pkgs.coreutils}/bin/seq 1 90); do + if ${pkgs.coreutils}/bin/timeout 1 ${pkgs.bash}/bin/bash -lc "/dev/null 2>&1; then + exit 0 + fi + sleep 1 + done + echo "plasmavmc: timed out waiting for IAM gRPC at ''${host}:''${port}" >&2 + exit 1 + ''; environment = lib.mkMerge [ { @@ -213,6 +307,16 @@ in (lib.mkIf (cfg.lightningstorAddr != null) { PLASMAVMC_LIGHTNINGSTOR_ENDPOINT = cfg.lightningstorAddr; }) + (lib.mkIf (effectiveCoronafsControllerEndpoint != null) { + PLASMAVMC_CORONAFS_CONTROLLER_ENDPOINT = effectiveCoronafsControllerEndpoint; + }) + (lib.mkIf (effectiveCoronafsNodeEndpoint != null) { + PLASMAVMC_CORONAFS_NODE_ENDPOINT = effectiveCoronafsNodeEndpoint; + }) + (lib.mkIf (cfg.coronafsNodeLocalAttach || cfg.experimentalCoronafsNodeLocalAttach) { + PLASMAVMC_CORONAFS_NODE_LOCAL_ATTACH = "1"; + PLASMAVMC_CORONAFS_ENABLE_EXPERIMENTAL_NODE_LOCAL_ATTACH = "1"; + }) (lib.mkIf (cfg.coronafsEndpoint != null) { PLASMAVMC_CORONAFS_ENDPOINT = cfg.coronafsEndpoint; }) @@ -273,6 +377,8 @@ in systemd.tmpfiles.rules = [ "d ${builtins.dirOf (toString cfg.managedVolumeRoot)} 0755 plasmavmc plasmavmc -" "d ${toString cfg.managedVolumeRoot} 0750 plasmavmc plasmavmc -" + ] ++ lib.optionals coronafsEnabled [ + "d ${toString cfg.dataDir}/images 2770 plasmavmc coronafs -" ]; }; } diff --git a/nix/nodes/vm-cluster/cluster.nix b/nix/nodes/vm-cluster/cluster.nix index 4c7af92..d16a79a 100644 --- a/nix/nodes/vm-cluster/cluster.nix +++ b/nix/nodes/vm-cluster/cluster.nix @@ -108,6 +108,19 @@ }; }; }; + + hostDeployments = { + control-plane-canary = { + selector.nodeIds = [ "node01" ]; + nixosConfiguration = "node01"; + flakeRef = "github:centra/cloud"; + batchSize = 1; + maxUnavailable = 1; + healthCheckCommand = [ "systemctl" "is-system-running" "--wait" ]; + switchAction = "switch"; + rollbackOnFailure = true; + }; + }; }; bootstrap.initialPeers = [ "node01" "node02" "node03" ]; diff --git a/nix/nodes/vm-cluster/node01/configuration.nix b/nix/nodes/vm-cluster/node01/configuration.nix index b32a2a0..efbf194 100644 --- a/nix/nodes/vm-cluster/node01/configuration.nix +++ b/nix/nodes/vm-cluster/node01/configuration.nix @@ -32,8 +32,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "192.168.100.11:2379"; - flaredbAddr = "192.168.100.11:2479"; + chainfireAddr = "192.168.100.11:2379,192.168.100.12:2379,192.168.100.13:2379"; + flaredbAddr = "192.168.100.11:2479,192.168.100.12:2479,192.168.100.13:2479"; }; services.openssh.enable = true; diff --git a/nix/nodes/vm-cluster/node02/configuration.nix b/nix/nodes/vm-cluster/node02/configuration.nix index 4f3f05d..62aed95 100644 --- a/nix/nodes/vm-cluster/node02/configuration.nix +++ b/nix/nodes/vm-cluster/node02/configuration.nix @@ -42,8 +42,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "192.168.100.11:2379"; - flaredbAddr = "192.168.100.11:2479"; + chainfireAddr = "192.168.100.11:2379,192.168.100.12:2379,192.168.100.13:2379"; + flaredbAddr = "192.168.100.11:2479,192.168.100.12:2479,192.168.100.13:2479"; }; services.openssh.enable = true; diff --git a/nix/nodes/vm-cluster/node03/configuration.nix b/nix/nodes/vm-cluster/node03/configuration.nix index 16644ee..f286e5f 100644 --- a/nix/nodes/vm-cluster/node03/configuration.nix +++ b/nix/nodes/vm-cluster/node03/configuration.nix @@ -42,8 +42,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "192.168.100.11:2379"; - flaredbAddr = "192.168.100.11:2479"; + chainfireAddr = "192.168.100.11:2379,192.168.100.12:2379,192.168.100.13:2379"; + flaredbAddr = "192.168.100.11:2479,192.168.100.12:2479,192.168.100.13:2479"; }; services.openssh.enable = true; diff --git a/nix/test-cluster/README.md b/nix/test-cluster/README.md index e66c86a..c494a44 100644 --- a/nix/test-cluster/README.md +++ b/nix/test-cluster/README.md @@ -63,10 +63,13 @@ Preferred entrypoint for publishable verification: `nix run ./nix/test-cluster#c Preferred entrypoint for publishable matrix verification: `nix run ./nix/test-cluster#cluster -- fresh-matrix` -`nix run ./nix/test-cluster#cluster -- bench-storage` benchmarks CoronaFS local-vs-shared-volume I/O, queued random-read behavior, cross-worker direct-I/O shared-volume reads, and LightningStor large/small-object S3 throughput and writes a report to `docs/storage-benchmarks.md`. +`nix run ./nix/test-cluster#cluster -- bench-storage` benchmarks CoronaFS controller-export vs node-local-export I/O, worker-side materialization latency, and LightningStor large/small-object S3 throughput, then writes a report to `docs/storage-benchmarks.md`. Preferred entrypoint for publishable storage numbers: `nix run ./nix/test-cluster#cluster -- fresh-storage-bench` +`nix run ./nix/test-cluster#cluster -- bench-coronafs-local-matrix` runs the local single-process CoronaFS export benchmark across the supported `cache`/`aio` combinations so software-path regressions can be separated from VM-lab network limits. +On the current lab hosts, `cache=none` with `aio=io_uring` is the strongest local-export profile and should be treated as the reference point when CoronaFS remote numbers are being distorted by the nested-QEMU/VDE network path. + ## Advanced usage Use the script entrypoint only for local debugging inside a prepared Nix shell: diff --git a/nix/test-cluster/common.nix b/nix/test-cluster/common.nix index 8ebdb47..b695b18 100644 --- a/nix/test-cluster/common.nix +++ b/nix/test-cluster/common.nix @@ -27,6 +27,18 @@ in default = "/tmp/photoncloud-test-cluster-vde.sock"; description = "VDE control socket path used for the east-west cluster NIC."; }; + + chainfireControlPlaneAddrs = lib.mkOption { + type = lib.types.str; + default = "10.100.0.11:2379,10.100.0.12:2379,10.100.0.13:2379"; + description = "Comma-separated ChainFire client endpoints for multi-endpoint failover."; + }; + + flaredbControlPlaneAddrs = lib.mkOption { + type = lib.types.str; + default = "10.100.0.11:2479,10.100.0.12:2479,10.100.0.13:2479"; + description = "Comma-separated FlareDB client endpoints for multi-endpoint failover."; + }; }; config = { @@ -84,10 +96,43 @@ in system.stateVersion = "24.05"; + systemd.services.photon-test-cluster-net-tuning = { + description = "Tune cluster NIC offloads for nested-QEMU storage tests"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + path = [ pkgs.ethtool pkgs.iproute2 pkgs.coreutils ]; + script = '' + set -eu + iface="eth1" + for _ in $(seq 1 30); do + if ip link show "$iface" >/dev/null 2>&1; then + break + fi + sleep 1 + done + if ! ip link show "$iface" >/dev/null 2>&1; then + echo "photon-test-cluster-net-tuning: $iface not present, skipping" >&2 + exit 0 + fi + + # Nested QEMU over VDE is sensitive to guest-side offloads; disabling + # them reduces retransmits and keeps the storage benchmarks closer to + # raw TCP throughput. + ethtool -K "$iface" tso off gso off gro off tx off rx off sg off || true + ip link set dev "$iface" txqueuelen 10000 || true + ''; + }; + environment.systemPackages = with pkgs; [ awscli2 curl dnsutils + ethtool fio jq grpcurl diff --git a/nix/test-cluster/flake.nix b/nix/test-cluster/flake.nix index 7de63b8..88ab10f 100644 --- a/nix/test-cluster/flake.nix +++ b/nix/test-cluster/flake.nix @@ -115,12 +115,17 @@ curl grpcurl jq + llvmPackages.clang + llvmPackages.libclang openssh + protobuf clusterPython qemu sshpass vde2 ]; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + PROTOC = "${pkgs.protobuf}/bin/protoc"; }; }; } diff --git a/nix/test-cluster/node01.nix b/nix/test-cluster/node01.nix index 6c47d5f..ff69c89 100644 --- a/nix/test-cluster/node01.nix +++ b/nix/test-cluster/node01.nix @@ -69,29 +69,29 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; services.prismnet = { enable = true; port = 50081; iamAddr = "10.100.0.11:50080"; - flaredbAddr = "10.100.0.11:2479"; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; services.flashdns = { enable = true; iamAddr = "10.100.0.11:50080"; - flaredbAddr = "10.100.0.11:2479"; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; services.fiberlb = { enable = true; port = 50085; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; services.plasmavmc = { @@ -101,14 +101,17 @@ httpPort = 8084; prismnetAddr = "10.100.0.11:50081"; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; lightningstorAddr = "10.100.0.11:50086"; - coronafsEndpoint = "http://10.100.0.11:50088"; + coronafsControllerEndpoint = "http://127.0.0.1:50088"; + coronafsNodeEndpoint = "http://127.0.0.1:50088"; }; services.coronafs = { enable = true; + metadataBackend = "chainfire"; + chainfireKeyPrefix = "/coronafs/test-cluster/control/volumes"; port = 50088; advertiseHost = "10.100.0.11"; exportBasePort = 11000; @@ -138,9 +141,9 @@ readQuorum = 1; writeQuorum = 2; nodeMetricsPort = 9198; - chainfireAddr = "10.100.0.11:2379"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; - flaredbAddr = "10.100.0.11:2479"; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; zone = "zone-a"; region = "test"; }; @@ -149,10 +152,10 @@ enable = true; port = 50087; iamAddr = "http://10.100.0.11:50080"; - chainfireAddr = "http://10.100.0.11:2379"; + chainfireAddr = "http://${config.photonTestCluster.chainfireControlPlaneAddrs}"; prismnetAddr = "http://10.100.0.11:50081"; - flaredbPdAddr = "10.100.0.11:2379"; - flaredbDirectAddr = "10.100.0.11:2479"; + flaredbPdAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbDirectAddr = config.photonTestCluster.flaredbControlPlaneAddrs; fiberlbAddr = "http://10.100.0.11:50085"; flashdnsAddr = "http://10.100.0.11:50084"; }; diff --git a/nix/test-cluster/node02.nix b/nix/test-cluster/node02.nix index 8149055..430fdf9 100644 --- a/nix/test-cluster/node02.nix +++ b/nix/test-cluster/node02.nix @@ -41,7 +41,6 @@ nodeId = "node02"; raftAddr = "10.100.0.12:2480"; apiAddr = "10.100.0.12:2479"; - pdAddr = "10.100.0.11:2379"; initialPeers = [ "node01=10.100.0.11:2479" "node02=10.100.0.12:2479" @@ -63,8 +62,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "10.100.0.12:2379"; - flaredbAddr = "10.100.0.12:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; systemd.services.iam.environment = { diff --git a/nix/test-cluster/node03.nix b/nix/test-cluster/node03.nix index 648bb46..d03704b 100644 --- a/nix/test-cluster/node03.nix +++ b/nix/test-cluster/node03.nix @@ -41,7 +41,6 @@ nodeId = "node03"; raftAddr = "10.100.0.13:2480"; apiAddr = "10.100.0.13:2479"; - pdAddr = "10.100.0.11:2379"; initialPeers = [ "node01=10.100.0.11:2479" "node02=10.100.0.12:2479" @@ -63,8 +62,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "10.100.0.13:2379"; - flaredbAddr = "10.100.0.13:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; systemd.services.iam.environment = { diff --git a/nix/test-cluster/node04.nix b/nix/test-cluster/node04.nix index 99dbbb5..17015a9 100644 --- a/nix/test-cluster/node04.nix +++ b/nix/test-cluster/node04.nix @@ -8,6 +8,7 @@ imports = [ ./common.nix ../modules/plasmavmc.nix + ../modules/coronafs.nix ../modules/lightningstor.nix ../modules/node-agent.nix ]; @@ -27,16 +28,26 @@ services.plasmavmc = { enable = true; mode = "agent"; + coronafsNodeLocalAttach = true; + sharedLiveMigration = false; port = 50082; httpPort = 8084; prismnetAddr = "10.100.0.11:50081"; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; controlPlaneAddr = "10.100.0.11:50082"; advertiseAddr = "10.100.0.21:50082"; lightningstorAddr = "10.100.0.11:50086"; - coronafsEndpoint = "http://10.100.0.11:50088"; + coronafsControllerEndpoint = "http://10.100.0.11:50088"; + coronafsNodeEndpoint = "http://127.0.0.1:50088"; + }; + + services.coronafs = { + enable = true; + mode = "node"; + port = 50088; + advertiseHost = "10.100.0.21"; }; services.lightningstor = { @@ -44,8 +55,8 @@ mode = "data"; port = 50086; distributedRequestTimeoutMs = 300000; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; zone = "zone-b"; region = "test"; @@ -53,7 +64,7 @@ services.node-agent = { enable = true; - chainfireEndpoint = "http://10.100.0.11:2379"; + chainfireEndpoint = config.photonTestCluster.chainfireControlPlaneAddrs; clusterId = "test-cluster"; nodeId = "node04"; intervalSecs = 5; diff --git a/nix/test-cluster/node05.nix b/nix/test-cluster/node05.nix index 55ed50a..343a1ff 100644 --- a/nix/test-cluster/node05.nix +++ b/nix/test-cluster/node05.nix @@ -8,6 +8,7 @@ imports = [ ./common.nix ../modules/plasmavmc.nix + ../modules/coronafs.nix ../modules/lightningstor.nix ../modules/node-agent.nix ]; @@ -27,16 +28,26 @@ services.plasmavmc = { enable = true; mode = "agent"; + coronafsNodeLocalAttach = true; + sharedLiveMigration = false; port = 50082; httpPort = 8084; prismnetAddr = "10.100.0.11:50081"; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; controlPlaneAddr = "10.100.0.11:50082"; advertiseAddr = "10.100.0.22:50082"; lightningstorAddr = "10.100.0.11:50086"; - coronafsEndpoint = "http://10.100.0.11:50088"; + coronafsControllerEndpoint = "http://10.100.0.11:50088"; + coronafsNodeEndpoint = "http://127.0.0.1:50088"; + }; + + services.coronafs = { + enable = true; + mode = "node"; + port = 50088; + advertiseHost = "10.100.0.22"; }; services.lightningstor = { @@ -44,8 +55,8 @@ mode = "data"; port = 50086; distributedRequestTimeoutMs = 300000; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; zone = "zone-c"; region = "test"; @@ -53,7 +64,7 @@ services.node-agent = { enable = true; - chainfireEndpoint = "http://10.100.0.11:2379"; + chainfireEndpoint = config.photonTestCluster.chainfireControlPlaneAddrs; clusterId = "test-cluster"; nodeId = "node05"; intervalSecs = 5; diff --git a/nix/test-cluster/node06.nix b/nix/test-cluster/node06.nix index 776397c..5e75bbc 100644 --- a/nix/test-cluster/node06.nix +++ b/nix/test-cluster/node06.nix @@ -66,15 +66,19 @@ services.creditservice = { enable = true; grpcPort = 50089; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; }; services.deployer = { enable = true; bindAddr = "0.0.0.0:8088"; - chainfireEndpoints = [ "http://10.100.0.11:2379" ]; + chainfireEndpoints = [ + "http://10.100.0.11:2379" + "http://10.100.0.12:2379" + "http://10.100.0.13:2379" + ]; clusterId = "test-cluster"; allowUnauthenticated = false; allowUnknownNodes = false; @@ -87,7 +91,7 @@ services.fleet-scheduler = { enable = true; - chainfireEndpoint = "http://10.100.0.11:2379"; + chainfireEndpoint = config.photonTestCluster.chainfireControlPlaneAddrs; clusterId = "test-cluster"; intervalSecs = 10; heartbeatTimeoutSecs = 60; diff --git a/nix/test-cluster/run-cluster.sh b/nix/test-cluster/run-cluster.sh index 8a2bace..7ae6a4f 100755 --- a/nix/test-cluster/run-cluster.sh +++ b/nix/test-cluster/run-cluster.sh @@ -110,8 +110,8 @@ declare -A NODE_UNITS=( [node01]="chainfire flaredb iam prismnet flashdns fiberlb plasmavmc lightningstor coronafs k8shost" [node02]="chainfire flaredb iam" [node03]="chainfire flaredb iam" - [node04]="plasmavmc lightningstor node-agent" - [node05]="plasmavmc lightningstor node-agent" + [node04]="plasmavmc lightningstor coronafs node-agent" + [node05]="plasmavmc lightningstor coronafs node-agent" [node06]="apigateway nightlight creditservice deployer fleet-scheduler" ) @@ -319,6 +319,41 @@ lightningstor_count_triplet() { "$(lightningstor_data_file_count node05)" } +capture_stable_lightningstor_count_triplet() { + local min_node01="${1:-0}" + local min_node04="${2:-0}" + local min_node05="${3:-0}" + local settle_secs="${4:-6}" + local timeout="${5:-${HTTP_WAIT_TIMEOUT}}" + local deadline=$((SECONDS + timeout)) + local stable_since=0 + local last_triplet="" + + while true; do + local count_node01 count_node04 count_node05 triplet + read -r count_node01 count_node04 count_node05 < <(lightningstor_count_triplet) + if (( count_node01 >= min_node01 )) && (( count_node04 >= min_node04 )) && (( count_node05 >= min_node05 )); then + triplet="${count_node01} ${count_node04} ${count_node05}" + if [[ "${triplet}" == "${last_triplet}" ]]; then + if (( stable_since > 0 )) && (( SECONDS - stable_since >= settle_secs )); then + printf '%s\n' "${triplet}" + return 0 + fi + else + last_triplet="${triplet}" + stable_since="${SECONDS}" + fi + else + last_triplet="" + stable_since=0 + fi + if (( SECONDS >= deadline )); then + die "timed out waiting for distributed LightningStor counts to settle: minimum ${min_node01}/${min_node04}/${min_node05}, last ${count_node01:-?}/${count_node04:-?}/${count_node05:-?}" + fi + sleep 2 + done +} + wait_for_lightningstor_counts_greater_than() { local before_node01="$1" local before_node04="$2" @@ -384,6 +419,14 @@ guest_bench_image_link() { printf '%s/build-vm-bench-guest-image' "$(vm_dir)" } +reuse_guest_images_requested() { + [[ "${PHOTON_CLUSTER_REUSE_GUEST_IMAGES:-0}" == "1" ]] +} + +preserve_build_links_requested() { + [[ "${PHOTON_CLUSTER_PRESERVE_BUILD_LINKS:-0}" == "1" ]] +} + runtime_dir() { printf '%s/%s' "$(vm_dir)" "$1" } @@ -415,6 +458,21 @@ guest_bench_image_path() { find -L "${link_path}" -maxdepth 2 -type f -name '*.qcow2' | head -n1 } +prepare_node01_image_source() { + local local_path="$1" + local remote_name="$2" + + if [[ "${local_path}" == /nix/store/* ]] && ssh_node node01 "test -r ${local_path}" >/dev/null 2>&1; then + printf '%s\n' "${local_path}" + return 0 + fi + + ssh_node node01 "install -d -m 0755 /var/lib/plasmavmc/imports" + local remote_path="/var/lib/plasmavmc/imports/${remote_name}.qcow2" + scp_to_node node01 "${local_path}" "${remote_path}" + printf '%s\n' "${remote_path}" +} + all_or_requested_nodes() { if [[ "$#" -eq 0 ]]; then printf '%s\n' "${NODES[@]}" @@ -656,7 +714,11 @@ remove_runtime_state_current_profile() { if [[ -d "${state_dir}" ]]; then log "Removing runtime state under ${state_dir}" - find "${state_dir}" -mindepth 1 -delete 2>/dev/null || true + if preserve_build_links_requested; then + find "${state_dir}" -mindepth 1 ! -name 'build-*' -delete 2>/dev/null || true + else + find "${state_dir}" -mindepth 1 -delete 2>/dev/null || true + fi fi } @@ -726,6 +788,11 @@ build_vms() { build_guest_image() { local out + if reuse_guest_images_requested && [[ -L "$(guest_image_link)" ]] && [[ -e "$(readlink -f "$(guest_image_link)")" ]]; then + log "Reusing cached bootable VM guest image from $(readlink -f "$(guest_image_link)")" + return 0 + fi + log "Building bootable VM guest image on the host" out="$(NIX_BUILD_CORES="${CLUSTER_NIX_BUILD_CORES}" nix build -L \ --max-jobs "${CLUSTER_NIX_MAX_JOBS}" \ @@ -739,6 +806,11 @@ build_guest_image() { build_guest_bench_image() { local out + if reuse_guest_images_requested && [[ -L "$(guest_bench_image_link)" ]] && [[ -e "$(readlink -f "$(guest_bench_image_link)")" ]]; then + log "Reusing cached VM benchmark guest image from $(readlink -f "$(guest_bench_image_link)")" + return 0 + fi + log "Building VM benchmark guest image on the host" out="$(NIX_BUILD_CORES="${CLUSTER_NIX_BUILD_CORES}" nix build -L \ --max-jobs "${CLUSTER_NIX_MAX_JOBS}" \ @@ -1046,6 +1118,47 @@ issue_project_admin_token() { printf '%s\n' "${token}" } +issue_s3_credential() { + local iam_port="$1" + local principal_id="$2" + local org_id="$3" + local project_id="$4" + local description="${5:-storage-bench}" + local create_credential_json access_key_id secret_key deadline output + + create_credential_json="$( + jq -cn \ + --arg principal "${principal_id}" \ + --arg org "${org_id}" \ + --arg project "${project_id}" \ + --arg description "${description}" \ + '{principalId:$principal, principalKind:"PRINCIPAL_KIND_SERVICE_ACCOUNT", orgId:$org, projectId:$project, description:$description}' + )" + + deadline=$((SECONDS + HTTP_WAIT_TIMEOUT + 120)) + while true; do + output="$( + timeout 15 grpcurl -plaintext \ + -import-path "${IAM_PROTO_DIR}" \ + -proto "${IAM_PROTO}" \ + -d "${create_credential_json}" \ + 127.0.0.1:"${iam_port}" iam.v1.IamCredential/CreateS3Credential 2>&1 + )" && { + access_key_id="$(printf '%s\n' "${output}" | jq -r '.accessKeyId // empty' 2>/dev/null || true)" + secret_key="$(printf '%s\n' "${output}" | jq -r '.secretKey // empty' 2>/dev/null || true)" + if [[ -n "${access_key_id}" && -n "${secret_key}" ]]; then + printf '%s\t%s\n' "${access_key_id}" "${secret_key}" + return 0 + fi + } + + if (( SECONDS >= deadline )); then + die "timed out issuing IAM S3 credential for ${principal_id}: ${output}" + fi + sleep 2 + done +} + issue_project_admin_token_any() { local org_id="$1" local project_id="$2" @@ -1479,7 +1592,7 @@ if [[ "${runtime_secs}" != "0" ]]; then fi if [[ "${rw}" == *write* ]]; then - fio_args+=(--fdatasync=1) + fio_args+=(--end_fsync=1) fi result_json="$(fio "${fio_args[@]}")" @@ -1526,7 +1639,7 @@ if [[ "${runtime_secs}" != "0" ]]; then fi if [[ "${rw}" == *write* ]]; then - fio_args+=(--fdatasync=1) + fio_args+=(--end_fsync=1) fi result_json="$(fio "${fio_args[@]}")" @@ -1593,12 +1706,47 @@ coronafs_export_volume_json() { coronafs_api_request "${base_port}" POST "/v1/volumes/${volume_id}/export" } +coronafs_materialize_volume() { + local base_port="$1" + local volume_id="$2" + local source_uri="$3" + local size_bytes="$4" + coronafs_api_request "${base_port}" POST "/v1/volumes/${volume_id}/materialize" \ + "$(jq -cn \ + --arg source_uri "${source_uri}" \ + --argjson size_bytes "${size_bytes}" \ + '{source_uri:$source_uri,size_bytes:$size_bytes}')" +} + coronafs_get_volume_json() { local base_port="$1" local volume_id="$2" coronafs_api_request "${base_port}" GET "/v1/volumes/${volume_id}" } +assert_coronafs_materialized_volume() { + local base_port="$1" + local volume_id="$2" + local json + json="$(coronafs_get_volume_json "${base_port}" "${volume_id}")" + printf '%s' "${json}" | jq -e ' + .node_local == true and + .path != null + ' >/dev/null +} + +coronafs_volume_export_uri() { + local base_port="$1" + local volume_id="$2" + coronafs_get_volume_json "${base_port}" "${volume_id}" | jq -r '.export.uri // empty' +} + +coronafs_volume_qemu_ref() { + local base_port="$1" + local volume_id="$2" + coronafs_get_volume_json "${base_port}" "${volume_id}" | jq -r '.export.uri // .path // empty' +} + coronafs_delete_volume() { local base_port="$1" local volume_id="$2" @@ -1626,12 +1774,26 @@ runtime_secs="$5" nbd_device="$6" iodepth="$7" +resolve_qemu_nbd_aio_mode() { + local requested_mode="${PHOTON_TEST_CLUSTER_NBD_AIO_MODE:-io_uring}" + if qemu-nbd --help 2>&1 | grep -q "io_uring"; then + case "${requested_mode}" in + io_uring|threads|native) + printf '%s\n' "${requested_mode}" + return + ;; + esac + fi + printf 'threads\n' +} + modprobe nbd nbds_max=16 max_part=8 >/dev/null 2>&1 || true qemu-nbd --disconnect "${nbd_device}" >/dev/null 2>&1 || true +aio_mode="$(resolve_qemu_nbd_aio_mode)" qemu-nbd \ --format=raw \ --cache=none \ - --aio=io_uring \ + --aio="${aio_mode}" \ --connect="${nbd_device}" \ "${nbd_uri}" trap 'qemu-nbd --disconnect "${nbd_device}" >/dev/null 2>&1 || true' EXIT @@ -1653,7 +1815,7 @@ if [[ "${runtime_secs}" != "0" ]]; then fi if [[ "${rw}" == *write* ]]; then - fio_args+=(--fdatasync=1) + fio_args+=(--end_fsync=1) fi result_json="$(fio "${fio_args[@]}")" @@ -1679,12 +1841,26 @@ nbd_uri="$1" size_mb="$2" nbd_device="$3" +resolve_qemu_nbd_aio_mode() { + local requested_mode="${PHOTON_TEST_CLUSTER_NBD_AIO_MODE:-io_uring}" + if qemu-nbd --help 2>&1 | grep -q "io_uring"; then + case "${requested_mode}" in + io_uring|threads|native) + printf '%s\n' "${requested_mode}" + return + ;; + esac + fi + printf 'threads\n' +} + modprobe nbd nbds_max=16 max_part=8 >/dev/null 2>&1 || true qemu-nbd --disconnect "${nbd_device}" >/dev/null 2>&1 || true +aio_mode="$(resolve_qemu_nbd_aio_mode)" qemu-nbd \ --format=raw \ --cache=none \ - --aio=io_uring \ + --aio="${aio_mode}" \ --connect="${nbd_device}" \ "${nbd_uri}" trap 'qemu-nbd --disconnect "${nbd_device}" >/dev/null 2>&1 || true' EXIT @@ -1834,6 +2010,24 @@ wait_for_unit() { done } +assert_unit_clean_boot() { + local node="$1" + local unit="$2" + local restart_count + + restart_count="$(ssh_node "${node}" "systemctl show --property=NRestarts --value ${unit}.service" 2>/dev/null || true)" + restart_count="${restart_count:-0}" + if [[ ! "${restart_count}" =~ ^[0-9]+$ ]]; then + ssh_node "${node}" "systemctl status --no-pager ${unit}.service || true" || true + die "could not determine restart count for ${unit}.service on ${node}: ${restart_count}" + fi + if (( restart_count != 0 )); then + ssh_node "${node}" "systemctl status --no-pager ${unit}.service || true" || true + ssh_node "${node}" "journalctl -u ${unit}.service -n 120 --no-pager || true" || true + die "${unit}.service on ${node} restarted ${restart_count} times during boot" + fi +} + wait_for_http() { local node="$1" local url="$2" @@ -2170,13 +2364,27 @@ wait_for_vm_console_pattern() { local pattern="$3" local timeout="${4:-${HTTP_WAIT_TIMEOUT}}" local deadline=$((SECONDS + timeout)) - local console_path console_q pattern_q + local console_path console_q pattern_q prefix prefix_q target_count console_path="$(vm_console_path "${vm_id}")" console_q="$(printf '%q' "${console_path}")" pattern_q="$(printf '%q' "${pattern}")" log "Waiting for VM console output on ${node}: ${pattern}" + if [[ "${pattern}" =~ ^(.*count=)([0-9]+)$ ]]; then + prefix="${BASH_REMATCH[1]}" + target_count="${BASH_REMATCH[2]}" + prefix_q="$(printf '%q' "${prefix}")" + until ssh_node "${node}" "bash -lc 'test -f ${console_q} && awk -v prefix=${prefix_q} -v target=${target_count} '\''index(\$0, prefix) { if (match(\$0, /count=([0-9]+)/, m) && (m[1] + 0) >= target) found = 1 } END { exit(found ? 0 : 1) }'\'' ${console_q}'" >/dev/null 2>&1; do + if (( SECONDS >= deadline )); then + ssh_node "${node}" "bash -lc 'test -f ${console_q} && tail -n 80 ${console_q} || true'" || true + die "timed out waiting for VM console pattern ${pattern} on ${node}" + fi + sleep 2 + done + return 0 + fi + until ssh_node "${node}" "bash -lc 'test -f ${console_q} && grep -F -- ${pattern_q} ${console_q} >/dev/null'" >/dev/null 2>&1; do if (( SECONDS >= deadline )); then ssh_node "${node}" "bash -lc 'test -f ${console_q} && tail -n 80 ${console_q} || true'" || true @@ -2200,14 +2408,23 @@ read_vm_console_line_matching() { wait_for_qemu_volume_present() { local node="$1" - local volume_path="$2" - local timeout="${3:-${HTTP_WAIT_TIMEOUT}}" + local volume_ref="$2" + local alternate_ref="${3:-}" + local timeout="${4:-${HTTP_WAIT_TIMEOUT}}" local deadline=$((SECONDS + timeout)) + local qemu_processes - until ssh_node "${node}" "pgrep -fa '[q]emu-system' | grep -F '${volume_path}' >/dev/null" >/dev/null 2>&1; do + while true; do + qemu_processes="$(ssh_node "${node}" "pgrep -fa '[q]emu-system' || true" 2>/dev/null || true)" + if [[ "${qemu_processes}" == *"${volume_ref}"* ]]; then + return 0 + fi + if [[ -n "${alternate_ref}" && "${qemu_processes}" == *"${alternate_ref}"* ]]; then + return 0 + fi if (( SECONDS >= deadline )); then - ssh_node "${node}" "pgrep -fa '[q]emu-system' || true" || true - die "timed out waiting for qemu to attach ${volume_path} on ${node}" + printf '%s\n' "${qemu_processes}" >&2 + die "timed out waiting for qemu to attach ${volume_ref}${alternate_ref:+ or ${alternate_ref}} on ${node}" fi sleep 2 done @@ -2215,14 +2432,20 @@ wait_for_qemu_volume_present() { wait_for_qemu_volume_absent() { local node="$1" - local volume_path="$2" - local timeout="${3:-${HTTP_WAIT_TIMEOUT}}" + local volume_ref="$2" + local alternate_ref="${3:-}" + local timeout="${4:-${HTTP_WAIT_TIMEOUT}}" local deadline=$((SECONDS + timeout)) + local qemu_processes - until ssh_node "${node}" "bash -lc '! pgrep -fa \"[q]emu-system\" | grep -F \"${volume_path}\" >/dev/null'" >/dev/null 2>&1; do + while true; do + qemu_processes="$(ssh_node "${node}" "pgrep -fa '[q]emu-system' || true" 2>/dev/null || true)" + if [[ "${qemu_processes}" != *"${volume_ref}"* ]] && [[ -z "${alternate_ref}" || "${qemu_processes}" != *"${alternate_ref}"* ]]; then + return 0 + fi if (( SECONDS >= deadline )); then - ssh_node "${node}" "pgrep -fa '[q]emu-system' || true" || true - die "timed out waiting for qemu to release ${volume_path} on ${node}" + printf '%s\n' "${qemu_processes}" >&2 + die "timed out waiting for qemu to release ${volume_ref}${alternate_ref:+ or ${alternate_ref}} on ${node}" fi sleep 2 done @@ -2231,13 +2454,27 @@ wait_for_qemu_volume_absent() { try_get_vm_json() { local token="$1" local get_vm_json="$2" + local vm_port="${3:-15082}" grpcurl -plaintext \ -H "authorization: Bearer ${token}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "${get_vm_json}" \ - 127.0.0.1:15082 plasmavmc.v1.VmService/GetVm + 127.0.0.1:${vm_port} plasmavmc.v1.VmService/GetVm +} + +try_get_volume_json() { + local token="$1" + local get_volume_json="$2" + local vm_port="${3:-15082}" + + grpcurl -plaintext \ + -H "authorization: Bearer ${token}" \ + -import-path "${PLASMAVMC_PROTO_DIR}" \ + -proto "${PLASMAVMC_PROTO}" \ + -d "${get_volume_json}" \ + 127.0.0.1:${vm_port} plasmavmc.v1.VolumeService/GetVolume } wait_requested() { @@ -2338,11 +2575,15 @@ validate_storage_units() { for unit in plasmavmc lightningstor coronafs; do wait_for_unit node01 "${unit}" done + assert_unit_clean_boot node01 plasmavmc + assert_unit_clean_boot node01 lightningstor for node in node04 node05; do for unit in ${NODE_UNITS[${node}]}; do wait_for_unit "${node}" "${unit}" done + assert_unit_clean_boot "${node}" plasmavmc + assert_unit_clean_boot "${node}" lightningstor done } @@ -2354,6 +2595,84 @@ validate_storage_control_plane() { wait_for_http node01 "http://127.0.0.1:${CORONAFS_API_PORT}/healthz" wait_for_tcp_port node01 50086 wait_for_tcp_port node01 9000 + ssh_node_script node01 <<'EOS' +set -euo pipefail +capabilities="$(curl -fsS http://127.0.0.1:50088/v1/capabilities)" +printf '%s' "${capabilities}" | grep -q '"mode":"combined"' +printf '%s' "${capabilities}" | grep -q '"supports_node_api":true' +printf '%s' "${capabilities}" | grep -q '"supports_controller_api":true' +EOS + wait_for_http node04 "http://127.0.0.1:${CORONAFS_API_PORT}/healthz" + wait_for_http node05 "http://127.0.0.1:${CORONAFS_API_PORT}/healthz" + ssh_node_script node04 <<'EOS' +set -euo pipefail +capabilities="$(curl -fsS http://127.0.0.1:50088/v1/capabilities)" +printf '%s' "${capabilities}" | grep -q '"mode":"node"' +printf '%s' "${capabilities}" | grep -q '"supports_node_api":true' +printf '%s' "${capabilities}" | grep -q '"supports_controller_api":false' +EOS + ssh_node_script node05 <<'EOS' +set -euo pipefail +capabilities="$(curl -fsS http://127.0.0.1:50088/v1/capabilities)" +printf '%s' "${capabilities}" | grep -q '"mode":"node"' +printf '%s' "${capabilities}" | grep -q '"supports_node_api":true' +printf '%s' "${capabilities}" | grep -q '"supports_controller_api":false' +EOS + ssh_node_script node01 <<'EOS' +set -euo pipefail +volume="coronafs-chainfire-smoke-$(date +%s)" +prefix="/coronafs/test-cluster/storage/volumes" +cleanup() { + curl -fsS -X DELETE "http://127.0.0.1:50088/v1/volumes/${volume}" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +create_response="$(curl -fsS -X PUT \ + -H 'content-type: application/json' \ + -d '{"size_bytes":67108864}' \ + "http://127.0.0.1:50088/v1/volumes/${volume}")" +printf '%s' "${create_response}" | jq -e --arg id "${volume}" '.id == $id' >/dev/null + +export_response="$(curl -fsS -X POST "http://127.0.0.1:50088/v1/volumes/${volume}/export")" +printf '%s' "${export_response}" | jq -e '.export.uri != null and .export.port != null and .export.read_only == false' >/dev/null + +readonly_response="$(curl -fsS -X POST "http://127.0.0.1:50088/v1/volumes/${volume}/export?read_only=true")" +printf '%s' "${readonly_response}" | jq -e '.export.uri != null and .export.port != null and .export.read_only == true' >/dev/null + +rewritable_response="$(curl -fsS -X POST "http://127.0.0.1:50088/v1/volumes/${volume}/export")" +printf '%s' "${rewritable_response}" | jq -e '.export.uri != null and .export.port != null and .export.read_only == false' >/dev/null + +curl -fsS --get http://127.0.0.1:8081/api/v1/kv \ + --data-urlencode "prefix=${prefix}/${volume}" \ + | jq -e --arg key "${prefix}/${volume}" --arg id "${volume}" ' + .data.items | length == 1 and + .[0].key == $key and + ((.[0].value | fromjson).id == $id) + ' >/dev/null + +systemctl restart coronafs +for _ in $(seq 1 30); do + if curl -fsS http://127.0.0.1:50088/healthz >/dev/null 2>&1; then + break + fi + sleep 1 +done +curl -fsS http://127.0.0.1:50088/healthz >/dev/null + +after_restart="$(curl -fsS "http://127.0.0.1:50088/v1/volumes/${volume}")" +printf '%s' "${after_restart}" | jq -e --arg id "${volume}" '.id == $id and (.export == null)' >/dev/null + +reexport_response="$(curl -fsS -X POST "http://127.0.0.1:50088/v1/volumes/${volume}/export")" +printf '%s' "${reexport_response}" | jq -e '.export.uri != null and .export.port != null and .export.read_only == false' >/dev/null + +curl -fsS -X DELETE "http://127.0.0.1:50088/v1/volumes/${volume}" >/dev/null +if curl -fsS "http://127.0.0.1:8081/api/v1/kv/${prefix#"/"}/${volume}" >/tmp/coronafs-chainfire-delete.out 2>/dev/null; then + echo "ChainFire metadata still exists for deleted CoronaFS volume ${volume}" >&2 + cat /tmp/coronafs-chainfire-delete.out >&2 || true + exit 1 +fi +trap - EXIT +EOS wait_for_http node02 http://127.0.0.1:8081/health wait_for_http node02 http://127.0.0.1:8082/health wait_for_http node02 http://127.0.0.1:8083/health @@ -3071,17 +3390,28 @@ validate_workers() { wait_for_http node01 "http://127.0.0.1:${CORONAFS_API_PORT}/healthz" log "Validating CoronaFS block export accessibility on worker nodes" - local coronafs_tunnel="" probe_volume="worker-probe-$(date +%s)" + local coronafs_tunnel="" worker_coronafs_tunnel="" probe_volume="" worker_probe_volume="" + probe_volume="worker-probe-$(date +%s)" + worker_probe_volume="${probe_volume}-node04" coronafs_tunnel="$(start_ssh_tunnel node01 15088 "${CORONAFS_API_PORT}")" - trap 'stop_ssh_tunnel node01 "${coronafs_tunnel}"' RETURN + worker_coronafs_tunnel="$(start_ssh_tunnel node04 25088 "${CORONAFS_API_PORT}")" + trap 'coronafs_delete_volume 25088 "'"${worker_probe_volume}"'" >/dev/null 2>&1 || true; coronafs_delete_volume 15088 "'"${probe_volume}"'" >/dev/null 2>&1 || true; stop_ssh_tunnel node04 "'"${worker_coronafs_tunnel}"'"; stop_ssh_tunnel node01 "'"${coronafs_tunnel}"'"' RETURN coronafs_create_volume 15088 "${probe_volume}" $((64 * 1024 * 1024)) >/dev/null - local probe_export_json probe_uri + local probe_export_json probe_uri worker_export_json worker_probe_uri probe_export_json="$(coronafs_export_volume_json 15088 "${probe_volume}")" probe_uri="$(printf '%s' "${probe_export_json}" | jq -r '.export.uri')" [[ -n "${probe_uri}" && "${probe_uri}" != "null" ]] || die "CoronaFS probe volume did not return an export URI" run_remote_nbd_fio_json node04 "${probe_uri}" write 1M 32 >/dev/null run_remote_nbd_dd_read_json node05 "${probe_uri}" 32 >/dev/null + coronafs_materialize_volume 25088 "${worker_probe_volume}" "${probe_uri}" $((64 * 1024 * 1024)) >/dev/null + worker_export_json="$(coronafs_export_volume_json 25088 "${worker_probe_volume}")" + worker_probe_uri="$(printf '%s' "${worker_export_json}" | jq -r '.export.uri')" + [[ -n "${worker_probe_uri}" && "${worker_probe_uri}" != "null" ]] || die "worker-local CoronaFS materialization did not return an export URI" + run_remote_nbd_fio_json node04 "${worker_probe_uri}" read 1M 32 >/dev/null + run_remote_nbd_dd_read_json node05 "${worker_probe_uri}" 32 >/dev/null + coronafs_delete_volume 25088 "${worker_probe_volume}" coronafs_delete_volume 15088 "${probe_volume}" + stop_ssh_tunnel node04 "${worker_coronafs_tunnel}" stop_ssh_tunnel node01 "${coronafs_tunnel}" trap - RETURN } @@ -3206,19 +3536,26 @@ validate_lightningstor_distributed_storage() { } validate_vm_storage_flow() { - log "Validating PlasmaVMC image import, shared-volume execution, and live migration" + log "Validating PlasmaVMC image import, shared-volume execution, and cross-node migration" local iam_tunnel="" ls_tunnel="" vm_tunnel="" coronafs_tunnel="" + local node04_coronafs_tunnel="" node05_coronafs_tunnel="" + local current_worker_coronafs_port="" peer_worker_coronafs_port="" + local vm_port=15082 iam_tunnel="$(start_ssh_tunnel node01 15080 50080)" ls_tunnel="$(start_ssh_tunnel node01 15086 50086)" - vm_tunnel="$(start_ssh_tunnel node01 15082 50082)" + vm_tunnel="$(start_ssh_tunnel node01 "${vm_port}" 50082)" coronafs_tunnel="$(start_ssh_tunnel node01 15088 "${CORONAFS_API_PORT}")" + node04_coronafs_tunnel="$(start_ssh_tunnel node04 25088 "${CORONAFS_API_PORT}")" + node05_coronafs_tunnel="$(start_ssh_tunnel node05 35088 "${CORONAFS_API_PORT}")" local image_source_path="" local node01_proto_root="/var/lib/plasmavmc/test-protos" cleanup_vm_storage_flow() { - if [[ -n "${image_source_path}" ]]; then + if [[ -n "${image_source_path}" && "${image_source_path}" != /nix/store/* ]]; then ssh_node node01 "rm -f ${image_source_path}" >/dev/null 2>&1 || true fi + stop_ssh_tunnel node05 "${node05_coronafs_tunnel}" + stop_ssh_tunnel node04 "${node04_coronafs_tunnel}" stop_ssh_tunnel node01 "${coronafs_tunnel}" stop_ssh_tunnel node01 "${vm_tunnel}" stop_ssh_tunnel node01 "${ls_tunnel}" @@ -3247,14 +3584,14 @@ validate_vm_storage_flow() { [[ -n "${guest_image_local_path}" ]] || die "failed to locate bootable VM guest image" guest_image_sha="$(sha256sum "${guest_image_local_path}" | awk '{print $1}')" guest_image_size="$(stat -c %s "${guest_image_local_path}")" - ssh_node node01 "install -d -m 0755 /var/lib/plasmavmc/imports" ssh_node node01 "install -d -m 0755 ${node01_proto_root}/iam ${node01_proto_root}/plasmavmc ${node01_proto_root}/lightningstor" scp_to_node node01 "${IAM_PROTO}" "${node01_proto_root}/iam/iam.proto" scp_to_node node01 "${PLASMAVMC_PROTO}" "${node01_proto_root}/plasmavmc/plasmavmc.proto" scp_to_node node01 "${LIGHTNINGSTOR_PROTO}" "${node01_proto_root}/lightningstor/lightningstor.proto" - ssh_node node01 "find /var/lib/plasmavmc/imports -maxdepth 1 -type f -name 'vm-image-*.qcow2' -delete" - image_source_path="/var/lib/plasmavmc/imports/${image_name}.qcow2" - scp_to_node node01 "${guest_image_local_path}" "${image_source_path}" + if [[ "${guest_image_local_path}" != /nix/store/* ]]; then + ssh_node node01 "install -d -m 0755 /var/lib/plasmavmc/imports && find /var/lib/plasmavmc/imports -maxdepth 1 -type f -name 'vm-image-*.qcow2' -delete" + fi + image_source_path="$(prepare_node01_image_source "${guest_image_local_path}" "${image_name}")" remote_guest_image_sha="$(ssh_node node01 "sha256sum ${image_source_path} | awk '{print \$1}'")" [[ "${remote_guest_image_sha}" == "${guest_image_sha}" ]] || die "bootable VM guest image checksum mismatch after host distribution" @@ -3339,7 +3676,12 @@ EOS ssh_node node01 "rm -f ${image_source_path}" image_source_path="" wait_for_lightningstor_counts_greater_than "${image_before_node01}" "${image_before_node04}" "${image_before_node05}" "PlasmaVMC image import" - read -r image_after_node01 image_after_node04 image_after_node05 < <(lightningstor_count_triplet) + read -r image_after_node01 image_after_node04 image_after_node05 < <( + capture_stable_lightningstor_count_triplet \ + "$((image_before_node01 + 1))" \ + "$((image_before_node04 + 1))" \ + "$((image_before_node05 + 1))" + ) local create_vm_rest_json create_vm_rest_json="$( @@ -3392,7 +3734,7 @@ EOS source:{imageId:$image_id}, sizeGib:4, bus:"DISK_BUS_VIRTIO", - cache:"DISK_CACHE_NONE", + cache:"DISK_CACHE_WRITEBACK", bootIndex:1 }, { @@ -3400,7 +3742,7 @@ EOS source:{blank:true}, sizeGib:2, bus:"DISK_BUS_VIRTIO", - cache:"DISK_CACHE_NONE" + cache:"DISK_CACHE_WRITEBACK" } ] } @@ -3434,7 +3776,7 @@ EOS local peer_node="" while true; do local vm_json - if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" 2>/dev/null)"; then + if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" "${vm_port}" 2>/dev/null)"; then if (( SECONDS >= deadline )); then die "timed out waiting for VM ${vm_id} to be scheduled onto a worker" fi @@ -3452,8 +3794,12 @@ EOS done if [[ "${node_id}" == "node04" ]]; then peer_node="node05" + current_worker_coronafs_port=25088 + peer_worker_coronafs_port=35088 else peer_node="node04" + current_worker_coronafs_port=35088 + peer_worker_coronafs_port=25088 fi grpcurl -plaintext \ @@ -3466,7 +3812,7 @@ EOS deadline=$((SECONDS + HTTP_WAIT_TIMEOUT)) while true; do local vm_json - if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" 2>/dev/null)"; then + if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" "${vm_port}" 2>/dev/null)"; then if (( SECONDS >= deadline )); then die "timed out waiting for VM ${vm_id} to reach RUNNING" fi @@ -3488,6 +3834,7 @@ EOS local volume_path="${CORONAFS_VOLUME_ROOT}/${volume_id}.raw" local data_volume_path="${CORONAFS_VOLUME_ROOT}/${data_volume_id}.raw" local volume_export_json data_volume_export_json volume_uri data_volume_uri + local current_volume_qemu_ref current_data_volume_qemu_ref volume_export_json="$(coronafs_export_volume_json 15088 "${volume_id}")" data_volume_export_json="$(coronafs_export_volume_json 15088 "${data_volume_id}")" volume_uri="$(printf '%s' "${volume_export_json}" | jq -r '.export.uri')" @@ -3496,11 +3843,52 @@ EOS [[ -n "${data_volume_uri}" && "${data_volume_uri}" != "null" ]] || die "CoronaFS data volume export URI missing" ssh_node node01 "test -f ${volume_path}" ssh_node node01 "test -f ${data_volume_path}" - wait_for_qemu_volume_present "${node_id}" "${volume_uri}" - wait_for_qemu_volume_present "${node_id}" "${data_volume_uri}" + assert_coronafs_materialized_volume "${current_worker_coronafs_port}" "${volume_id}" + assert_coronafs_materialized_volume "${current_worker_coronafs_port}" "${data_volume_id}" + if coronafs_get_volume_json "${peer_worker_coronafs_port}" "${volume_id}" >/dev/null 2>&1; then + die "peer worker ${peer_node} unexpectedly materialized mutable root volume ${volume_id}" + fi + if coronafs_get_volume_json "${peer_worker_coronafs_port}" "${data_volume_id}" >/dev/null 2>&1; then + die "peer worker ${peer_node} unexpectedly materialized mutable data volume ${data_volume_id}" + fi + ssh_node "${node_id}" "test -f ${volume_path}" + ssh_node "${node_id}" "test -f ${data_volume_path}" + current_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${volume_id}")" + current_data_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${data_volume_id}")" + [[ -n "${current_volume_qemu_ref}" ]] || die "worker ${node_id} did not expose an attachable local ref for ${volume_id}" + [[ -n "${current_data_volume_qemu_ref}" ]] || die "worker ${node_id} did not expose an attachable local ref for ${data_volume_id}" + wait_for_qemu_volume_present "${node_id}" "${volume_path}" "${current_volume_qemu_ref}" + wait_for_qemu_volume_present "${node_id}" "${data_volume_path}" "${current_data_volume_qemu_ref}" wait_for_lightningstor_counts_equal "${image_after_node01}" "${image_after_node04}" "${image_after_node05}" "shared-fs VM startup" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_READY count=1" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_DATA_READY count=1" + local get_root_volume_json get_data_volume_json + local root_volume_state_json data_volume_state_json + local root_attachment_generation data_attachment_generation + get_root_volume_json="$( + jq -cn \ + --arg org "${org_id}" \ + --arg project "${project_id}" \ + --arg volume "${volume_id}" \ + '{orgId:$org, projectId:$project, volumeId:$volume}' + )" + get_data_volume_json="$( + jq -cn \ + --arg org "${org_id}" \ + --arg project "${project_id}" \ + --arg volume "${data_volume_id}" \ + '{orgId:$org, projectId:$project, volumeId:$volume}' + )" + root_volume_state_json="$(try_get_volume_json "${token}" "${get_root_volume_json}")" + data_volume_state_json="$(try_get_volume_json "${token}" "${get_data_volume_json}")" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachedToVm // empty')" == "${vm_id}" ]] || die "root volume ${volume_id} is not attached to VM ${vm_id}" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "root volume ${volume_id} is not owned by node ${node_id}" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachedToVm // empty')" == "${vm_id}" ]] || die "data volume ${data_volume_id} is not attached to VM ${vm_id}" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "data volume ${data_volume_id} is not owned by node ${node_id}" + root_attachment_generation="$(printf '%s' "${root_volume_state_json}" | jq -r '.attachmentGeneration // 0')" + data_attachment_generation="$(printf '%s' "${data_volume_state_json}" | jq -r '.attachmentGeneration // 0')" + (( root_attachment_generation >= 1 )) || die "root volume ${volume_id} did not report a positive attachment generation" + (( data_attachment_generation >= 1 )) || die "data volume ${data_volume_id} did not report a positive attachment generation" log "Matrix case: PlasmaVMC + CoronaFS + LightningStor" grpcurl -plaintext \ @@ -3557,12 +3945,38 @@ EOS done if [[ "${node_id}" == "node04" ]]; then peer_node="node05" + current_worker_coronafs_port=25088 + peer_worker_coronafs_port=35088 else peer_node="node04" + current_worker_coronafs_port=35088 + peer_worker_coronafs_port=25088 + fi + assert_coronafs_materialized_volume "${current_worker_coronafs_port}" "${volume_id}" + assert_coronafs_materialized_volume "${current_worker_coronafs_port}" "${data_volume_id}" + current_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${volume_id}")" + current_data_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${data_volume_id}")" + [[ -n "${current_volume_qemu_ref}" ]] || die "worker ${node_id} did not republish an attachable local ref for ${volume_id} after restart" + [[ -n "${current_data_volume_qemu_ref}" ]] || die "worker ${node_id} did not republish an attachable local ref for ${data_volume_id} after restart" + wait_for_qemu_volume_present "${node_id}" "${volume_path}" "${current_volume_qemu_ref}" + wait_for_qemu_volume_present "${node_id}" "${data_volume_path}" "${current_data_volume_qemu_ref}" + if coronafs_get_volume_json "${peer_worker_coronafs_port}" "${volume_id}" >/dev/null 2>&1; then + die "peer worker ${peer_node} unexpectedly materialized mutable root volume ${volume_id} after restart" + fi + if coronafs_get_volume_json "${peer_worker_coronafs_port}" "${data_volume_id}" >/dev/null 2>&1; then + die "peer worker ${peer_node} unexpectedly materialized mutable data volume ${data_volume_id} after restart" fi wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_READY count=2" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_DATA_READY count=2" wait_for_lightningstor_counts_equal "${image_after_node01}" "${image_after_node04}" "${image_after_node05}" "shared-fs VM restart" + root_volume_state_json="$(try_get_volume_json "${token}" "${get_root_volume_json}")" + data_volume_state_json="$(try_get_volume_json "${token}" "${get_data_volume_json}")" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "root volume ${volume_id} drifted away from node ${node_id} after restart" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "data volume ${data_volume_id} drifted away from node ${node_id} after restart" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachmentGeneration // 0')" == "${root_attachment_generation}" ]] || die "root volume ${volume_id} attachment generation changed across same-node restart" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachmentGeneration // 0')" == "${data_attachment_generation}" ]] || die "data volume ${data_volume_id} attachment generation changed across same-node restart" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0')" == "${root_attachment_generation}" ]] || die "root volume ${volume_id} was not flushed before same-node restart" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0')" == "${data_attachment_generation}" ]] || die "data volume ${data_volume_id} was not flushed before same-node restart" local migrate_vm_json migrate_vm_json="$( @@ -3589,12 +4003,15 @@ EOS local source_node="${node_id}" local destination_node="${peer_node}" + local source_worker_coronafs_port="${current_worker_coronafs_port}" + local source_volume_qemu_ref="${current_volume_qemu_ref}" + local source_data_volume_qemu_ref="${current_data_volume_qemu_ref}" deadline=$((SECONDS + HTTP_WAIT_TIMEOUT)) while true; do local vm_json if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" 2>/dev/null)"; then if (( SECONDS >= deadline )); then - die "timed out waiting for VM ${vm_id} live migration to ${destination_node}" + die "timed out waiting for VM ${vm_id} migration to ${destination_node}" fi sleep 2 continue @@ -3603,16 +4020,50 @@ EOS break fi if (( SECONDS >= deadline )); then - die "timed out waiting for VM ${vm_id} live migration to ${destination_node}" + die "timed out waiting for VM ${vm_id} migration to ${destination_node}" fi sleep 2 done node_id="${destination_node}" - wait_for_qemu_volume_present "${node_id}" "${volume_uri}" - wait_for_qemu_volume_present "${node_id}" "${data_volume_uri}" - wait_for_qemu_volume_absent "${source_node}" "${volume_uri}" - wait_for_qemu_volume_absent "${source_node}" "${data_volume_uri}" + if [[ "${node_id}" == "node04" ]]; then + current_worker_coronafs_port=25088 + peer_worker_coronafs_port=35088 + else + current_worker_coronafs_port=35088 + peer_worker_coronafs_port=25088 + fi + assert_coronafs_materialized_volume "${current_worker_coronafs_port}" "${volume_id}" + assert_coronafs_materialized_volume "${current_worker_coronafs_port}" "${data_volume_id}" + current_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${volume_id}")" + current_data_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${data_volume_id}")" + [[ -n "${current_volume_qemu_ref}" ]] || die "destination worker ${node_id} did not expose an attachable local ref for ${volume_id}" + [[ -n "${current_data_volume_qemu_ref}" ]] || die "destination worker ${node_id} did not expose an attachable local ref for ${data_volume_id}" + if coronafs_get_volume_json "${source_worker_coronafs_port}" "${volume_id}" >/dev/null 2>&1; then + die "source worker ${source_node} unexpectedly retained mutable root volume ${volume_id} after migration" + fi + if coronafs_get_volume_json "${source_worker_coronafs_port}" "${data_volume_id}" >/dev/null 2>&1; then + die "source worker ${source_node} unexpectedly retained mutable data volume ${data_volume_id} after migration" + fi + ssh_node "${node_id}" "test -f ${volume_path}" + ssh_node "${node_id}" "test -f ${data_volume_path}" + wait_for_qemu_volume_present "${node_id}" "${volume_path}" "${current_volume_qemu_ref}" + wait_for_qemu_volume_present "${node_id}" "${data_volume_path}" "${current_data_volume_qemu_ref}" + wait_for_qemu_volume_absent "${source_node}" "${volume_path}" "${source_volume_qemu_ref}" + wait_for_qemu_volume_absent "${source_node}" "${data_volume_path}" "${source_data_volume_qemu_ref}" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_HEARTBEAT count=2" + root_volume_state_json="$(try_get_volume_json "${token}" "${get_root_volume_json}")" + data_volume_state_json="$(try_get_volume_json "${token}" "${get_data_volume_json}")" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "root volume ${volume_id} is not owned by migrated node ${node_id}" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "data volume ${data_volume_id} is not owned by migrated node ${node_id}" + local next_root_attachment_generation next_data_attachment_generation + next_root_attachment_generation="$(printf '%s' "${root_volume_state_json}" | jq -r '.attachmentGeneration // 0')" + next_data_attachment_generation="$(printf '%s' "${data_volume_state_json}" | jq -r '.attachmentGeneration // 0')" + (( next_root_attachment_generation > root_attachment_generation )) || die "root volume ${volume_id} attachment generation did not advance after migration" + (( next_data_attachment_generation > data_attachment_generation )) || die "data volume ${data_volume_id} attachment generation did not advance after migration" + (( $(printf '%s' "${root_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0') < next_root_attachment_generation )) || die "root volume ${volume_id} unexpectedly reported destination flush before post-migration stop" + (( $(printf '%s' "${data_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0') < next_data_attachment_generation )) || die "data volume ${data_volume_id} unexpectedly reported destination flush before post-migration stop" + root_attachment_generation="${next_root_attachment_generation}" + data_attachment_generation="${next_data_attachment_generation}" grpcurl -plaintext \ -H "authorization: Bearer ${token}" \ @@ -3626,7 +4077,7 @@ EOS local vm_json if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" 2>/dev/null)"; then if (( SECONDS >= deadline )); then - die "timed out waiting for VM ${vm_id} to stop after live migration" + die "timed out waiting for VM ${vm_id} to stop after migration" fi sleep 2 continue @@ -3635,7 +4086,7 @@ EOS break fi if (( SECONDS >= deadline )); then - die "timed out waiting for VM ${vm_id} to stop after live migration" + die "timed out waiting for VM ${vm_id} to stop after migration" fi sleep 2 done @@ -3666,11 +4117,31 @@ EOS sleep 2 done - wait_for_qemu_volume_present "${node_id}" "${volume_uri}" - wait_for_qemu_volume_present "${node_id}" "${data_volume_uri}" + coronafs_get_volume_json "${current_worker_coronafs_port}" "${volume_id}" >/dev/null + coronafs_get_volume_json "${current_worker_coronafs_port}" "${data_volume_id}" >/dev/null + if coronafs_get_volume_json "${peer_worker_coronafs_port}" "${volume_id}" >/dev/null 2>&1; then + die "peer worker ${peer_node} unexpectedly materialized mutable root volume ${volume_id} after post-migration restart" + fi + if coronafs_get_volume_json "${peer_worker_coronafs_port}" "${data_volume_id}" >/dev/null 2>&1; then + die "peer worker ${peer_node} unexpectedly materialized mutable data volume ${data_volume_id} after post-migration restart" + fi + current_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${volume_id}")" + current_data_volume_qemu_ref="$(coronafs_volume_qemu_ref "${current_worker_coronafs_port}" "${data_volume_id}")" + [[ -n "${current_volume_qemu_ref}" ]] || die "worker ${node_id} did not republish an attachable local ref for ${volume_id} after post-migration restart" + [[ -n "${current_data_volume_qemu_ref}" ]] || die "worker ${node_id} did not republish an attachable local ref for ${data_volume_id} after post-migration restart" + wait_for_qemu_volume_present "${node_id}" "${volume_path}" "${current_volume_qemu_ref}" + wait_for_qemu_volume_present "${node_id}" "${data_volume_path}" "${current_data_volume_qemu_ref}" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_READY count=3" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_SMOKE_DATA_READY count=3" wait_for_lightningstor_counts_equal "${image_after_node01}" "${image_after_node04}" "${image_after_node05}" "shared-fs VM post-migration restart" + root_volume_state_json="$(try_get_volume_json "${token}" "${get_root_volume_json}")" + data_volume_state_json="$(try_get_volume_json "${token}" "${get_data_volume_json}")" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "root volume ${volume_id} drifted away from migrated node ${node_id} after restart" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachedToNode // empty')" == "${node_id}" ]] || die "data volume ${data_volume_id} drifted away from migrated node ${node_id} after restart" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.attachmentGeneration // 0')" == "${root_attachment_generation}" ]] || die "root volume ${volume_id} attachment generation changed across migrated-node restart" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.attachmentGeneration // 0')" == "${data_attachment_generation}" ]] || die "data volume ${data_volume_id} attachment generation changed across migrated-node restart" + [[ "$(printf '%s' "${root_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0')" == "${root_attachment_generation}" ]] || die "root volume ${volume_id} was not flushed before migrated-node restart" + [[ "$(printf '%s' "${data_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0')" == "${data_attachment_generation}" ]] || die "data volume ${data_volume_id} was not flushed before migrated-node restart" grpcurl -plaintext \ -H "authorization: Bearer ${token}" \ @@ -3730,6 +4201,18 @@ EOS if coronafs_get_volume_json 15088 "${data_volume_id}" >/dev/null 2>&1; then die "CoronaFS data volume metadata still exists after VM deletion" fi + if coronafs_get_volume_json 25088 "${volume_id}" >/dev/null 2>&1; then + die "worker node04 retained mutable root volume metadata after VM deletion" + fi + if coronafs_get_volume_json 25088 "${data_volume_id}" >/dev/null 2>&1; then + die "worker node04 retained mutable data volume metadata after VM deletion" + fi + if coronafs_get_volume_json 35088 "${volume_id}" >/dev/null 2>&1; then + die "worker node05 retained mutable root volume metadata after VM deletion" + fi + if coronafs_get_volume_json 35088 "${data_volume_id}" >/dev/null 2>&1; then + die "worker node05 retained mutable data volume metadata after VM deletion" + fi wait_for_lightningstor_counts_equal "${image_after_node01}" "${image_after_node04}" "${image_after_node05}" "shared-fs VM deletion" grpcurl -plaintext \ @@ -4952,20 +5435,33 @@ validate_component_matrix() { } benchmark_coronafs_performance() { - log "Benchmarking CoronaFS NBD-backed volume throughput against local worker disk" + log "Benchmarking CoronaFS controller-export and node-local volume throughput against local worker disk" local local_write_json local_read_json local_rand_json - local coronafs_write_json coronafs_read_json coronafs_rand_json + local coronafs_controller_write_json coronafs_controller_read_json coronafs_controller_rand_json local local_depth_write_json local_depth_read_json - local coronafs_depth_write_json coronafs_depth_read_json - local cross_worker_read_json - local coronafs_tunnel="" bench_volume="coronafs-bench-$(date +%s)" - local coronafs_export_json coronafs_uri + local coronafs_controller_depth_write_json coronafs_controller_depth_read_json + local coronafs_local_write_json coronafs_local_read_json coronafs_local_rand_json + local coronafs_local_depth_write_json coronafs_local_depth_read_json + local coronafs_target_local_read_json + local coronafs_controller_tunnel="" coronafs_node04_tunnel="" coronafs_node05_tunnel="" + local bench_volume="coronafs-bench-$(date +%s)" + local node04_local_volume="${bench_volume}-node04-local" + local node05_local_volume="${bench_volume}-node05-local" + local coronafs_export_json coronafs_uri node04_export_json node04_local_uri node05_export_json node05_local_uri + local node04_materialize_ns_start node04_materialize_ns_end node05_materialize_ns_start node05_materialize_ns_end + local node04_materialize_sec node05_materialize_sec - coronafs_tunnel="$(start_ssh_tunnel node01 15088 "${CORONAFS_API_PORT}")" + coronafs_controller_tunnel="$(start_ssh_tunnel node01 15088 "${CORONAFS_API_PORT}")" + coronafs_node04_tunnel="$(start_ssh_tunnel node04 25088 "${CORONAFS_API_PORT}")" + coronafs_node05_tunnel="$(start_ssh_tunnel node05 26088 "${CORONAFS_API_PORT}")" cleanup_coronafs_bench() { + coronafs_delete_volume 25088 "${node04_local_volume}" >/dev/null 2>&1 || true + coronafs_delete_volume 26088 "${node05_local_volume}" >/dev/null 2>&1 || true coronafs_delete_volume 15088 "${bench_volume}" >/dev/null 2>&1 || true - stop_ssh_tunnel node01 "${coronafs_tunnel}" + stop_ssh_tunnel node05 "${coronafs_node05_tunnel}" + stop_ssh_tunnel node04 "${coronafs_node04_tunnel}" + stop_ssh_tunnel node01 "${coronafs_controller_tunnel}" } trap cleanup_coronafs_bench RETURN @@ -4979,46 +5475,77 @@ benchmark_coronafs_performance() { local_rand_json="$(run_remote_fio_json node04 /var/tmp/photon-bench/local-randread.dat randread 4k 128 10)" local_rand_depth_json="$(run_remote_fio_json node04 /var/tmp/photon-bench/local-randread-depth.dat randread 4k 512 15 32 libaio)" - coronafs_write_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" write 1M 256)" - coronafs_read_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" read 1M 256)" - coronafs_rand_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" randread 4k 128 10)" - coronafs_rand_depth_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" randread 4k 512 15 /dev/nbd0 32)" + coronafs_controller_write_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" write 1M 256)" + node04_materialize_ns_start="$(date +%s%N)" + coronafs_materialize_volume 25088 "${node04_local_volume}" "${coronafs_uri}" $((512 * 1024 * 1024)) >/dev/null + node04_materialize_ns_end="$(date +%s%N)" + node05_materialize_ns_start="$(date +%s%N)" + coronafs_materialize_volume 26088 "${node05_local_volume}" "${coronafs_uri}" $((512 * 1024 * 1024)) >/dev/null + node05_materialize_ns_end="$(date +%s%N)" + + node04_export_json="$(coronafs_export_volume_json 25088 "${node04_local_volume}")" + node04_local_uri="$(printf '%s' "${node04_export_json}" | jq -r '.export.uri')" + [[ -n "${node04_local_uri}" && "${node04_local_uri}" != "null" ]] || die "node04 local CoronaFS benchmark volume did not return an export URI" + node05_export_json="$(coronafs_export_volume_json 26088 "${node05_local_volume}")" + node05_local_uri="$(printf '%s' "${node05_export_json}" | jq -r '.export.uri')" + [[ -n "${node05_local_uri}" && "${node05_local_uri}" != "null" ]] || die "node05 local CoronaFS benchmark volume did not return an export URI" + + node04_materialize_sec="$(calc_seconds_from_ns "$((node04_materialize_ns_end - node04_materialize_ns_start))")" + node05_materialize_sec="$(calc_seconds_from_ns "$((node05_materialize_ns_end - node05_materialize_ns_start))")" + coronafs_controller_read_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" read 1M 256)" + coronafs_controller_rand_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" randread 4k 128 10)" + coronafs_controller_depth_read_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" randread 4k 512 15 /dev/nbd0 32)" local_depth_write_json="$(run_remote_fio_json node04 /var/tmp/photon-bench/local-depthwrite.dat write 1M 1024 15 32 libaio)" local_depth_read_json="$(run_remote_fio_json node04 /var/tmp/photon-bench/local-depthread.dat read 1M 1024 15 32 libaio)" - coronafs_depth_write_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" write 1M 1024 15 /dev/nbd0 32)" - coronafs_depth_read_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" read 1M 1024 15 /dev/nbd0 32)" - cross_worker_read_json="$(run_remote_nbd_fio_json node05 "${coronafs_uri}" read 1M 256 0 /dev/nbd1 1)" + coronafs_controller_depth_write_json="$(run_remote_nbd_fio_json node04 "${coronafs_uri}" write 1M 1024 15 /dev/nbd0 32)" + coronafs_local_write_json="$(run_remote_nbd_fio_json node04 "${node04_local_uri}" write 1M 256)" + coronafs_local_read_json="$(run_remote_nbd_fio_json node04 "${node04_local_uri}" read 1M 256)" + coronafs_local_rand_json="$(run_remote_nbd_fio_json node04 "${node04_local_uri}" randread 4k 128 10)" + coronafs_local_depth_read_json="$(run_remote_nbd_fio_json node04 "${node04_local_uri}" randread 4k 512 15 /dev/nbd0 32)" + coronafs_local_depth_write_json="$(run_remote_nbd_fio_json node04 "${node04_local_uri}" write 1M 1024 15 /dev/nbd0 32)" + coronafs_target_local_read_json="$(run_remote_nbd_fio_json node05 "${node05_local_uri}" read 1M 256 0 /dev/nbd1 1)" local local_write_mibps local_read_mibps local_rand_iops local_rand_depth_iops - local coronafs_write_mibps coronafs_read_mibps coronafs_rand_iops coronafs_rand_depth_iops coronafs_cross_read_mibps - local local_depth_write_mibps local_depth_read_mibps coronafs_depth_write_mibps coronafs_depth_read_mibps + local coronafs_controller_write_mibps coronafs_controller_read_mibps coronafs_controller_rand_iops coronafs_controller_rand_depth_iops + local local_depth_write_mibps local_depth_read_mibps coronafs_controller_depth_write_mibps coronafs_controller_depth_read_mibps + local coronafs_local_write_mibps coronafs_local_read_mibps coronafs_local_rand_iops coronafs_local_depth_read_iops + local coronafs_local_depth_write_mibps coronafs_local_depth_read_mibps coronafs_target_local_read_mibps local_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${local_write_json}" | jq -r '.bw_bytes')")" local_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${local_read_json}" | jq -r '.bw_bytes')")" local_rand_iops="$(printf '%s' "${local_rand_json}" | jq -r '.iops | floor')" local_rand_depth_iops="$(printf '%s' "${local_rand_depth_json}" | jq -r '.iops | floor')" - coronafs_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_write_json}" | jq -r '.bw_bytes')")" - coronafs_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_read_json}" | jq -r '.bw_bytes')")" - coronafs_rand_iops="$(printf '%s' "${coronafs_rand_json}" | jq -r '.iops | floor')" - coronafs_rand_depth_iops="$(printf '%s' "${coronafs_rand_depth_json}" | jq -r '.iops | floor')" + coronafs_controller_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_controller_write_json}" | jq -r '.bw_bytes')")" + coronafs_controller_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_controller_read_json}" | jq -r '.bw_bytes')")" + coronafs_controller_rand_iops="$(printf '%s' "${coronafs_controller_rand_json}" | jq -r '.iops | floor')" + coronafs_controller_rand_depth_iops="$(printf '%s' "${coronafs_controller_depth_read_json}" | jq -r '.iops | floor')" local_depth_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${local_depth_write_json}" | jq -r '.bw_bytes')")" local_depth_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${local_depth_read_json}" | jq -r '.bw_bytes')")" - coronafs_depth_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_depth_write_json}" | jq -r '.bw_bytes')")" - coronafs_depth_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_depth_read_json}" | jq -r '.bw_bytes')")" - coronafs_cross_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${cross_worker_read_json}" | jq -r '.bw_bytes')")" + coronafs_controller_depth_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_controller_depth_write_json}" | jq -r '.bw_bytes')")" + coronafs_controller_depth_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_controller_depth_read_json}" | jq -r '.bw_bytes')")" + coronafs_local_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_local_write_json}" | jq -r '.bw_bytes')")" + coronafs_local_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_local_read_json}" | jq -r '.bw_bytes')")" + coronafs_local_rand_iops="$(printf '%s' "${coronafs_local_rand_json}" | jq -r '.iops | floor')" + coronafs_local_depth_read_iops="$(printf '%s' "${coronafs_local_depth_read_json}" | jq -r '.iops | floor')" + coronafs_local_depth_write_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_local_depth_write_json}" | jq -r '.bw_bytes')")" + coronafs_local_depth_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_local_depth_read_json}" | jq -r '.bw_bytes')")" + coronafs_target_local_read_mibps="$(bw_bytes_to_mibps "$(printf '%s' "${coronafs_target_local_read_json}" | jq -r '.bw_bytes')")" log "CoronaFS local baseline: write=${local_write_mibps} MiB/s read=${local_read_mibps} MiB/s randread=${local_rand_iops} IOPS queued_randread=${local_rand_depth_iops} IOPS" - log "CoronaFS shared block volume: write=${coronafs_write_mibps} MiB/s read=${coronafs_read_mibps} MiB/s randread=${coronafs_rand_iops} IOPS queued_randread=${coronafs_rand_depth_iops} IOPS" - log "CoronaFS queued depth-32 profile: local_write=${local_depth_write_mibps} MiB/s local_read=${local_depth_read_mibps} MiB/s shared_write=${coronafs_depth_write_mibps} MiB/s shared_read=${coronafs_depth_read_mibps} MiB/s" - log "CoronaFS cross-worker shared read: read=${coronafs_cross_read_mibps} MiB/s (node04 write -> node05 direct read over the same NBD export)" + log "CoronaFS controller export path: write=${coronafs_controller_write_mibps} MiB/s read=${coronafs_controller_read_mibps} MiB/s randread=${coronafs_controller_rand_iops} IOPS queued_randread=${coronafs_controller_rand_depth_iops} IOPS" + log "CoronaFS node-local export path: write=${coronafs_local_write_mibps} MiB/s read=${coronafs_local_read_mibps} MiB/s randread=${coronafs_local_rand_iops} IOPS queued_randread=${coronafs_local_depth_read_iops} IOPS" + log "CoronaFS depth-32 profile: local_write=${local_depth_write_mibps} MiB/s local_read=${local_depth_read_mibps} MiB/s controller_write=${coronafs_controller_depth_write_mibps} MiB/s controller_read=${coronafs_controller_depth_read_mibps} MiB/s node_local_write=${coronafs_local_depth_write_mibps} MiB/s node_local_read=${coronafs_local_depth_read_mibps} MiB/s" + log "CoronaFS materialize latency: node04=${node04_materialize_sec}s node05=${node05_materialize_sec}s target_local_read=${coronafs_target_local_read_mibps} MiB/s" - printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "${local_write_mibps}" "${local_read_mibps}" "${local_rand_iops}" "${local_rand_depth_iops}" \ - "${coronafs_write_mibps}" "${coronafs_read_mibps}" "${coronafs_rand_iops}" "${coronafs_rand_depth_iops}" \ - "${coronafs_cross_read_mibps}" \ + "${coronafs_controller_write_mibps}" "${coronafs_controller_read_mibps}" "${coronafs_controller_rand_iops}" "${coronafs_controller_rand_depth_iops}" \ "${local_depth_write_mibps}" "${local_depth_read_mibps}" \ - "${coronafs_depth_write_mibps}" "${coronafs_depth_read_mibps}" + "${coronafs_controller_depth_write_mibps}" "${coronafs_controller_depth_read_mibps}" \ + "${coronafs_local_write_mibps}" "${coronafs_local_read_mibps}" "${coronafs_local_rand_iops}" "${coronafs_local_depth_read_iops}" \ + "${coronafs_local_depth_write_mibps}" "${coronafs_local_depth_read_mibps}" \ + "${node04_materialize_sec}" "${node05_materialize_sec}" "${coronafs_target_local_read_mibps}" trap - RETURN cleanup_coronafs_bench @@ -5026,12 +5553,30 @@ benchmark_coronafs_performance() { benchmark_lightningstor_performance() { local client_node="${LIGHTNINGSTOR_BENCH_CLIENT_NODE:-node03}" + local large_object_size_mb="${LIGHTNINGSTOR_BENCH_SIZE_MB:-256}" + local small_object_count="${LIGHTNINGSTOR_BENCH_SMALL_COUNT:-32}" + local small_object_size_mb="${LIGHTNINGSTOR_BENCH_SMALL_SIZE_MB:-4}" + local parallelism="${LIGHTNINGSTOR_BENCH_PARALLELISM:-8}" log "Benchmarking LightningStor S3 throughput from ${client_node}" + local org_id="bench-org" + local project_id="bench-project" + local principal_id="lightningstor-s3-bench-$(date +%s)" + local iam_tunnel="" + local s3_access_key="" s3_secret_key="" local bucket="ls-bench-$(date +%s)" local object_key="bench-object.bin" local result_json - if ! result_json="$(ssh_node_script "${client_node}" "${bucket}" "${object_key}" 256 32 4 8 <<'EOS' + cleanup_lightningstor_bench_auth() { + stop_ssh_tunnel node01 "${iam_tunnel}" + } + trap cleanup_lightningstor_bench_auth RETURN + + iam_tunnel="$(start_ssh_tunnel node01 15080 50080)" + issue_project_admin_token 15080 "${org_id}" "${project_id}" "${principal_id}" >/dev/null + IFS=$'\t' read -r s3_access_key s3_secret_key < <(issue_s3_credential 15080 "${principal_id}" "${org_id}" "${project_id}" "lightningstor-storage-benchmark") + + if ! result_json="$(ssh_node_script "${client_node}" "${bucket}" "${object_key}" "${large_object_size_mb}" "${small_object_count}" "${small_object_size_mb}" "${parallelism}" "${s3_access_key}" "${s3_secret_key}" <<'EOS' set -euo pipefail bucket="$1" @@ -5040,12 +5585,14 @@ size_mb="$3" small_count="$4" small_size_mb="$5" parallelism="$6" +access_key_id="$7" +secret_key="$8" endpoint="http://10.100.0.11:9000" workdir="/var/tmp/photon-bench-s3" src="${workdir}/upload.bin" dst="${workdir}/download.bin" mkdir -p "${workdir}" -python3 - "${bucket}" "${object_key}" "${size_mb}" "${small_count}" "${small_size_mb}" "${parallelism}" "${endpoint}" "${workdir}" "${src}" "${dst}" <<'PY' +python3 - "${bucket}" "${object_key}" "${size_mb}" "${small_count}" "${small_size_mb}" "${parallelism}" "${endpoint}" "${workdir}" "${src}" "${dst}" "${access_key_id}" "${secret_key}" <<'PY' import concurrent.futures import hashlib import json @@ -5058,7 +5605,7 @@ import boto3 from botocore.config import Config -bucket, object_key, size_mb, small_count, small_size_mb, parallelism, endpoint, workdir, src, dst = os.sys.argv[1:11] +bucket, object_key, size_mb, small_count, small_size_mb, parallelism, endpoint, workdir, src, dst, access_key_id, secret_key = os.sys.argv[1:13] size_mb = int(size_mb) small_count = int(small_count) small_size_mb = int(small_size_mb) @@ -5094,8 +5641,8 @@ def new_client(): "s3", endpoint_url=endpoint, region_name="us-east-1", - aws_access_key_id="photoncloud-test", - aws_secret_access_key="photoncloud-test-secret", + aws_access_key_id=access_key_id, + aws_secret_access_key=secret_key, use_ssl=False, verify=False, config=Config( @@ -5270,48 +5817,54 @@ EOS "${small_put_ops}/${small_get_ops}" \ "${parallel_small_upload_mibps}" "${parallel_small_download_mibps}" \ "${parallel_small_put_ops}/${parallel_small_get_ops}" + + trap - RETURN + cleanup_lightningstor_bench_auth } benchmark_plasmavmc_image_path() { log "Benchmarking PlasmaVMC image import plus CoronaFS-backed volume clone latency" local iam_tunnel="" ls_tunnel="" vm_tunnel="" + local iam_port=15180 + local ls_port=15186 + local vm_port=15182 local image_id="" cold_volume_id="" warm_volume_id="" image_source_path="" - iam_tunnel="$(start_ssh_tunnel node01 15080 50080)" - ls_tunnel="$(start_ssh_tunnel node01 15086 50086)" - vm_tunnel="$(start_ssh_tunnel node01 15082 50082)" + iam_tunnel="$(start_ssh_tunnel node01 "${iam_port}" 50080)" + ls_tunnel="$(start_ssh_tunnel node01 "${ls_port}" 50086)" + vm_tunnel="$(start_ssh_tunnel node01 "${vm_port}" 50082)" cleanup_plasmavmc_image_bench() { - if [[ -n "${cold_volume_id}" ]]; then + if [[ -n "${cold_volume_id:-}" ]]; then grpcurl -plaintext \ - -H "authorization: Bearer ${token}" \ + -H "authorization: Bearer ${token:-}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ - -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg volume "${cold_volume_id}" '{orgId:$org, projectId:$project, volumeId:$volume}')" \ - 127.0.0.1:15082 plasmavmc.v1.VolumeService/DeleteVolume >/dev/null 2>&1 || true + -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg volume "${cold_volume_id:-}" '{orgId:$org, projectId:$project, volumeId:$volume}')" \ + 127.0.0.1:${vm_port} plasmavmc.v1.VolumeService/DeleteVolume >/dev/null 2>&1 || true fi - if [[ -n "${warm_volume_id}" ]]; then + if [[ -n "${warm_volume_id:-}" ]]; then grpcurl -plaintext \ - -H "authorization: Bearer ${token}" \ + -H "authorization: Bearer ${token:-}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ - -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg volume "${warm_volume_id}" '{orgId:$org, projectId:$project, volumeId:$volume}')" \ - 127.0.0.1:15082 plasmavmc.v1.VolumeService/DeleteVolume >/dev/null 2>&1 || true + -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg volume "${warm_volume_id:-}" '{orgId:$org, projectId:$project, volumeId:$volume}')" \ + 127.0.0.1:${vm_port} plasmavmc.v1.VolumeService/DeleteVolume >/dev/null 2>&1 || true fi - if [[ -n "${image_id}" ]]; then + if [[ -n "${image_id:-}" ]]; then grpcurl -plaintext \ - -H "authorization: Bearer ${token}" \ + -H "authorization: Bearer ${token:-}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ - -d "$(jq -cn --arg org "${org_id}" --arg image "${image_id}" '{orgId:$org, imageId:$image}')" \ - 127.0.0.1:15082 plasmavmc.v1.ImageService/DeleteImage >/dev/null 2>&1 || true + -d "$(jq -cn --arg org "${org_id:-}" --arg image "${image_id:-}" '{orgId:$org, imageId:$image}')" \ + 127.0.0.1:${vm_port} plasmavmc.v1.ImageService/DeleteImage >/dev/null 2>&1 || true fi - if [[ -n "${image_source_path}" ]]; then - ssh_node node01 "rm -f ${image_source_path}" >/dev/null 2>&1 || true + if [[ -n "${image_source_path:-}" && "${image_source_path}" != /nix/store/* ]]; then + ssh_node node01 "rm -f ${image_source_path:-}" >/dev/null 2>&1 || true fi - stop_ssh_tunnel node01 "${vm_tunnel}" >/dev/null 2>&1 || true - stop_ssh_tunnel node01 "${ls_tunnel}" >/dev/null 2>&1 || true - stop_ssh_tunnel node01 "${iam_tunnel}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${vm_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${ls_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${iam_tunnel:-}" >/dev/null 2>&1 || true } trap cleanup_plasmavmc_image_bench RETURN @@ -5319,10 +5872,10 @@ benchmark_plasmavmc_image_path() { local project_id="plasmavmc-bench-project" local principal_id="plasmavmc-bench-$(date +%s)" local token - token="$(issue_project_admin_token 15080 "${org_id}" "${project_id}" "${principal_id}")" + token="$(issue_project_admin_token "${iam_port}" "${org_id}" "${project_id}" "${principal_id}")" - ensure_lightningstor_bucket 15086 "${token}" "plasmavmc-images" "${org_id}" "${project_id}" - wait_for_lightningstor_write_quorum 15086 "${token}" "plasmavmc-images" "PlasmaVMC benchmark image import" + ensure_lightningstor_bucket "${ls_port}" "${token}" "plasmavmc-images" "${org_id}" "${project_id}" + wait_for_lightningstor_write_quorum "${ls_port}" "${token}" "plasmavmc-images" "PlasmaVMC benchmark image import" local guest_image_local_path guest_image_sha artifact_size_bytes artifact_mib virtual_size_bytes virtual_mib guest_image_local_path="$(guest_image_path)" @@ -5334,9 +5887,7 @@ benchmark_plasmavmc_image_path() { virtual_mib="$(awk "BEGIN { printf \"%.0f\", ${virtual_size_bytes} / 1048576 }")" local image_name="bench-image-$(date +%s)" - ssh_node node01 "install -d -m 0755 /var/lib/plasmavmc/imports" - image_source_path="/var/lib/plasmavmc/imports/${image_name}.qcow2" - scp_to_node node01 "${guest_image_local_path}" "${image_source_path}" + image_source_path="$(prepare_node01_image_source "${guest_image_local_path}" "${image_name}")" [[ "$(ssh_node node01 "sha256sum ${image_source_path} | awk '{print \$1}'")" == "${guest_image_sha}" ]] || die "PlasmaVMC benchmark image checksum mismatch after distribution" local create_image_json create_image_response create_image_start_ns create_image_end_ns @@ -5366,7 +5917,7 @@ benchmark_plasmavmc_image_path() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "${create_image_json}" \ - 127.0.0.1:15082 plasmavmc.v1.ImageService/CreateImage)" + 127.0.0.1:${vm_port} plasmavmc.v1.ImageService/CreateImage)" create_image_end_ns="$(date +%s%N)" image_id="$(printf '%s' "${create_image_response}" | jq -r '.id')" [[ -n "${image_id}" && "${image_id}" != "null" ]] || die "PlasmaVMC benchmark image import did not return an image ID" @@ -5390,7 +5941,7 @@ benchmark_plasmavmc_image_path() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "${cold_request}" \ - 127.0.0.1:15082 plasmavmc.v1.VolumeService/CreateVolume)" + 127.0.0.1:${vm_port} plasmavmc.v1.VolumeService/CreateVolume)" cold_end_ns="$(date +%s%N)" cold_volume_id="$(printf '%s' "${cold_response}" | jq -r '.id')" [[ -n "${cold_volume_id}" && "${cold_volume_id}" != "null" ]] || die "PlasmaVMC cold image-backed volume create did not return a volume ID" @@ -5400,7 +5951,7 @@ benchmark_plasmavmc_image_path() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg volume "${cold_volume_id}" '{orgId:$org, projectId:$project, volumeId:$volume}')" \ - 127.0.0.1:15082 plasmavmc.v1.VolumeService/DeleteVolume >/dev/null + 127.0.0.1:${vm_port} plasmavmc.v1.VolumeService/DeleteVolume >/dev/null cold_volume_id="" warm_request="$(jq -cn --arg name "bench-warm-$(date +%s)" --arg org "${org_id}" --arg project "${project_id}" --arg image "${image_id}" '{ @@ -5420,7 +5971,7 @@ benchmark_plasmavmc_image_path() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "${warm_request}" \ - 127.0.0.1:15082 plasmavmc.v1.VolumeService/CreateVolume)" + 127.0.0.1:${vm_port} plasmavmc.v1.VolumeService/CreateVolume)" warm_end_ns="$(date +%s%N)" warm_volume_id="$(printf '%s' "${warm_response}" | jq -r '.id')" [[ -n "${warm_volume_id}" && "${warm_volume_id}" != "null" ]] || die "PlasmaVMC warm image-backed volume create did not return a volume ID" @@ -5441,64 +5992,71 @@ benchmark_plasmavmc_guest_runtime() { log "Benchmarking PlasmaVMC guest-side CoronaFS runtime throughput" local iam_tunnel="" ls_tunnel="" vm_tunnel="" coronafs_tunnel="" + local node04_coronafs_tunnel="" node05_coronafs_tunnel="" current_worker_coronafs_port="" + local iam_port=15280 + local ls_port=15286 + local vm_port=15282 + local coronafs_port=15288 local image_id="" vm_id="" image_source_path="" - iam_tunnel="$(start_ssh_tunnel node01 15080 50080)" - ls_tunnel="$(start_ssh_tunnel node01 15086 50086)" - vm_tunnel="$(start_ssh_tunnel node01 15082 50082)" - coronafs_tunnel="$(start_ssh_tunnel node01 15088 "${CORONAFS_API_PORT}")" + iam_tunnel="$(start_ssh_tunnel node01 "${iam_port}" 50080)" + ls_tunnel="$(start_ssh_tunnel node01 "${ls_port}" 50086)" + vm_tunnel="$(start_ssh_tunnel node01 "${vm_port}" 50082)" + coronafs_tunnel="$(start_ssh_tunnel node01 "${coronafs_port}" "${CORONAFS_API_PORT}")" + node04_coronafs_tunnel="$(start_ssh_tunnel node04 25288 "${CORONAFS_API_PORT}")" + node05_coronafs_tunnel="$(start_ssh_tunnel node05 35288 "${CORONAFS_API_PORT}")" cleanup_plasmavmc_guest_runtime() { - if [[ -n "${vm_id}" ]]; then + if [[ -n "${vm_id:-}" ]]; then grpcurl -plaintext \ - -H "authorization: Bearer ${token}" \ + -H "authorization: Bearer ${token:-}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ - -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg vm "${vm_id}" '{orgId:$org, projectId:$project, vmId:$vm, force:true, timeoutSeconds:30}')" \ - 127.0.0.1:15082 plasmavmc.v1.VmService/StopVm >/dev/null 2>&1 || true + -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg vm "${vm_id:-}" '{orgId:$org, projectId:$project, vmId:$vm, force:true, timeoutSeconds:30}')" \ + 127.0.0.1:${vm_port} plasmavmc.v1.VmService/StopVm >/dev/null 2>&1 || true grpcurl -plaintext \ - -H "authorization: Bearer ${token}" \ + -H "authorization: Bearer ${token:-}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ - -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg vm "${vm_id}" '{orgId:$org, projectId:$project, vmId:$vm}' )" \ - 127.0.0.1:15082 plasmavmc.v1.VmService/DeleteVm >/dev/null 2>&1 || true + -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg vm "${vm_id:-}" '{orgId:$org, projectId:$project, vmId:$vm}' )" \ + 127.0.0.1:${vm_port} plasmavmc.v1.VmService/DeleteVm >/dev/null 2>&1 || true fi - if [[ -n "${image_id}" ]]; then + if [[ -n "${image_id:-}" ]]; then grpcurl -plaintext \ - -H "authorization: Bearer ${token}" \ + -H "authorization: Bearer ${token:-}" \ -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ - -d "$(jq -cn --arg org "${org_id}" --arg image "${image_id}" '{orgId:$org, imageId:$image}')" \ - 127.0.0.1:15082 plasmavmc.v1.ImageService/DeleteImage >/dev/null 2>&1 || true + -d "$(jq -cn --arg org "${org_id:-}" --arg image "${image_id:-}" '{orgId:$org, imageId:$image}')" \ + 127.0.0.1:${vm_port} plasmavmc.v1.ImageService/DeleteImage >/dev/null 2>&1 || true fi - if [[ -n "${image_source_path}" ]]; then - ssh_node node01 "rm -f ${image_source_path}" >/dev/null 2>&1 || true + if [[ -n "${image_source_path:-}" && "${image_source_path}" != /nix/store/* ]]; then + ssh_node node01 "rm -f ${image_source_path:-}" >/dev/null 2>&1 || true fi - stop_ssh_tunnel node01 "${coronafs_tunnel}" >/dev/null 2>&1 || true - stop_ssh_tunnel node01 "${vm_tunnel}" >/dev/null 2>&1 || true - stop_ssh_tunnel node01 "${ls_tunnel}" >/dev/null 2>&1 || true - stop_ssh_tunnel node01 "${iam_tunnel}" >/dev/null 2>&1 || true + stop_ssh_tunnel node05 "${node05_coronafs_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node04 "${node04_coronafs_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${coronafs_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${vm_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${ls_tunnel:-}" >/dev/null 2>&1 || true + stop_ssh_tunnel node01 "${iam_tunnel:-}" >/dev/null 2>&1 || true } trap cleanup_plasmavmc_guest_runtime RETURN - wait_for_plasmavmc_workers_registered 15082 + wait_for_plasmavmc_workers_registered "${vm_port}" local org_id="plasmavmc-runtime-org-$(date +%s)" local project_id="plasmavmc-runtime-project" local principal_id="plasmavmc-runtime-$(date +%s)" local token - token="$(issue_project_admin_token 15080 "${org_id}" "${project_id}" "${principal_id}")" + token="$(issue_project_admin_token "${iam_port}" "${org_id}" "${project_id}" "${principal_id}")" - ensure_lightningstor_bucket 15086 "${token}" "plasmavmc-images" "${org_id}" "${project_id}" - wait_for_lightningstor_write_quorum 15086 "${token}" "plasmavmc-images" "PlasmaVMC runtime benchmark image import" + ensure_lightningstor_bucket "${ls_port}" "${token}" "plasmavmc-images" "${org_id}" "${project_id}" + wait_for_lightningstor_write_quorum "${ls_port}" "${token}" "plasmavmc-images" "PlasmaVMC runtime benchmark image import" local guest_image_local_path guest_image_sha image_name create_image_json create_image_response guest_image_local_path="$(guest_bench_image_path)" [[ -n "${guest_image_local_path}" ]] || die "failed to locate VM benchmark guest image" guest_image_sha="$(sha256sum "${guest_image_local_path}" | awk '{print $1}')" image_name="bench-runtime-image-$(date +%s)" - ssh_node node01 "install -d -m 0755 /var/lib/plasmavmc/imports" - image_source_path="/var/lib/plasmavmc/imports/${image_name}.qcow2" - scp_to_node node01 "${guest_image_local_path}" "${image_source_path}" + image_source_path="$(prepare_node01_image_source "${guest_image_local_path}" "${image_name}")" [[ "$(ssh_node node01 "sha256sum ${image_source_path} | awk '{print \$1}'")" == "${guest_image_sha}" ]] || die "PlasmaVMC runtime benchmark image checksum mismatch after distribution" create_image_json="$( @@ -5526,7 +6084,7 @@ benchmark_plasmavmc_guest_runtime() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "${create_image_json}" \ - 127.0.0.1:15082 plasmavmc.v1.ImageService/CreateImage)" + 127.0.0.1:${vm_port} plasmavmc.v1.ImageService/CreateImage)" image_id="$(printf '%s' "${create_image_response}" | jq -r '.id')" [[ -n "${image_id}" && "${image_id}" != "null" ]] || die "PlasmaVMC runtime benchmark image import did not return an image ID" printf '%s' "${create_image_response}" | jq -e '.status == "IMAGE_STATUS_AVAILABLE"' >/dev/null @@ -5571,7 +6129,7 @@ benchmark_plasmavmc_guest_runtime() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "${create_vm_json}" \ - 127.0.0.1:15082 plasmavmc.v1.VmService/CreateVm)" + 127.0.0.1:${vm_port} plasmavmc.v1.VmService/CreateVm)" vm_id="$(printf '%s' "${create_response}" | jq -r '.id')" [[ -n "${vm_id}" && "${vm_id}" != "null" ]] || die "PlasmaVMC runtime benchmark VM create did not return a VM ID" @@ -5579,7 +6137,7 @@ benchmark_plasmavmc_guest_runtime() { local deadline=$((SECONDS + HTTP_WAIT_TIMEOUT)) while true; do local vm_json - if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" 2>/dev/null)"; then + if ! vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" "${vm_port}" 2>/dev/null)"; then if (( SECONDS >= deadline )); then die "timed out waiting for runtime benchmark VM ${vm_id} scheduling" fi @@ -5597,8 +6155,10 @@ benchmark_plasmavmc_guest_runtime() { done if [[ "${node_id}" == "node04" ]]; then peer_node="node05" + current_worker_coronafs_port=25288 else peer_node="node04" + current_worker_coronafs_port=35288 fi local start_ns attach_ns ready_ns attach_sec ready_sec @@ -5612,15 +6172,20 @@ benchmark_plasmavmc_guest_runtime() { -import-path "${PLASMAVMC_PROTO_DIR}" \ -proto "${PLASMAVMC_PROTO}" \ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg vm "${vm_id}" '{orgId:$org, projectId:$project, vmId:$vm}')" \ - 127.0.0.1:15082 plasmavmc.v1.VmService/StartVm >/dev/null + 127.0.0.1:${vm_port} plasmavmc.v1.VmService/StartVm >/dev/null - root_uri="$(coronafs_export_volume_json 15088 "${root_volume_id}" | jq -r '.export.uri')" - data_uri="$(coronafs_export_volume_json 15088 "${data_volume_id}" | jq -r '.export.uri')" + root_uri="$(coronafs_export_volume_json "${coronafs_port}" "${root_volume_id}" | jq -r '.export.uri')" + data_uri="$(coronafs_export_volume_json "${coronafs_port}" "${data_volume_id}" | jq -r '.export.uri')" [[ -n "${root_uri}" && "${root_uri}" != "null" ]] || die "runtime benchmark root volume export URI missing" [[ -n "${data_uri}" && "${data_uri}" != "null" ]] || die "runtime benchmark data volume export URI missing" + coronafs_get_volume_json "${current_worker_coronafs_port}" "${root_volume_id}" >/dev/null + coronafs_get_volume_json "${current_worker_coronafs_port}" "${data_volume_id}" >/dev/null - wait_for_qemu_volume_present "${node_id}" "${root_uri}" - wait_for_qemu_volume_present "${node_id}" "${data_uri}" + local root_local_export_uri data_local_export_uri + root_local_export_uri="$(coronafs_volume_export_uri "${current_worker_coronafs_port}" "${root_volume_id}")" + data_local_export_uri="$(coronafs_volume_export_uri "${current_worker_coronafs_port}" "${data_volume_id}")" + wait_for_qemu_volume_present "${node_id}" "${CORONAFS_VOLUME_ROOT}/${root_volume_id}.raw" "${root_local_export_uri}" + wait_for_qemu_volume_present "${node_id}" "${CORONAFS_VOLUME_ROOT}/${data_volume_id}.raw" "${data_local_export_uri}" attach_ns="$(date +%s%N)" wait_for_vm_console_pattern "${node_id}" "${vm_id}" "PHOTON_VM_BENCH_RESULT" @@ -5628,9 +6193,9 @@ benchmark_plasmavmc_guest_runtime() { local result_line seq_write_mibps seq_read_mibps randread_iops result_line="$(read_vm_console_line_matching "${node_id}" "${vm_id}" "PHOTON_VM_BENCH_RESULT")" - seq_write_mibps="$(printf '%s\n' "${result_line}" | sed -n 's/.*seq_write_mibps=\([^ ]*\).*/\1/p')" - seq_read_mibps="$(printf '%s\n' "${result_line}" | sed -n 's/.*seq_read_mibps=\([^ ]*\).*/\1/p')" - randread_iops="$(printf '%s\n' "${result_line}" | sed -n 's/.*randread_iops=\([^ ]*\).*/\1/p')" + seq_write_mibps="$(printf '%s\n' "${result_line}" | tr -d '\r' | sed -n 's/.*seq_write_mibps=\([^ ]*\).*/\1/p')" + seq_read_mibps="$(printf '%s\n' "${result_line}" | tr -d '\r' | sed -n 's/.*seq_read_mibps=\([^ ]*\).*/\1/p')" + randread_iops="$(printf '%s\n' "${result_line}" | tr -d '\r' | sed -n 's/.*randread_iops=\([^ ]*\).*/\1/p')" [[ -n "${seq_write_mibps}" && -n "${seq_read_mibps}" && -n "${randread_iops}" ]] || die "failed to parse runtime benchmark result line: ${result_line}" attach_sec="$(calc_seconds_from_ns "$((attach_ns - start_ns))")" @@ -5650,49 +6215,63 @@ write_storage_benchmark_report() { local local_read_mibps="$6" local local_rand_iops="$7" local local_rand_depth_iops="$8" - local coronafs_write_mibps="$9" - local coronafs_read_mibps="${10}" - local coronafs_rand_iops="${11}" - local coronafs_rand_depth_iops="${12}" - local coronafs_cross_read_mibps="${13}" - local local_depth_write_mibps="${14}" - local local_depth_read_mibps="${15}" - local coronafs_depth_write_mibps="${16}" - local coronafs_depth_read_mibps="${17}" - local lightningstor_upload_mibps="${18}" - local lightningstor_download_mibps="${19}" - local lightningstor_object_mib="${20}" - local lightningstor_small_object_count="${21}" - local lightningstor_small_object_mib="${22}" - local lightningstor_small_upload_mibps="${23}" - local lightningstor_small_download_mibps="${24}" - local lightningstor_small_ops="${25}" - local lightningstor_parallel_small_upload_mibps="${26}" - local lightningstor_parallel_small_download_mibps="${27}" - local lightningstor_parallel_small_ops="${28}" - local plasmavmc_image_artifact_mib="${29}" - local plasmavmc_image_virtual_mib="${30}" - local plasmavmc_image_import_sec="${31}" - local plasmavmc_cold_clone_sec="${32}" - local plasmavmc_warm_clone_sec="${33}" - local plasmavmc_runtime_attach_sec="${34}" - local plasmavmc_runtime_ready_sec="${35}" - local plasmavmc_runtime_seq_write_mibps="${36}" - local plasmavmc_runtime_seq_read_mibps="${37}" - local plasmavmc_runtime_randread_iops="${38}" - local coronafs_read_ratio coronafs_rand_ratio coronafs_rand_depth_ratio coronafs_cross_read_ratio coronafs_vs_network_ratio coronafs_depth_read_ratio lightningstor_vs_network_ratio + local coronafs_controller_write_mibps="$9" + local coronafs_controller_read_mibps="${10}" + local coronafs_controller_rand_iops="${11}" + local coronafs_controller_rand_depth_iops="${12}" + local local_depth_write_mibps="${13}" + local local_depth_read_mibps="${14}" + local coronafs_controller_depth_write_mibps="${15}" + local coronafs_controller_depth_read_mibps="${16}" + local coronafs_local_write_mibps="${17}" + local coronafs_local_read_mibps="${18}" + local coronafs_local_rand_iops="${19}" + local coronafs_local_rand_depth_iops="${20}" + local coronafs_local_depth_write_mibps="${21}" + local coronafs_local_depth_read_mibps="${22}" + local coronafs_node04_materialize_sec="${23}" + local coronafs_node05_materialize_sec="${24}" + local coronafs_target_local_read_mibps="${25}" + local lightningstor_upload_mibps="${26}" + local lightningstor_download_mibps="${27}" + local lightningstor_object_mib="${28}" + local lightningstor_small_object_count="${29}" + local lightningstor_small_object_mib="${30}" + local lightningstor_small_upload_mibps="${31}" + local lightningstor_small_download_mibps="${32}" + local lightningstor_small_ops="${33}" + local lightningstor_parallel_small_upload_mibps="${34}" + local lightningstor_parallel_small_download_mibps="${35}" + local lightningstor_parallel_small_ops="${36}" + local plasmavmc_image_artifact_mib="${37}" + local plasmavmc_image_virtual_mib="${38}" + local plasmavmc_image_import_sec="${39}" + local plasmavmc_cold_clone_sec="${40}" + local plasmavmc_warm_clone_sec="${41}" + local plasmavmc_runtime_attach_sec="${42}" + local plasmavmc_runtime_ready_sec="${43}" + local plasmavmc_runtime_seq_write_mibps="${44}" + local plasmavmc_runtime_seq_read_mibps="${45}" + local plasmavmc_runtime_randread_iops="${46}" + local coronafs_controller_read_ratio coronafs_controller_rand_ratio coronafs_controller_rand_depth_ratio coronafs_controller_vs_network_ratio coronafs_controller_depth_read_ratio + local coronafs_local_read_ratio coronafs_local_rand_ratio coronafs_local_rand_depth_ratio coronafs_local_depth_read_ratio coronafs_target_local_read_ratio + local lightningstor_vs_network_ratio local lightningstor_small_put_ops lightningstor_small_get_ops local lightningstor_parallel_small_put_ops lightningstor_parallel_small_get_ops IFS=/ read -r lightningstor_small_put_ops lightningstor_small_get_ops <<<"${lightningstor_small_ops}" IFS=/ read -r lightningstor_parallel_small_put_ops lightningstor_parallel_small_get_ops <<<"${lightningstor_parallel_small_ops}" - coronafs_read_ratio="$(awk "BEGIN { if (${local_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_read_mibps} / ${local_read_mibps}) * 100 }")" - coronafs_rand_ratio="$(awk "BEGIN { if (${local_rand_iops} == 0) print 0; else printf \"%.1f\", (${coronafs_rand_iops} / ${local_rand_iops}) * 100 }")" - coronafs_rand_depth_ratio="$(awk "BEGIN { if (${local_rand_depth_iops} == 0) print 0; else printf \"%.1f\", (${coronafs_rand_depth_iops} / ${local_rand_depth_iops}) * 100 }")" - coronafs_cross_read_ratio="$(awk "BEGIN { if (${local_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_cross_read_mibps} / ${local_read_mibps}) * 100 }")" - coronafs_vs_network_ratio="$(awk "BEGIN { if (${coronafs_network_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_read_mibps} / ${coronafs_network_mibps}) * 100 }")" - coronafs_depth_read_ratio="$(awk "BEGIN { if (${local_depth_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_depth_read_mibps} / ${local_depth_read_mibps}) * 100 }")" + coronafs_controller_read_ratio="$(awk "BEGIN { if (${local_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_controller_read_mibps} / ${local_read_mibps}) * 100 }")" + coronafs_controller_rand_ratio="$(awk "BEGIN { if (${local_rand_iops} == 0) print 0; else printf \"%.1f\", (${coronafs_controller_rand_iops} / ${local_rand_iops}) * 100 }")" + coronafs_controller_rand_depth_ratio="$(awk "BEGIN { if (${local_rand_depth_iops} == 0) print 0; else printf \"%.1f\", (${coronafs_controller_rand_depth_iops} / ${local_rand_depth_iops}) * 100 }")" + coronafs_controller_vs_network_ratio="$(awk "BEGIN { if (${coronafs_network_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_controller_read_mibps} / ${coronafs_network_mibps}) * 100 }")" + coronafs_controller_depth_read_ratio="$(awk "BEGIN { if (${local_depth_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_controller_depth_read_mibps} / ${local_depth_read_mibps}) * 100 }")" + coronafs_local_read_ratio="$(awk "BEGIN { if (${local_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_local_read_mibps} / ${local_read_mibps}) * 100 }")" + coronafs_local_rand_ratio="$(awk "BEGIN { if (${local_rand_iops} == 0) print 0; else printf \"%.1f\", (${coronafs_local_rand_iops} / ${local_rand_iops}) * 100 }")" + coronafs_local_rand_depth_ratio="$(awk "BEGIN { if (${local_rand_depth_iops} == 0) print 0; else printf \"%.1f\", (${coronafs_local_rand_depth_iops} / ${local_rand_depth_iops}) * 100 }")" + coronafs_local_depth_read_ratio="$(awk "BEGIN { if (${local_depth_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_local_depth_read_mibps} / ${local_depth_read_mibps}) * 100 }")" + coronafs_target_local_read_ratio="$(awk "BEGIN { if (${local_read_mibps} == 0) print 0; else printf \"%.1f\", (${coronafs_target_local_read_mibps} / ${local_read_mibps}) * 100 }")" lightningstor_vs_network_ratio="$(awk "BEGIN { if (${lightningstor_network_mibps} == 0) print 0; else printf \"%.1f\", (${lightningstor_download_mibps} / ${lightningstor_network_mibps}) * 100 }")" cat > "${REPO_ROOT}/docs/storage-benchmarks.md" < qemu attach -> guest boot -> guest fio\` ## Assessment -- CoronaFS shared-volume reads are currently ${coronafs_read_ratio}% of the measured local-disk baseline on this nested-QEMU lab cluster. -- CoronaFS 4k random reads are currently ${coronafs_rand_ratio}% of the measured local-disk baseline. -- CoronaFS queued 4k random reads are currently ${coronafs_rand_depth_ratio}% of the measured local queued-random-read baseline. -- CoronaFS cross-worker reads are currently ${coronafs_cross_read_ratio}% of the measured local-disk sequential-read baseline, which is the more relevant signal for VM restart and migration paths. -- CoronaFS sequential reads are currently ${coronafs_vs_network_ratio}% of the measured node04->node01 TCP baseline, which helps separate NBD/export overhead from raw cluster-network limits. -- CoronaFS depth-32 reads are currently ${coronafs_depth_read_ratio}% of the local depth-32 baseline, which is a better proxy for queued guest I/O than the single-depth path. -- The shared-volume path is functionally correct for mutable VM disks and migration tests, but its read-side throughput is still too low to call production-ready for heavier VM workloads. +- CoronaFS controller-export reads are currently ${coronafs_controller_read_ratio}% of the measured local-disk baseline on this nested-QEMU lab cluster. +- CoronaFS controller-export 4k random reads are currently ${coronafs_controller_rand_ratio}% of the measured local-disk baseline. +- CoronaFS controller-export queued 4k random reads are currently ${coronafs_controller_rand_depth_ratio}% of the measured local queued-random-read baseline. +- CoronaFS controller-export sequential reads are currently ${coronafs_controller_vs_network_ratio}% of the measured node04->node01 TCP baseline, which isolates the centralized source path from raw cluster-network limits. +- CoronaFS controller-export depth-32 reads are currently ${coronafs_controller_depth_read_ratio}% of the local depth-32 baseline. +- CoronaFS node-local reads are currently ${coronafs_local_read_ratio}% of the measured local-disk baseline, which is the more relevant steady-state signal for mutable VM disks after attachment. +- CoronaFS node-local 4k random reads are currently ${coronafs_local_rand_ratio}% of the measured local-disk baseline. +- CoronaFS node-local queued 4k random reads are currently ${coronafs_local_rand_depth_ratio}% of the measured local queued-random-read baseline. +- CoronaFS node-local depth-32 reads are currently ${coronafs_local_depth_read_ratio}% of the local depth-32 baseline. +- The target worker's node-local read path is ${coronafs_target_local_read_ratio}% of the measured local sequential-read baseline after materialization, which is the better proxy for restart and migration steady state than the old shared-export read. +- PlasmaVMC now attaches writable node-local volumes through the worker-local CoronaFS export, so the guest-runtime section should be treated as the real local VM steady-state path rather than the node-local export numbers alone. +- CoronaFS single-depth writes remain sensitive to the nested-QEMU/VDE lab transport, so the queued-depth and guest-runtime numbers are still the more reliable proxy for real VM workload behavior than the single-stream write figure alone. +- The central export path is now best understood as a source/materialization path; the worker-local export is the path that should determine VM-disk readiness going forward. - LightningStor's replicated S3 path is working correctly, but ${lightningstor_upload_mibps} MiB/s upload and ${lightningstor_download_mibps} MiB/s download are still lab-grade numbers rather than strong object-store throughput. - LightningStor large-object downloads are currently ${lightningstor_vs_network_ratio}% of the same node04->node01 TCP baseline, which indicates how much of the headroom is being lost above the raw network path. +- The current S3 frontend tuning baseline is the built-in 16 MiB streaming threshold with multipart PUT/FETCH concurrency of 8; that combination is the best default observed on this lab cluster so far. +- LightningStor uploads should be read against the replication write quorum and the same ~${lightningstor_network_mibps} MiB/s lab network ceiling; this environment still limits end-to-end throughput well before modern bare-metal NICs would. - LightningStor's small-object batch path is also functional, but ${lightningstor_small_put_ops} PUT/s and ${lightningstor_small_get_ops} GET/s still indicate a lab cluster rather than a tuned object-storage deployment. - The parallel small-object profile is the more relevant control-plane/object-ingest signal; it currently reaches ${lightningstor_parallel_small_put_ops} PUT/s and ${lightningstor_parallel_small_get_ops} GET/s. - The VM image section measures clone/materialization cost, not guest runtime I/O. +- The PlasmaVMC local image-backed clone fast path is now active again; a ${plasmavmc_warm_clone_sec} s second clone indicates the CoronaFS qcow2 backing-file path is being hit on node01 rather than falling back to eager raw materialization. - The VM runtime section is the real \`PlasmaVMC + CoronaFS + QEMU virtio-blk + guest kernel\` path; use it to judge whether QEMU/NBD tuning is helping. - The local sequential-write baseline is noisy in this environment, so the read and random-read deltas are the more reliable signal. EOF @@ -5826,8 +6418,10 @@ benchmark_storage() { local coronafs_network_mibps coronafs_network_retransmits local lightningstor_network_mibps lightningstor_network_retransmits local local_write_mibps local_read_mibps local_rand_iops local_rand_depth_iops - local coronafs_write_mibps coronafs_read_mibps coronafs_rand_iops coronafs_rand_depth_iops coronafs_cross_read_mibps - local local_depth_write_mibps local_depth_read_mibps coronafs_depth_write_mibps coronafs_depth_read_mibps + local coronafs_controller_write_mibps coronafs_controller_read_mibps coronafs_controller_rand_iops coronafs_controller_rand_depth_iops + local local_depth_write_mibps local_depth_read_mibps coronafs_controller_depth_write_mibps coronafs_controller_depth_read_mibps + local coronafs_local_write_mibps coronafs_local_read_mibps coronafs_local_rand_iops coronafs_local_rand_depth_iops + local coronafs_local_depth_write_mibps coronafs_local_depth_read_mibps coronafs_node04_materialize_sec coronafs_node05_materialize_sec coronafs_target_local_read_mibps local lightningstor_upload_mibps lightningstor_download_mibps lightningstor_object_mib local lightningstor_small_object_count lightningstor_small_object_mib local lightningstor_small_upload_mibps lightningstor_small_download_mibps lightningstor_small_ops @@ -5858,8 +6452,10 @@ benchmark_storage() { lightningstor_network_retransmits="$(printf '%s' "${lightningstor_network_results}" | jq -r '.retransmits')" IFS=$'\t' read -r \ local_write_mibps local_read_mibps local_rand_iops local_rand_depth_iops \ - coronafs_write_mibps coronafs_read_mibps coronafs_rand_iops coronafs_rand_depth_iops coronafs_cross_read_mibps \ - local_depth_write_mibps local_depth_read_mibps coronafs_depth_write_mibps coronafs_depth_read_mibps <<<"${coronafs_results}" + coronafs_controller_write_mibps coronafs_controller_read_mibps coronafs_controller_rand_iops coronafs_controller_rand_depth_iops \ + local_depth_write_mibps local_depth_read_mibps coronafs_controller_depth_write_mibps coronafs_controller_depth_read_mibps \ + coronafs_local_write_mibps coronafs_local_read_mibps coronafs_local_rand_iops coronafs_local_rand_depth_iops \ + coronafs_local_depth_write_mibps coronafs_local_depth_read_mibps coronafs_node04_materialize_sec coronafs_node05_materialize_sec coronafs_target_local_read_mibps <<<"${coronafs_results}" IFS=$'\t' read -r \ lightningstor_upload_mibps lightningstor_download_mibps lightningstor_object_mib \ lightningstor_small_object_count lightningstor_small_object_mib lightningstor_small_upload_mibps lightningstor_small_download_mibps lightningstor_small_ops \ @@ -5873,8 +6469,10 @@ benchmark_storage() { "${coronafs_network_mibps}" "${coronafs_network_retransmits}" \ "${lightningstor_network_mibps}" "${lightningstor_network_retransmits}" \ "${local_write_mibps}" "${local_read_mibps}" "${local_rand_iops}" "${local_rand_depth_iops}" \ - "${coronafs_write_mibps}" "${coronafs_read_mibps}" "${coronafs_rand_iops}" "${coronafs_rand_depth_iops}" "${coronafs_cross_read_mibps}" \ - "${local_depth_write_mibps}" "${local_depth_read_mibps}" "${coronafs_depth_write_mibps}" "${coronafs_depth_read_mibps}" \ + "${coronafs_controller_write_mibps}" "${coronafs_controller_read_mibps}" "${coronafs_controller_rand_iops}" "${coronafs_controller_rand_depth_iops}" \ + "${local_depth_write_mibps}" "${local_depth_read_mibps}" "${coronafs_controller_depth_write_mibps}" "${coronafs_controller_depth_read_mibps}" \ + "${coronafs_local_write_mibps}" "${coronafs_local_read_mibps}" "${coronafs_local_rand_iops}" "${coronafs_local_rand_depth_iops}" \ + "${coronafs_local_depth_write_mibps}" "${coronafs_local_depth_read_mibps}" "${coronafs_node04_materialize_sec}" "${coronafs_node05_materialize_sec}" "${coronafs_target_local_read_mibps}" \ "${lightningstor_upload_mibps}" "${lightningstor_download_mibps}" "${lightningstor_object_mib}" \ "${lightningstor_small_object_count}" "${lightningstor_small_object_mib}" "${lightningstor_small_upload_mibps}" "${lightningstor_small_download_mibps}" "${lightningstor_small_ops}" \ "${lightningstor_parallel_small_upload_mibps}" "${lightningstor_parallel_small_download_mibps}" "${lightningstor_parallel_small_ops}" \ @@ -6170,23 +6768,189 @@ fresh_matrix_requested() { bench_storage_requested() { STORAGE_BENCHMARK_COMMAND="${STORAGE_BENCHMARK_COMMAND:-bench-storage}" - start_requested "$@" - validate_units + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs benchmark_storage } fresh_bench_storage_requested() { STORAGE_BENCHMARK_COMMAND="fresh-bench-storage" - clean_requested "$@" + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + clean_requested "${STORAGE_NODES[@]}" bench_storage_requested "$@" } +validate_storage_bench_prereqs() { + if [[ "${PHOTON_CLUSTER_SKIP_VALIDATE:-0}" == "1" ]]; then + log "Skipping storage validation because PHOTON_CLUSTER_SKIP_VALIDATE=1" + return 0 + fi + + validate_storage_units + validate_storage_control_plane +} + +plasmavmc_image_bench_requested() { + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs + benchmark_plasmavmc_image_path +} + +plasmavmc_runtime_bench_requested() { + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs + benchmark_plasmavmc_guest_runtime +} + +coronafs_bench_requested() { + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs + benchmark_coronafs_performance +} + +coronafs_local_bench_requested() { + BUILD_PROFILE="storage" + "${REPO_ROOT}/coronafs/scripts/benchmark-local-export.sh" +} + +coronafs_local_matrix_requested() { + BUILD_PROFILE="storage" + local -a cache_modes=("none" "writeback") + local -a aio_modes=("io_uring" "threads") + local cache_mode="" + local aio_mode="" + + for cache_mode in "${cache_modes[@]}"; do + for aio_mode in "${aio_modes[@]}"; do + log "Running CoronaFS local export matrix case: cache=${cache_mode} aio=${aio_mode}" + CORONAFS_BENCH_EXPORT_CACHE_MODE="${cache_mode}" \ + CORONAFS_BENCH_EXPORT_AIO_MODE="${aio_mode}" \ + "${REPO_ROOT}/coronafs/scripts/benchmark-local-export.sh" + done + done +} + +lightningstor_bench_requested() { + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs + benchmark_lightningstor_performance +} + +set_lightningstor_runtime_s3_tuning() { + local streaming_threshold_bytes="${1:-default}" + local inline_put_max_bytes="${2:-134217728}" + local multipart_put_concurrency="${3:-8}" + local multipart_fetch_concurrency="${4:-8}" + + ssh_node_script node01 "${streaming_threshold_bytes}" "${inline_put_max_bytes}" "${multipart_put_concurrency}" "${multipart_fetch_concurrency}" <<'EOS' +set -euo pipefail +streaming_threshold_bytes="$1" +inline_put_max_bytes="$2" +multipart_put_concurrency="$3" +multipart_fetch_concurrency="$4" + +systemctl unset-environment \ + LIGHTNINGSTOR_S3_STREAMING_PUT_THRESHOLD_BYTES \ + LIGHTNINGSTOR_S3_INLINE_PUT_MAX_BYTES \ + LIGHTNINGSTOR_S3_MULTIPART_PUT_CONCURRENCY \ + LIGHTNINGSTOR_S3_MULTIPART_FETCH_CONCURRENCY + +if [[ "${streaming_threshold_bytes}" != "default" ]]; then + systemctl set-environment \ + LIGHTNINGSTOR_S3_STREAMING_PUT_THRESHOLD_BYTES="${streaming_threshold_bytes}" \ + LIGHTNINGSTOR_S3_INLINE_PUT_MAX_BYTES="${inline_put_max_bytes}" \ + LIGHTNINGSTOR_S3_MULTIPART_PUT_CONCURRENCY="${multipart_put_concurrency}" \ + LIGHTNINGSTOR_S3_MULTIPART_FETCH_CONCURRENCY="${multipart_fetch_concurrency}" +fi + +systemctl restart lightningstor.service +for _ in $(seq 1 30); do + if systemctl is-active --quiet lightningstor.service; then + exit 0 + fi + sleep 1 +done + +echo "timed out waiting for lightningstor.service to restart" >&2 +exit 1 +EOS +} + +benchmark_lightningstor_threshold_matrix() { + local tuned_8 tuned_16 baseline tuned_32 tuned_64 tuned_128 + + set_lightningstor_runtime_s3_tuning 8388608 + tuned_8="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning 16777216 + tuned_16="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning default + baseline="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning 33554432 + tuned_32="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning 67108864 + tuned_64="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning 134217728 + tuned_128="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning default + + printf 'threshold\tlarge_up\tlarge_down\tobject_mib\tsmall_count\tsmall_total_mib\tsmall_up\tsmall_down\tsmall_ops\tparallel_up\tparallel_down\tparallel_ops\n' + printf '8MiB\t%s\n' "${tuned_8}" + printf '16MiB\t%s\n' "${tuned_16}" + printf 'default(16MiB)\t%s\n' "${baseline}" + printf '32MiB\t%s\n' "${tuned_32}" + printf '64MiB\t%s\n' "${tuned_64}" + printf '128MiB\t%s\n' "${tuned_128}" +} + +lightningstor_threshold_matrix_requested() { + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs + benchmark_lightningstor_threshold_matrix +} + +benchmark_lightningstor_concurrency_matrix() { + local threshold_bytes="${LIGHTNINGSTOR_MATRIX_THRESHOLD_BYTES:-16777216}" + local tuned_4 tuned_8 tuned_16 + + set_lightningstor_runtime_s3_tuning "${threshold_bytes}" 134217728 4 4 + tuned_4="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning "${threshold_bytes}" 134217728 8 8 + tuned_8="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning "${threshold_bytes}" 134217728 16 16 + tuned_16="$(benchmark_lightningstor_performance)" + set_lightningstor_runtime_s3_tuning default + + printf 'concurrency\tlarge_up\tlarge_down\tobject_mib\tsmall_count\tsmall_total_mib\tsmall_up\tsmall_down\tsmall_ops\tparallel_up\tparallel_down\tparallel_ops\tthreshold_bytes\n' + printf '4\t%s\t%s\n' "${tuned_4}" "${threshold_bytes}" + printf '8(default)\t%s\t%s\n' "${tuned_8}" "${threshold_bytes}" + printf '16\t%s\t%s\n' "${tuned_16}" "${threshold_bytes}" +} + +lightningstor_concurrency_matrix_requested() { + LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" + BUILD_PROFILE="storage" + start_requested "${STORAGE_NODES[@]}" + validate_storage_bench_prereqs + benchmark_lightningstor_concurrency_matrix +} + storage_bench_requested() { LIGHTNINGSTOR_BENCH_CLIENT_NODE="node03" BUILD_PROFILE="storage" start_requested "${STORAGE_NODES[@]}" - validate_storage_units - validate_storage_control_plane + validate_storage_bench_prereqs benchmark_storage } @@ -6241,7 +7005,9 @@ clean_requested() { log "Removing runtime state for ${node}" find "$(runtime_dir "${node}")" -mindepth 1 -delete 2>/dev/null || true rmdir "$(runtime_dir "${node}")" 2>/dev/null || true - rm -f "$(build_link "${node}")" + if ! preserve_build_links_requested; then + rm -f "$(build_link "${node}")" + fi done fi } @@ -6281,6 +7047,14 @@ Commands: fresh-matrix clean local runtime state, rebuild on the host, start, and validate composed service configurations bench-storage start the cluster and benchmark CoronaFS plus LightningStor against the current running VMs fresh-bench-storage clean local runtime state, rebuild on the host, start, and benchmark CoronaFS plus LightningStor + bench-coronafs start the storage lab and benchmark CoronaFS against the current running VMs + bench-coronafs-local run the local single-process CoronaFS export benchmark without starting the VM lab + bench-coronafs-local-matrix run the local CoronaFS export benchmark across cache/aio combinations + bench-lightningstor start the storage lab and benchmark LightningStor against the current running VMs + bench-lightningstor-thresholds start the storage lab and benchmark LightningStor with 8/16/default/64/128 MiB multipart thresholds + bench-lightningstor-concurrency start the storage lab and benchmark LightningStor with 4/default/16 multipart fetch+put concurrency + bench-plasmavmc-image start the storage lab and benchmark the PlasmaVMC image import and clone path + bench-plasmavmc-runtime start the storage lab and benchmark the PlasmaVMC guest runtime path storage-bench start the storage lab (node01-05) and benchmark CoronaFS plus LightningStor fresh-storage-bench clean local runtime state, rebuild node01-05 on the host, start, and benchmark the storage lab stop Stop one or more VMs @@ -6299,6 +7073,14 @@ Examples: $0 fresh-matrix $0 bench-storage $0 fresh-bench-storage + $0 bench-coronafs + $0 bench-coronafs-local + $0 bench-coronafs-local-matrix + $0 bench-lightningstor + $0 bench-lightningstor-thresholds + $0 bench-lightningstor-concurrency + $0 bench-plasmavmc-image + $0 bench-plasmavmc-runtime $0 storage-bench $0 fresh-storage-bench $0 start node01 node02 node03 @@ -6324,6 +7106,14 @@ main() { fresh-matrix) fresh_matrix_requested "$@" ;; bench-storage) bench_storage_requested "$@" ;; fresh-bench-storage) fresh_bench_storage_requested "$@" ;; + bench-coronafs) coronafs_bench_requested ;; + bench-coronafs-local) coronafs_local_bench_requested ;; + bench-coronafs-local-matrix) coronafs_local_matrix_requested ;; + bench-lightningstor) lightningstor_bench_requested ;; + bench-lightningstor-thresholds) lightningstor_threshold_matrix_requested ;; + bench-lightningstor-concurrency) lightningstor_concurrency_matrix_requested ;; + bench-plasmavmc-image) plasmavmc_image_bench_requested ;; + bench-plasmavmc-runtime) plasmavmc_runtime_bench_requested ;; storage-bench) storage_bench_requested ;; fresh-storage-bench) fresh_storage_bench_requested ;; stop) stop_requested "$@" ;; diff --git a/nix/test-cluster/storage-node01.nix b/nix/test-cluster/storage-node01.nix index 1c1ab0f..d773127 100644 --- a/nix/test-cluster/storage-node01.nix +++ b/nix/test-cluster/storage-node01.nix @@ -67,8 +67,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; services.plasmavmc = { @@ -77,14 +77,17 @@ port = 50082; httpPort = 8084; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; lightningstorAddr = "10.100.0.11:50086"; - coronafsEndpoint = "http://10.100.0.11:50088"; + coronafsControllerEndpoint = "http://127.0.0.1:50088"; + coronafsNodeEndpoint = "http://127.0.0.1:50088"; }; services.coronafs = { enable = true; + metadataBackend = "chainfire"; + chainfireKeyPrefix = "/coronafs/test-cluster/storage/volumes"; port = 50088; advertiseHost = "10.100.0.11"; exportBasePort = 11000; @@ -114,9 +117,9 @@ readQuorum = 1; writeQuorum = 2; nodeMetricsPort = 9198; - chainfireAddr = "10.100.0.11:2379"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; - flaredbAddr = "10.100.0.11:2479"; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; zone = "zone-a"; region = "test"; }; diff --git a/nix/test-cluster/storage-node02.nix b/nix/test-cluster/storage-node02.nix index 33cdf43..77a12f2 100644 --- a/nix/test-cluster/storage-node02.nix +++ b/nix/test-cluster/storage-node02.nix @@ -47,7 +47,6 @@ nodeId = "node02"; raftAddr = "10.100.0.12:2480"; apiAddr = "10.100.0.12:2479"; - pdAddr = "10.100.0.11:2379"; initialPeers = [ "node01=10.100.0.11:2479" "node02=10.100.0.12:2479" @@ -65,8 +64,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; systemd.services.iam.environment = { diff --git a/nix/test-cluster/storage-node03.nix b/nix/test-cluster/storage-node03.nix index a6497f7..4cad017 100644 --- a/nix/test-cluster/storage-node03.nix +++ b/nix/test-cluster/storage-node03.nix @@ -47,7 +47,6 @@ nodeId = "node03"; raftAddr = "10.100.0.13:2480"; apiAddr = "10.100.0.13:2479"; - pdAddr = "10.100.0.11:2379"; initialPeers = [ "node01=10.100.0.11:2479" "node02=10.100.0.12:2479" @@ -65,8 +64,8 @@ services.iam = { enable = true; port = 50080; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; }; systemd.services.iam.environment = { diff --git a/nix/test-cluster/storage-node04.nix b/nix/test-cluster/storage-node04.nix index 3f2cd8d..db3caf6 100644 --- a/nix/test-cluster/storage-node04.nix +++ b/nix/test-cluster/storage-node04.nix @@ -8,6 +8,7 @@ imports = [ ./common.nix ../modules/plasmavmc.nix + ../modules/coronafs.nix ../modules/lightningstor.nix ../modules/node-agent.nix ]; @@ -33,15 +34,25 @@ services.plasmavmc = { enable = true; mode = "agent"; + coronafsNodeLocalAttach = true; + sharedLiveMigration = false; port = 50082; httpPort = 8084; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; controlPlaneAddr = "10.100.0.11:50082"; advertiseAddr = "10.100.0.21:50082"; lightningstorAddr = "10.100.0.11:50086"; - coronafsEndpoint = "http://10.100.0.11:50088"; + coronafsControllerEndpoint = "http://10.100.0.11:50088"; + coronafsNodeEndpoint = "http://127.0.0.1:50088"; + }; + + services.coronafs = { + enable = true; + mode = "node"; + port = 50088; + advertiseHost = "10.100.0.21"; }; services.lightningstor = { @@ -49,8 +60,8 @@ mode = "data"; port = 50086; distributedRequestTimeoutMs = 300000; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; zone = "zone-b"; region = "test"; @@ -58,7 +69,7 @@ services.node-agent = { enable = true; - chainfireEndpoint = "http://10.100.0.11:2379"; + chainfireEndpoint = config.photonTestCluster.chainfireControlPlaneAddrs; clusterId = "test-cluster"; nodeId = "node04"; intervalSecs = 10; diff --git a/nix/test-cluster/storage-node05.nix b/nix/test-cluster/storage-node05.nix index 4b31d32..3865057 100644 --- a/nix/test-cluster/storage-node05.nix +++ b/nix/test-cluster/storage-node05.nix @@ -8,6 +8,7 @@ imports = [ ./common.nix ../modules/plasmavmc.nix + ../modules/coronafs.nix ../modules/lightningstor.nix ../modules/node-agent.nix ]; @@ -33,15 +34,25 @@ services.plasmavmc = { enable = true; mode = "agent"; + coronafsNodeLocalAttach = true; + sharedLiveMigration = false; port = 50082; httpPort = 8084; iamAddr = "10.100.0.11:50080"; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; controlPlaneAddr = "10.100.0.11:50082"; advertiseAddr = "10.100.0.22:50082"; lightningstorAddr = "10.100.0.11:50086"; - coronafsEndpoint = "http://10.100.0.11:50088"; + coronafsControllerEndpoint = "http://10.100.0.11:50088"; + coronafsNodeEndpoint = "http://127.0.0.1:50088"; + }; + + services.coronafs = { + enable = true; + mode = "node"; + port = 50088; + advertiseHost = "10.100.0.22"; }; services.lightningstor = { @@ -49,8 +60,8 @@ mode = "data"; port = 50086; distributedRequestTimeoutMs = 300000; - chainfireAddr = "10.100.0.11:2379"; - flaredbAddr = "10.100.0.11:2479"; + chainfireAddr = config.photonTestCluster.chainfireControlPlaneAddrs; + flaredbAddr = config.photonTestCluster.flaredbControlPlaneAddrs; iamAddr = "10.100.0.11:50080"; zone = "zone-c"; region = "test"; @@ -58,7 +69,7 @@ services.node-agent = { enable = true; - chainfireEndpoint = "http://10.100.0.11:2379"; + chainfireEndpoint = config.photonTestCluster.chainfireControlPlaneAddrs; clusterId = "test-cluster"; nodeId = "node05"; intervalSecs = 10; diff --git a/nix/test-cluster/vm-bench-guest-image.nix b/nix/test-cluster/vm-bench-guest-image.nix index 2ac99ed..61e70a0 100644 --- a/nix/test-cluster/vm-bench-guest-image.nix +++ b/nix/test-cluster/vm-bench-guest-image.nix @@ -137,7 +137,7 @@ --size=512M \ --ioengine=libaio \ --direct=1 \ - --fdatasync=1 \ + --end_fsync=1 \ --output-format=json)" seq_write_mibps="$(printf '%s' "$seq_write_json" | jq -r '(.jobs[0].write.bw_bytes // 0) / 1048576')" diff --git a/nix/tests/deployer-vm-smoke.nix b/nix/tests/deployer-vm-smoke.nix new file mode 100644 index 0000000..0e19420 --- /dev/null +++ b/nix/tests/deployer-vm-smoke.nix @@ -0,0 +1,403 @@ +{ + pkgs, + photoncloudPackages, + smokeTargetToplevel, + desiredSystemOverrides ? { }, + expectedStatus ? "active", + expectCurrentSystemMatchesTarget ? true, + expectMarkerPresent ? true, +}: + +let + desiredSystemOverridesJson = builtins.toJSON desiredSystemOverrides; +in +{ + name = "deployer-vm-smoke"; + + nodes = { + deployer = + { ... }: + { + imports = [ + ../modules/chainfire.nix + ../modules/deployer.nix + ]; + + networking.hostName = "deployer"; + networking.firewall.enable = false; + networking.nameservers = [ "10.0.2.3" ]; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + + services.chainfire = { + enable = true; + nodeId = "deployer01"; + package = photoncloudPackages.chainfire-server; + }; + + services.deployer = { + enable = true; + package = photoncloudPackages.deployer-server; + ctlPackage = photoncloudPackages.deployer-ctl; + bindAddr = "0.0.0.0:8088"; + chainfireEndpoints = [ "http://127.0.0.1:2379" ]; + clusterId = "vm-smoke"; + bootstrapToken = "vm-smoke-bootstrap-token"; + adminToken = "vm-smoke-admin-token"; + requireChainfire = true; + allowUnknownNodes = false; + allowUnauthenticated = false; + bootstrapFlakeBundle = photoncloudPackages.plasmacloudFlakeBundle; + }; + + environment.systemPackages = with pkgs; [ + curl + gnutar + gzip + jq + photoncloudPackages.deployer-ctl + ]; + + virtualisation.memorySize = 1536; + virtualisation.diskSize = 4096; + system.stateVersion = "24.11"; + }; + + worker = + { ... }: + { + networking.hostName = "worker"; + networking.firewall.enable = false; + networking.nameservers = [ "10.0.2.3" ]; + + nix.settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + substituters = [ ]; + }; + + environment.systemPackages = with pkgs; [ + curl + gnutar + gzip + jq + photoncloudPackages.deployer-ctl + photoncloudPackages.nix-agent + ]; + + virtualisation.memorySize = 4096; + virtualisation.diskSize = 20480; + virtualisation.additionalPaths = [ smokeTargetToplevel ]; + system.stateVersion = "24.11"; + }; + }; + + testScript = '' + import json + import tempfile + import time + + desired_system_overrides = json.loads("""${desiredSystemOverridesJson}""") + + def write_remote_json(machine, path, payload): + machine.succeed( + "cat >{path} <<'EOF'\n{payload}\nEOF".format( + path=path, + payload=json.dumps(payload, indent=2, sort_keys=True), + ) + ) + + start_all() + serial_stdout_off() + + with tempfile.TemporaryDirectory(prefix="deployer-vm-smoke-"): + deployer.wait_for_unit("chainfire.service") + deployer.wait_for_unit("deployer.service") + deployer.wait_for_open_port(2379) + deployer.wait_for_open_port(8088) + + deployer_ip = worker.succeed("getent ahostsv4 deployer | awk '{print $1; exit}'").strip() + assert deployer_ip, "deployer did not report an IP address" + worker_machine_id = worker.succeed("cat /etc/machine-id").strip() + worker_ip = worker.succeed("hostname -I | awk '{print $1}'").strip() + assert worker_ip, "worker did not report an IP address" + + cluster_state = { + "cluster": { + "cluster_id": "vm-smoke", + "environment": "test", + }, + "nodes": [ + { + "node_id": "worker", + "machine_id": worker_machine_id, + "hostname": "worker", + "ip": worker_ip, + "roles": ["worker"], + "labels": { + "tier": "general", + }, + "pool": "general", + "node_class": "worker-linux", + "failure_domain": "lab-a", + "install_plan": { + "nixos_configuration": "vm-smoke-target", + "target_disk": "/dev/vda", + }, + "desired_system": { + "nixos_configuration": "vm-smoke-target", + **desired_system_overrides, + }, + "state": "pending", + } + ], + "node_classes": [ + { + "name": "worker-linux", + "description": "General-purpose worker profile for VM smoke tests", + "roles": ["worker"], + "labels": { + "tier": "general", + }, + } + ], + "pools": [ + { + "name": "general", + "description": "General-purpose worker pool", + "node_class": "worker-linux", + "labels": { + "pool.photoncloud.io/name": "general", + }, + } + ], + "enrollment_rules": [], + "services": [], + "instances": [], + "mtls_policies": [], + } + write_remote_json(deployer, "/tmp/cluster-state.json", cluster_state) + + deployer.succeed( + "deployer-ctl " + "--chainfire-endpoint http://127.0.0.1:2379 " + "--cluster-id vm-smoke " + "--cluster-namespace photoncloud " + "--deployer-namespace deployer " + "apply --config /tmp/cluster-state.json --prune", + timeout=120, + ) + print("cluster_state_applied") + + worker.succeed( + "curl -fsS " + "-H 'x-deployer-token: vm-smoke-bootstrap-token' " + "http://{deployer_ip}:8088/api/v1/bootstrap/flake-bundle " + "-o /tmp/plasmacloud-flake-bundle.tar.gz".format( + deployer_ip=deployer_ip, + ), + timeout=120, + ) + print("bundle_downloaded") + worker.succeed("mkdir -p /var/lib/photon-src", timeout=30) + worker.succeed("tar xzf /tmp/plasmacloud-flake-bundle.tar.gz -C /var/lib/photon-src", timeout=180) + print("bundle_extracted") + worker.succeed("test -f /var/lib/photon-src/flake.nix") + worker.succeed("test -d /var/lib/photon-src/nix") + worker.succeed("test -d /var/lib/photon-src/.bundle-inputs/nixpkgs") + worker.succeed("test -d /var/lib/photon-src/.bundle-inputs/rust-overlay") + worker.succeed("test -d /var/lib/photon-src/.bundle-inputs/flake-utils") + worker.succeed("test -d /var/lib/photon-src/.bundle-inputs/disko") + worker.succeed("test -d /var/lib/photon-src/.bundle-inputs/systems") + + phone_home_request = { + "machine_id": worker_machine_id, + "node_id": "worker", + "ip": worker_ip, + "metadata": { + "rack": "lab-a", + "sku": "vm-smoke", + }, + "hardware_facts": { + "architecture": "x86_64", + "cpu_model": "NixOS Test CPU", + "cpu_threads": 4, + "cpu_cores": 2, + "memory_bytes": 2147483648, + "disks": [ + { + "name": "vda", + "path": "/dev/vda", + "by_id": "/dev/disk/by-id/virtio-vm-smoke-root", + "size_bytes": 21474836480, + "model": "QEMU HARDDISK", + "serial": "vm-smoke-root", + "rotational": False, + } + ], + "nics": [ + { + "name": "eth0", + "mac_address": "52:54:00:12:34:56", + "oper_state": "up", + } + ], + }, + } + write_remote_json(worker, "/tmp/phone-home.json", phone_home_request) + + phone_home_response = worker.succeed( + "curl -fsS " + "-H 'content-type: application/json' " + "-H 'x-deployer-token: vm-smoke-bootstrap-token' " + "--data @/tmp/phone-home.json " + "http://{deployer_ip}:8088/api/v1/phone-home".format( + deployer_ip=deployer_ip, + ), + timeout=120, + ) + phone_home_payload = json.loads(phone_home_response) + assert phone_home_payload["node_id"] == "worker" + assert phone_home_payload["node_config"]["install_plan"]["nixos_configuration"] == "vm-smoke-target" + assert phone_home_payload["node_config"]["install_plan"]["target_disk"] == "/dev/vda" + print("phone_home_complete") + + node_dump_output = deployer.succeed( + "deployer-ctl " + "--chainfire-endpoint http://127.0.0.1:2379 " + "--cluster-id vm-smoke " + "--cluster-namespace photoncloud " + "--deployer-namespace deployer " + "dump --prefix photoncloud/clusters/vm-smoke/nodes/worker --format json" + ) + node_entries = [json.loads(line) for line in node_dump_output.splitlines() if line.strip()] + node_record = next(entry["value"] for entry in node_entries if entry["key"].endswith("/nodes/worker")) + print("node_record=", json.dumps(node_record, sort_keys=True)) + assert node_record["hardware_facts"]["architecture"] == "x86_64" + assert node_record["hardware_facts"]["disks"][0]["by_id"] == "/dev/disk/by-id/virtio-vm-smoke-root" + assert node_record["labels"]["hardware.architecture"] == "x86_64" + assert node_record["labels"]["hardware.disk_count"] == "1" + + worker.succeed( + "${photoncloudPackages.deployer-ctl}/bin/deployer-ctl " + "--chainfire-endpoint http://{deployer_ip}:2379 " + "--cluster-id vm-smoke " + "--cluster-namespace photoncloud " + "--deployer-namespace deployer " + "dump --prefix photoncloud/clusters/vm-smoke/nodes/worker --format json >/tmp/worker-chainfire-preflight.json".format( + deployer_ip=deployer_ip, + ), + timeout=120, + ) + print("chainfire_preflight_complete") + worker.succeed("rm -f /tmp/vm-smoke-nix-agent.log") + worker.succeed( + "systemd-run " + "--no-block " + "--unit vm-smoke-nix-agent " + "--service-type=exec " + "--property=StandardOutput=append:/tmp/vm-smoke-nix-agent.log " + "--property=StandardError=append:/tmp/vm-smoke-nix-agent.log " + "--setenv=PATH=/run/current-system/sw/bin " + "--setenv=RUST_LOG=info " + "-- " + "${photoncloudPackages.nix-agent}/bin/nix-agent " + "--apply " + "--once " + "--chainfire-endpoint http://{deployer_ip}:2379 " + "--cluster-namespace photoncloud " + "--cluster-id vm-smoke " + "--node-id worker " + "--flake-root /var/lib/photon-src".format( + deployer_ip=deployer_ip, + ), + timeout=60, + ) + worker.wait_until_succeeds( + "systemctl show -P ActiveState vm-smoke-nix-agent.service | grep -Eq 'active|inactive|failed'", + timeout=60, + ) + + def read_observed_system(): + observed_dump_output = deployer.succeed( + "deployer-ctl " + "--chainfire-endpoint http://127.0.0.1:2379 " + "--cluster-id vm-smoke " + "--cluster-namespace photoncloud " + "--deployer-namespace deployer " + "dump --prefix photoncloud/clusters/vm-smoke/nodes/worker/observed-system --format json" + ) + observed_entries = [json.loads(line) for line in observed_dump_output.splitlines() if line.strip()] + if not observed_entries: + return None + return observed_entries[0]["value"] + + observed = None + last_observed_snapshot = None + next_nix_agent_log_dump = time.time() + 30 + deadline = time.time() + 900 + while time.time() < deadline: + observed = read_observed_system() + if observed is None: + if time.time() >= next_nix_agent_log_dump: + print( + "nix_agent_log_tail=", + worker.succeed("tail -n 50 /tmp/vm-smoke-nix-agent.log || true"), + ) + next_nix_agent_log_dump += 30 + time.sleep(2) + continue + observed_snapshot = json.dumps(observed, sort_keys=True) + if observed_snapshot != last_observed_snapshot: + print("observed_system=", observed_snapshot) + last_observed_snapshot = observed_snapshot + status = observed.get("status") + if status in ("active", "failed", "rolled-back"): + break + if time.time() >= next_nix_agent_log_dump: + print( + "nix_agent_log_tail=", + worker.succeed("tail -n 50 /tmp/vm-smoke-nix-agent.log || true"), + ) + next_nix_agent_log_dump += 30 + time.sleep(5) + + assert observed is not None, "observed-system was never read" + nix_agent_state_output = worker.succeed( + "systemctl show " + "-P ActiveState " + "-P SubState " + "-P Result " + "-P ExecMainStatus " + "vm-smoke-nix-agent.service || true" + ) + nix_agent_status_output = worker.succeed( + "systemctl status vm-smoke-nix-agent.service --no-pager || true" + ) + nix_agent_log_output = worker.succeed("cat /tmp/vm-smoke-nix-agent.log || true") + print("nix_agent_state=", nix_agent_state_output) + print("nix_agent_systemd_status=", nix_agent_status_output) + print("nix_agent_log=", nix_agent_log_output) + print("observed_system=", json.dumps(observed, sort_keys=True)) + assert observed["status"] == "${expectedStatus}", observed + assert observed["nixos_configuration"] == "vm-smoke-target" + assert observed["flake_root"] == "/var/lib/photon-src" + assert observed["target_system"].startswith("/nix/store/") + current_system = worker.succeed("readlink -f /run/current-system").strip() + print("worker_current_system=", current_system) + if ${if expectCurrentSystemMatchesTarget then "True" else "False"}: + assert current_system == observed["target_system"], (current_system, observed) + else: + assert current_system != observed["target_system"], (current_system, observed) + assert current_system == observed["rollback_system"], (current_system, observed) + if ${if expectMarkerPresent then "True" else "False"}: + worker.succeed("test -f /run/current-system/etc/photon-vm-smoke-target") + else: + worker.succeed("test ! -f /run/current-system/etc/photon-vm-smoke-target") + ''; +} diff --git a/plasmavmc/Cargo.lock b/plasmavmc/Cargo.lock index 6e6024d..a255636 100644 --- a/plasmavmc/Cargo.lock +++ b/plasmavmc/Cargo.lock @@ -2,13 +2,48 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -51,9 +86,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -66,15 +101,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -110,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -125,6 +160,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -150,7 +197,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -161,7 +208,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -187,9 +234,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -197,9 +244,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -235,7 +282,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -247,7 +294,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "bytes", "form_urlencoded", "futures-util", @@ -269,7 +316,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -298,9 +345,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -333,6 +380,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bincode" version = "1.3.3" @@ -348,7 +401,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -357,7 +410,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -368,9 +421,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -384,6 +437,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -395,32 +457,33 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-unit" @@ -464,9 +527,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bzip2-sys" @@ -480,9 +543,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -619,13 +682,14 @@ dependencies = [ "http-body-util", "metrics", "metrics-exporter-prometheus", + "reqwest 0.12.28", "serde", "serde_json", "tokio", "toml 0.8.23", "tonic", "tonic-health", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tracing", "tracing-subscriber", @@ -672,9 +736,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -684,6 +748,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -696,9 +770,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -706,9 +780,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -718,27 +792,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -749,14 +823,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -926,9 +1000,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -945,9 +1029,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -969,7 +1053,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "unicode-xid", ] @@ -992,7 +1076,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1083,9 +1167,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -1262,9 +1346,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1277,9 +1361,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1287,15 +1371,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1315,38 +1399,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1356,7 +1440,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1372,9 +1455,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1397,6 +1480,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.3" @@ -1421,7 +1514,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1430,9 +1523,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1440,7 +1533,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1637,7 +1730,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "httparse", @@ -1674,13 +1767,13 @@ dependencies = [ "hyper 1.8.1", "hyper-util", "log", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -1698,14 +1791,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1714,7 +1806,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1724,7 +1816,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64 0.22.1", "iam-audit", @@ -1734,6 +1828,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1770,7 +1865,7 @@ dependencies = [ "iam-types", "jsonwebtoken", "rand 0.8.5", - "reqwest 0.12.25", + "reqwest 0.12.28", "serde", "serde_json", "sha2", @@ -1819,6 +1914,7 @@ dependencies = [ "iam-client", "iam-types", "serde_json", + "tokio", "tonic", "tracing", ] @@ -1854,9 +1950,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1990,19 +2086,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -2015,9 +2120,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -2049,9 +2154,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -2065,9 +2170,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2107,9 +2212,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" @@ -2117,7 +2222,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "plain", "redox_syscall 0.7.3", @@ -2151,9 +2256,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "pkg-config", @@ -2194,9 +2299,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2274,9 +2379,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -2299,7 +2404,7 @@ dependencies = [ "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -2418,9 +2523,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2428,6 +2533,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openraft" version = "0.9.21" @@ -2461,14 +2572,14 @@ dependencies = [ "proc-macro2", "quote", "semver", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "ordered-multimap" @@ -2509,6 +2620,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -2533,9 +2655,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -2543,9 +2665,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -2553,22 +2675,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -2581,7 +2703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] @@ -2594,29 +2716,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2728,7 +2850,7 @@ dependencies = [ "prismnet-server", "prismnet-types", "prost", - "reqwest 0.12.25", + "reqwest 0.12.28", "serde", "serde_json", "tempfile", @@ -2754,10 +2876,22 @@ dependencies = [ ] [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "polyval" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postcard" @@ -2800,7 +2934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2863,9 +2997,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2896,7 +3030,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.111", + "syn 2.0.117", "tempfile", ] @@ -2910,7 +3044,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3033,9 +3167,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", - "socket2 0.6.1", - "thiserror 2.0.17", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3043,9 +3177,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3053,10 +3187,10 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3071,16 +3205,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3115,7 +3249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3135,7 +3269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3144,14 +3278,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -3162,7 +3296,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3171,7 +3305,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3180,7 +3314,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3200,14 +3334,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3217,9 +3351,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3228,9 +3362,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -3284,9 +3418,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.25" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -3302,7 +3436,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -3310,14 +3444,14 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -3328,7 +3462,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3336,9 +3470,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -3354,9 +3488,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -3396,9 +3530,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" dependencies = [ "arrayvec", "borsh", @@ -3418,11 +3552,11 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -3443,25 +3577,25 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3489,9 +3623,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -3509,9 +3643,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -3527,24 +3661,24 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3576,11 +3710,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3589,9 +3723,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3620,7 +3754,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3694,10 +3828,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3715,7 +3850,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -3727,9 +3862,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3752,12 +3887,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3808,17 +3943,17 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", "percent-encoding", - "rustls 0.23.35", + "rustls 0.23.37", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -3836,7 +3971,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3858,7 +3993,7 @@ dependencies = [ "sqlx-core", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.111", + "syn 2.0.117", "tokio", "url", ] @@ -3871,7 +4006,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "crc", "dotenvy", @@ -3895,7 +4030,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -3919,7 +4054,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", ] @@ -3966,9 +4101,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3998,7 +4133,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4030,9 +4165,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -4052,11 +4187,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4067,18 +4202,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4133,9 +4268,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4148,9 +4283,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -4158,20 +4293,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4190,15 +4325,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.37", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -4207,9 +4342,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4263,12 +4398,12 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -4277,19 +4412,19 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "toml_datetime 0.7.0", "toml_parser", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -4309,7 +4444,7 @@ dependencies = [ "axum 0.7.9", "base64 0.22.1", "bytes", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -4342,7 +4477,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4380,9 +4515,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4400,14 +4535,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -4427,9 +4562,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -4445,14 +4580,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -4481,9 +4616,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -4523,9 +4658,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -4548,6 +4683,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4556,9 +4701,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -4598,9 +4743,9 @@ dependencies = [ [[package]] name = "validit" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fad49f3eae9c160c06b4d49700a99e75817f127cf856e494b56d5e23170020" +checksum = "4efba0434d5a0a62d4f22070b44ce055dc18cb64d4fa98276aa523dadfaba0e7" dependencies = [ "anyerror", ] @@ -4640,9 +4785,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -4655,9 +4800,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -4668,11 +4813,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4681,9 +4827,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4691,31 +4837,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -4743,14 +4889,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -4808,7 +4954,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4819,7 +4965,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -5070,13 +5216,19 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.50.0" @@ -5089,9 +5241,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -5136,28 +5288,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -5177,7 +5329,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] @@ -5217,7 +5369,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] diff --git a/plasmavmc/crates/plasmavmc-kvm/src/env.rs b/plasmavmc/crates/plasmavmc-kvm/src/env.rs index 526fcec..40f5c3e 100644 --- a/plasmavmc/crates/plasmavmc-kvm/src/env.rs +++ b/plasmavmc/crates/plasmavmc-kvm/src/env.rs @@ -10,6 +10,7 @@ pub const ENV_INITRD_PATH: &str = "PLASMAVMC_INITRD_PATH"; pub const ENV_RUNTIME_DIR: &str = "PLASMAVMC_RUNTIME_DIR"; pub const ENV_QMP_TIMEOUT_SECS: &str = "PLASMAVMC_QMP_TIMEOUT_SECS"; pub const ENV_NBD_MAX_QUEUES: &str = "PLASMAVMC_NBD_MAX_QUEUES"; +pub const ENV_NBD_AIO_MODE: &str = "PLASMAVMC_NBD_AIO_MODE"; /// Resolve QEMU binary path, falling back to a provided default. pub fn resolve_qemu_path(default: impl AsRef) -> PathBuf { @@ -54,6 +55,15 @@ pub fn resolve_nbd_max_queues() -> u16 { .unwrap_or(16) } +pub fn resolve_nbd_aio_mode() -> &'static str { + match std::env::var(ENV_NBD_AIO_MODE).ok().as_deref() { + Some("threads") => "threads", + Some("native") => "native", + Some("io_uring") => "io_uring", + _ => "io_uring", + } +} + #[cfg(test)] pub(crate) fn env_test_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); @@ -145,4 +155,29 @@ mod tests { assert_eq!(resolve_nbd_max_queues(), 12); std::env::remove_var(ENV_NBD_MAX_QUEUES); } + + #[test] + fn resolve_nbd_aio_mode_defaults_to_io_uring() { + let _guard = env_test_lock().lock().unwrap(); + std::env::remove_var(ENV_NBD_AIO_MODE); + assert_eq!(resolve_nbd_aio_mode(), "io_uring"); + } + + #[test] + fn resolve_nbd_aio_mode_accepts_supported_modes() { + let _guard = env_test_lock().lock().unwrap(); + for mode in ["threads", "native", "io_uring"] { + std::env::set_var(ENV_NBD_AIO_MODE, mode); + assert_eq!(resolve_nbd_aio_mode(), mode); + } + std::env::remove_var(ENV_NBD_AIO_MODE); + } + + #[test] + fn resolve_nbd_aio_mode_falls_back_for_invalid_values() { + let _guard = env_test_lock().lock().unwrap(); + std::env::set_var(ENV_NBD_AIO_MODE, "bogus"); + assert_eq!(resolve_nbd_aio_mode(), "io_uring"); + std::env::remove_var(ENV_NBD_AIO_MODE); + } } diff --git a/plasmavmc/crates/plasmavmc-kvm/src/lib.rs b/plasmavmc/crates/plasmavmc-kvm/src/lib.rs index b1c6645..2ebc400 100644 --- a/plasmavmc/crates/plasmavmc-kvm/src/lib.rs +++ b/plasmavmc/crates/plasmavmc-kvm/src/lib.rs @@ -8,8 +8,8 @@ mod qmp; use async_trait::async_trait; use env::{ - resolve_kernel_initrd, resolve_nbd_max_queues, resolve_qcow2_path, resolve_qemu_path, resolve_qmp_timeout_secs, - resolve_runtime_dir, ENV_QCOW2_PATH, + resolve_kernel_initrd, resolve_nbd_aio_mode, resolve_nbd_max_queues, resolve_qcow2_path, resolve_qemu_path, + resolve_qmp_timeout_secs, resolve_runtime_dir, ENV_QCOW2_PATH, }; use plasmavmc_hypervisor::{BackendCapabilities, HypervisorBackend, UnsupportedReason}; use plasmavmc_types::{ @@ -87,6 +87,15 @@ fn disk_source_arg(disk: &AttachedDisk) -> Result<(String, &'static str)> { } } +fn effective_disk_cache(disk: &AttachedDisk) -> DiskCache { + match (&disk.attachment, disk.cache) { + // Shared NBD-backed volumes perform better and behave more predictably + // with direct I/O than with host-side writeback caching. + (DiskAttachment::Nbd { .. }, DiskCache::Writeback) => DiskCache::None, + _ => disk.cache, + } +} + fn disk_cache_mode(cache: DiskCache) -> &'static str { match cache { DiskCache::None => "none", @@ -97,10 +106,9 @@ fn disk_cache_mode(cache: DiskCache) -> &'static str { fn disk_aio_mode(disk: &AttachedDisk) -> Option<&'static str> { match (&disk.attachment, disk.cache) { - (DiskAttachment::File { .. } | DiskAttachment::Nbd { .. }, DiskCache::None) => { - Some("native") - } - (DiskAttachment::File { .. } | DiskAttachment::Nbd { .. }, _) => Some("threads"), + (DiskAttachment::File { .. }, DiskCache::None) => Some("native"), + (DiskAttachment::File { .. }, _) => Some("threads"), + (DiskAttachment::Nbd { .. }, _) => Some(resolve_nbd_aio_mode()), (DiskAttachment::CephRbd { .. }, _) => None, } } @@ -202,9 +210,10 @@ fn build_disk_args(vm: &VirtualMachine, disks: &[AttachedDisk]) -> Result usize { std::env::var("PLASMAVMC_LIGHTNINGSTOR_MULTIPART_PART_SIZE") .ok() .and_then(|value| value.parse::().ok()) - .map(|value| value.clamp(MIN_MULTIPART_UPLOAD_PART_SIZE, MAX_MULTIPART_UPLOAD_PART_SIZE)) + .map(|value| { + value.clamp( + MIN_MULTIPART_UPLOAD_PART_SIZE, + MAX_MULTIPART_UPLOAD_PART_SIZE, + ) + }) .unwrap_or(DEFAULT_MULTIPART_UPLOAD_PART_SIZE) } @@ -946,6 +944,48 @@ fn raw_image_convert_parallelism() -> usize { .unwrap_or(DEFAULT_RAW_IMAGE_CONVERT_PARALLELISM) } +fn qemu_img_convert_to_qcow2_args( + source: &Path, + destination: &Path, + parallelism: &str, +) -> Vec { + vec![ + "convert".to_string(), + "-t".to_string(), + "none".to_string(), + "-T".to_string(), + "none".to_string(), + "-m".to_string(), + parallelism.to_string(), + "-c".to_string(), + "-O".to_string(), + "qcow2".to_string(), + source.to_string_lossy().into_owned(), + destination.to_string_lossy().into_owned(), + ] +} + +fn qemu_img_convert_to_raw_args( + source: &Path, + destination: &Path, + parallelism: &str, +) -> Vec { + vec![ + "convert".to_string(), + "-t".to_string(), + "none".to_string(), + "-T".to_string(), + "none".to_string(), + "-m".to_string(), + parallelism.to_string(), + "-W".to_string(), + "-O".to_string(), + "raw".to_string(), + source.to_string_lossy().into_owned(), + destination.to_string_lossy().into_owned(), + ] +} + fn attach_bearer(request: &mut Request, token: &str) -> Result<(), Status> { let value = MetadataValue::try_from(format!("Bearer {token}")) .map_err(|_| Status::internal("invalid bearer token"))?; @@ -971,3 +1011,96 @@ fn sanitize_identifier(value: &str) -> String { fn image_object_key(org_id: &str, project_id: &str, image_id: &str) -> String { format!("{org_id}/{project_id}/{image_id}.qcow2") } + +async fn ensure_cache_dir_permissions(path: &Path) -> Result<(), Status> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let permissions = std::fs::Permissions::from_mode(0o2770); + tokio::fs::set_permissions(path, permissions) + .await + .map_err(|e| { + Status::internal(format!( + "failed to set image cache directory permissions on {}: {e}", + path.display() + )) + })?; + } + Ok(()) +} + +async fn ensure_cache_file_permissions(path: &Path) -> Result<(), Status> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let permissions = std::fs::Permissions::from_mode(0o640); + tokio::fs::set_permissions(path, permissions) + .await + .map_err(|e| { + Status::internal(format!( + "failed to set image cache file permissions on {}: {e}", + path.display() + )) + })?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn qemu_img_convert_to_qcow2_args_include_parallel_direct_io() { + let args = qemu_img_convert_to_qcow2_args( + Path::new("/tmp/source.raw"), + Path::new("/tmp/image.qcow2"), + "8", + ); + assert_eq!( + args, + vec![ + "convert", + "-t", + "none", + "-T", + "none", + "-m", + "8", + "-c", + "-O", + "qcow2", + "/tmp/source.raw", + "/tmp/image.qcow2", + ] + ); + } + + #[test] + fn qemu_img_convert_to_raw_args_enable_fast_cache_rebuild() { + let args = qemu_img_convert_to_raw_args( + Path::new("/tmp/image.qcow2"), + Path::new("/tmp/image.raw"), + "8", + ); + assert_eq!( + args, + vec![ + "convert", + "-t", + "none", + "-T", + "none", + "-m", + "8", + "-W", + "-O", + "raw", + "/tmp/image.qcow2", + "/tmp/image.raw", + ] + ); + } +} diff --git a/plasmavmc/crates/plasmavmc-server/src/main.rs b/plasmavmc/crates/plasmavmc-server/src/main.rs index 4d82722..9c8cdcb 100644 --- a/plasmavmc/crates/plasmavmc-server/src/main.rs +++ b/plasmavmc/crates/plasmavmc-server/src/main.rs @@ -258,14 +258,16 @@ async fn main() -> Result<(), Box> { .map_err(|e| format!("Failed to connect to IAM server: {}", e))?; let auth_service = Arc::new(auth_service); - // Dedicated runtime for auth interceptors to avoid blocking the main async runtime - let auth_runtime = Arc::new(tokio::runtime::Runtime::new()?); + // gRPC interceptors are synchronous, so bridge into the current Tokio runtime + // from a blocking section instead of creating a nested runtime that would + // later be dropped from async context during shutdown. + let auth_handle = tokio::runtime::Handle::current(); let make_interceptor = |auth: Arc| { - let rt = auth_runtime.clone(); + let handle = auth_handle.clone(); move |mut req: Request<()>| -> Result, Status> { let auth = auth.clone(); tokio::task::block_in_place(|| { - rt.block_on(async move { + handle.block_on(async move { let tenant_context = auth.authenticate_request(&req).await?; req.extensions_mut().insert(tenant_context); Ok(req) diff --git a/plasmavmc/crates/plasmavmc-server/src/rest.rs b/plasmavmc/crates/plasmavmc-server/src/rest.rs index fb878f2..0c40c7a 100644 --- a/plasmavmc/crates/plasmavmc-server/src/rest.rs +++ b/plasmavmc/crates/plasmavmc-server/src/rest.rs @@ -239,7 +239,7 @@ async fn create_vm( cache: match disk.cache.as_deref() { Some("writeback") => DiskCache::Writeback as i32, Some("writethrough") => DiskCache::Writethrough as i32, - _ => DiskCache::None as i32, + _ => DiskCache::Writeback as i32, }, boot_index: disk.boot_index.unwrap_or_default(), }) diff --git a/plasmavmc/crates/plasmavmc-server/src/storage.rs b/plasmavmc/crates/plasmavmc-server/src/storage.rs index 53a7949..82c9afc 100644 --- a/plasmavmc/crates/plasmavmc-server/src/storage.rs +++ b/plasmavmc/crates/plasmavmc-server/src/storage.rs @@ -126,6 +126,13 @@ pub trait VmStore: Send + Sync { /// Save a persistent volume async fn save_volume(&self, volume: &Volume) -> StorageResult<()>; + /// Conditionally save a persistent volume if the currently stored value still matches `expected`. + async fn compare_and_swap_volume( + &self, + expected: Option<&Volume>, + volume: &Volume, + ) -> StorageResult; + /// Load a volume by ID async fn load_volume( &self, @@ -291,6 +298,14 @@ impl FlareDBStore { .map(|(_, value)| value)) } + async fn cas_get_versioned(&self, key: &str) -> StorageResult)>> { + let mut client = self.client.lock().await; + client + .cas_get(key.as_bytes().to_vec()) + .await + .map_err(|e| StorageError::FlareDB(format!("FlareDB get failed: {}", e))) + } + async fn cas_delete(&self, key: &str) -> StorageResult<()> { let key = key.as_bytes().to_vec(); let mut attempts = 0; @@ -538,6 +553,38 @@ impl VmStore for FlareDBStore { self.cas_put(&key, value).await } + async fn compare_and_swap_volume( + &self, + expected: Option<&Volume>, + volume: &Volume, + ) -> StorageResult { + let key = volume_key(&volume.org_id, &volume.project_id, &volume.id); + let current = self.cas_get_versioned(&key).await?; + + match (expected, ¤t) { + (None, None) => {} + (None, Some(_)) => return Ok(false), + (Some(_), None) => return Ok(false), + (Some(expected), Some((_, data))) => { + let current_volume: Volume = serde_json::from_slice(data)?; + if ¤t_volume != expected { + return Ok(false); + } + } + } + + let expected_version = current.as_ref().map(|(version, _)| *version).unwrap_or(0); + let value = serde_json::to_vec(volume)?; + let (success, _, _) = { + let mut client = self.client.lock().await; + client + .cas(key.as_bytes().to_vec(), value, expected_version) + .await + .map_err(|e| StorageError::FlareDB(format!("FlareDB CAS volume save failed: {}", e)))? + }; + Ok(success) + } + async fn load_volume( &self, org_id: &str, @@ -782,6 +829,36 @@ impl VmStore for FileStore { Ok(()) } + async fn compare_and_swap_volume( + &self, + expected: Option<&Volume>, + volume: &Volume, + ) -> StorageResult { + let mut state = self.load_state().unwrap_or_default(); + let current = state.volumes.iter().find(|candidate| { + candidate.org_id == volume.org_id + && candidate.project_id == volume.project_id + && candidate.id == volume.id + }); + + match (expected, current) { + (None, None) => {} + (None, Some(_)) => return Ok(false), + (Some(_), None) => return Ok(false), + (Some(expected), Some(current)) if current == expected => {} + (Some(_), Some(_)) => return Ok(false), + } + + state.volumes.retain(|existing| { + !(existing.org_id == volume.org_id + && existing.project_id == volume.project_id + && existing.id == volume.id) + }); + state.volumes.push(volume.clone()); + self.save_state(&state)?; + Ok(true) + } + async fn load_volume( &self, org_id: &str, @@ -817,3 +894,62 @@ impl VmStore for FileStore { .collect()) } } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + fn test_volume() -> Volume { + Volume::new("vol-1", "vol-1", "org-1", "project-1", 8) + } + + #[tokio::test] + async fn filestore_compare_and_swap_volume_creates_when_absent() { + let tempdir = tempdir().unwrap(); + let store = FileStore::new(Some(tempdir.path().join("state.json"))); + let volume = test_volume(); + + let created = store.compare_and_swap_volume(None, &volume).await.unwrap(); + assert!(created); + + let loaded = store + .load_volume(&volume.org_id, &volume.project_id, &volume.id) + .await + .unwrap() + .expect("volume should exist after CAS create"); + assert_eq!(loaded, volume); + } + + #[tokio::test] + async fn filestore_compare_and_swap_volume_rejects_stale_expected_value() { + let tempdir = tempdir().unwrap(); + let store = FileStore::new(Some(tempdir.path().join("state.json"))); + let original = test_volume(); + store.save_volume(&original).await.unwrap(); + + let mut current = original.clone(); + current.attached_to_vm = Some("vm-a".to_string()); + current.attached_to_node = Some("node04".to_string()); + current.attachment_generation = 1; + store.save_volume(¤t).await.unwrap(); + + let mut stale_update = original.clone(); + stale_update.attached_to_vm = Some("vm-b".to_string()); + stale_update.attached_to_node = Some("node05".to_string()); + stale_update.attachment_generation = 1; + + let swapped = store + .compare_and_swap_volume(Some(&original), &stale_update) + .await + .unwrap(); + assert!(!swapped); + + let loaded = store + .load_volume(¤t.org_id, ¤t.project_id, ¤t.id) + .await + .unwrap() + .expect("current volume should remain"); + assert_eq!(loaded, current); + } +} diff --git a/plasmavmc/crates/plasmavmc-server/src/vm_service.rs b/plasmavmc/crates/plasmavmc-server/src/vm_service.rs index 499f46e..9d616de 100644 --- a/plasmavmc/crates/plasmavmc-server/src/vm_service.rs +++ b/plasmavmc/crates/plasmavmc-server/src/vm_service.rs @@ -7,42 +7,39 @@ use crate::volume_manager::VolumeManager; use crate::watcher::StateSink; use creditservice_client::{Client as CreditServiceClient, ResourceType as CreditResourceType}; use dashmap::DashMap; -use iam_client::client::IamClientConfig; use iam_client::IamClient; +use iam_client::client::IamClientConfig; use iam_service_auth::{ - get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService, + AuthService, get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, }; use iam_types::{PolicyBinding, PrincipalRef, Scope}; use plasmavmc_api::proto::{ - image_service_server::ImageService, node_service_server::NodeService, - volume_service_server::VolumeService, vm_service_client::VmServiceClient, - vm_service_server::VmService, Architecture as ProtoArchitecture, AttachDiskRequest, - AttachNicRequest, CephRbdBacking, CordonNodeRequest, CreateImageRequest, CreateVmRequest, - CreateVolumeRequest, DeleteImageRequest, DeleteVmRequest, DeleteVolumeRequest, - DetachDiskRequest, DetachNicRequest, + Architecture as ProtoArchitecture, AttachDiskRequest, AttachNicRequest, CephRbdBacking, + CordonNodeRequest, CreateImageRequest, CreateVmRequest, CreateVolumeRequest, + DeleteImageRequest, DeleteVmRequest, DeleteVolumeRequest, DetachDiskRequest, DetachNicRequest, DiskBus as ProtoDiskBus, DiskCache as ProtoDiskCache, DiskSource as ProtoDiskSource, - DrainNodeRequest, Empty, GetImageRequest, GetNodeRequest, GetVmRequest, - GetVolumeRequest, HeartbeatNodeRequest, HypervisorType as ProtoHypervisorType, - Image as ProtoImage, ImageFormat as ProtoImageFormat, ImageStatus as ProtoImageStatus, - ListImagesRequest, ListImagesResponse, ListNodesRequest, ListNodesResponse, - ListVolumesRequest, ListVolumesResponse, ListVmsRequest, ListVmsResponse, - ManagedVolumeBacking, MigrateVmRequest, NicModel as ProtoNicModel, Node as ProtoNode, - NodeCapacity as ProtoNodeCapacity, NodeState as ProtoNodeState, OsType as ProtoOsType, - PrepareVmMigrationRequest, RebootVmRequest, RecoverVmRequest, RegisterExternalVolumeRequest, - ResetVmRequest, ResizeVolumeRequest, StartVmRequest, StopVmRequest, - UncordonNodeRequest, UpdateImageRequest, UpdateVmRequest, VirtualMachine, - Visibility as ProtoVisibility, VmEvent, VmSpec as ProtoVmSpec, + DrainNodeRequest, Empty, GetImageRequest, GetNodeRequest, GetVmRequest, GetVolumeRequest, + HeartbeatNodeRequest, HypervisorType as ProtoHypervisorType, Image as ProtoImage, + ImageFormat as ProtoImageFormat, ImageStatus as ProtoImageStatus, ListImagesRequest, + ListImagesResponse, ListNodesRequest, ListNodesResponse, ListVmsRequest, ListVmsResponse, + ListVolumesRequest, ListVolumesResponse, ManagedVolumeBacking, MigrateVmRequest, + NicModel as ProtoNicModel, Node as ProtoNode, NodeCapacity as ProtoNodeCapacity, + NodeState as ProtoNodeState, OsType as ProtoOsType, PrepareVmMigrationRequest, RebootVmRequest, + RecoverVmRequest, RegisterExternalVolumeRequest, ResetVmRequest, ResizeVolumeRequest, + StartVmRequest, StopVmRequest, UncordonNodeRequest, UpdateImageRequest, UpdateVmRequest, + VirtualMachine, Visibility as ProtoVisibility, VmEvent, VmSpec as ProtoVmSpec, VmState as ProtoVmState, VmStatus as ProtoVmStatus, Volume as ProtoVolume, VolumeBacking as ProtoVolumeBacking, VolumeDriverKind as ProtoVolumeDriverKind, VolumeFormat as ProtoVolumeFormat, VolumeStatus as ProtoVolumeStatus, WatchVmRequest, - disk_source::Source as ProtoDiskSourceKind, + disk_source::Source as ProtoDiskSourceKind, image_service_server::ImageService, + node_service_server::NodeService, vm_service_client::VmServiceClient, + vm_service_server::VmService, volume_service_server::VolumeService, }; use plasmavmc_hypervisor::HypervisorRegistry; use plasmavmc_types::{ - Architecture, DiskBus, DiskCache, DiskSource, HypervisorType, Image, ImageFormat, - ImageStatus, NetworkSpec, NicModel, Node, NodeCapacity, NodeId, NodeState, OsType, - Visibility, VmId, VmState, Volume, VolumeBacking, VolumeDriverKind, VolumeFormat, - VolumeStatus, + Architecture, DiskBus, DiskCache, DiskSource, HypervisorType, Image, ImageFormat, ImageStatus, + NetworkSpec, NicModel, Node, NodeCapacity, NodeId, NodeState, OsType, Visibility, VmId, + VmState, Volume, VolumeBacking, VolumeDriverKind, VolumeFormat, VolumeStatus, }; use std::collections::HashSet; use std::hash::{Hash, Hasher}; @@ -201,13 +198,15 @@ impl VmServiceImpl { } let normalized_iam_endpoint = Self::normalize_iam_endpoint(&iam_endpoint.into()); - let mut iam_config = IamClientConfig::new(normalized_iam_endpoint.clone()).with_timeout(5000); + let mut iam_config = + IamClientConfig::new(normalized_iam_endpoint.clone()).with_timeout(5000); if normalized_iam_endpoint.starts_with("http://") { iam_config = iam_config.without_tls(); } let iam_client = Arc::new(IamClient::connect(iam_config).await?); - let artifact_store = - ArtifactStore::from_env(&normalized_iam_endpoint).await?.map(Arc::new); + let artifact_store = ArtifactStore::from_env(&normalized_iam_endpoint) + .await? + .map(Arc::new); if artifact_store.is_some() { tracing::info!("LightningStor artifact backing enabled for VM disks"); } @@ -297,10 +296,7 @@ impl VmServiceImpl { .unwrap_or(endpoint); let host_port = authority.rsplit('@').next().unwrap_or(authority); let host = if let Some(rest) = host_port.strip_prefix('[') { - rest.split(']') - .next() - .unwrap_or_default() - .to_string() + rest.split(']').next().unwrap_or_default().to_string() } else { host_port.split(':').next().unwrap_or_default().to_string() }; @@ -322,10 +318,7 @@ impl VmServiceImpl { vm_id.hash(&mut hasher); let port = 4400 + (hasher.finish() % 1000) as u16; let host = Self::endpoint_host(endpoint)?; - Ok(( - format!("tcp:{host}:{port}"), - format!("tcp:0.0.0.0:{port}"), - )) + Ok((format!("tcp:{host}:{port}"), format!("tcp:0.0.0.0:{port}"))) } async fn connect_vm_service(endpoint: &str) -> Result, Status> { @@ -382,7 +375,9 @@ impl VmServiceImpl { .iam_client .create_service_account(&principal_id, &principal_id, project_id) .await - .map_err(|e| Status::unavailable(format!("IAM service account create failed: {e}")))?, + .map_err(|e| { + Status::unavailable(format!("IAM service account create failed: {e}")) + })?, }; let scope = Scope::project(project_id, org_id); @@ -473,9 +468,11 @@ impl VmServiceImpl { required_drivers .iter() .all(|driver| node.supported_volume_drivers.contains(driver)) - && required_storage_classes - .iter() - .all(|class| node.supported_storage_classes.iter().any(|item| item == class)) + && required_storage_classes.iter().all(|class| { + node.supported_storage_classes + .iter() + .any(|item| item == class) + }) } async fn select_target_node( @@ -486,22 +483,23 @@ impl VmServiceImpl { spec: &plasmavmc_types::VmSpec, ) -> Option { self.ensure_nodes_loaded().await; - let Ok((required_drivers, required_storage_classes)) = - self.required_storage_for_spec(org_id, project_id, spec).await + let Ok((required_drivers, required_storage_classes)) = self + .required_storage_for_spec(org_id, project_id, spec) + .await else { return None; }; - let mut nodes: Vec = self.nodes.iter().map(|entry| entry.value().clone()).collect(); + let mut nodes: Vec = self + .nodes + .iter() + .map(|entry| entry.value().clone()) + .collect(); nodes.sort_by(|lhs, rhs| lhs.id.as_str().cmp(rhs.id.as_str())); nodes.into_iter().find(|node| { node.state == NodeState::Ready && node.labels.contains_key(NODE_ENDPOINT_LABEL) && (node.hypervisors.is_empty() || node.hypervisors.contains(&hypervisor)) - && Self::node_supports_storage( - node, - &required_drivers, - &required_storage_classes, - ) + && Self::node_supports_storage(node, &required_drivers, &required_storage_classes) }) } @@ -570,7 +568,9 @@ impl VmServiceImpl { ProtoDiskCache::None => DiskCache::None, ProtoDiskCache::Writeback => DiskCache::Writeback, ProtoDiskCache::Writethrough => DiskCache::Writethrough, - ProtoDiskCache::Unspecified => DiskCache::None, + // Writeback keeps QEMU/NBD flush semantics while avoiding the + // worst direct-I/O path for shared CoronaFS volumes. + ProtoDiskCache::Unspecified => DiskCache::Writeback, } } @@ -1171,6 +1171,9 @@ impl VmServiceImpl { format: Self::map_volume_format_proto(volume.format) as i32, status: Self::map_volume_status_proto(volume.status) as i32, attached_to_vm: volume.attached_to_vm.clone().unwrap_or_default(), + attached_to_node: volume.attached_to_node.clone().unwrap_or_default(), + attachment_generation: volume.attachment_generation, + last_flushed_attachment_generation: volume.last_flushed_attachment_generation, metadata: volume.metadata.clone(), labels: volume.labels.clone(), created_at: volume.created_at as i64, @@ -1548,6 +1551,31 @@ impl VmServiceImpl { Ok(()) } + async fn rollback_prepared_vm_resources( + &self, + vm: &plasmavmc_types::VirtualMachine, + delete_auto_delete_volumes: bool, + ) { + if let Err(error) = self.detach_prismnet_ports(vm).await { + tracing::warn!( + vm_id = %vm.id, + error = %error, + "Failed to detach PrismNET ports during VM rollback" + ); + } + if let Err(error) = self + .volume_manager + .rollback_vm_volumes(vm, delete_auto_delete_volumes) + .await + { + tracing::warn!( + vm_id = %vm.id, + error = %error, + "Failed to roll back provisional VM volumes" + ); + } + } + /// Spawn a background health monitor that periodically refreshes VM status. pub fn start_health_monitor(self: Arc, interval: Duration) { if interval.as_secs() == 0 { @@ -1887,6 +1915,19 @@ impl VmServiceImpl { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unspecified_disk_cache_defaults_to_writeback() { + assert_eq!( + VmServiceImpl::map_disk_cache(ProtoDiskCache::Unspecified as i32), + DiskCache::Writeback + ); + } +} + impl StateSink for VmServiceImpl { fn on_vm_updated( &self, @@ -1999,13 +2040,6 @@ impl VmService for VmServiceImpl { if let Some(ref node_id) = self.local_node_id { vm.node_id = Some(NodeId::new(node_id.clone())); } - let attached_disks = self.volume_manager.prepare_vm_volumes(&mut vm).await?; - - // Attach to PrismNET ports if configured - if let Err(e) = self.attach_prismnet_ports(&mut vm).await { - tracing::warn!("Failed to attach PrismNET ports: {}", e); - // Continue anyway - network attachment is optional - } // CreditService Admission Control (2-phase commit) let reservation_id: Option = if let Some(ref credit_svc) = self.credit_service { @@ -2076,6 +2110,30 @@ impl VmService for VmServiceImpl { None }; + let attached_disks = match self.volume_manager.prepare_vm_volumes(&mut vm).await { + Ok(attached_disks) => attached_disks, + Err(error) => { + if let (Some(ref credit_svc), Some(ref res_id)) = + (&self.credit_service, &reservation_id) + { + let mut client = credit_svc.write().await; + if let Err(release_err) = client + .release_reservation(res_id, format!("VM volume preparation failed: {}", error)) + .await + { + tracing::warn!("Failed to release reservation {}: {}", res_id, release_err); + } + } + return Err(error); + } + }; + + // Attach to PrismNET ports if configured + if let Err(e) = self.attach_prismnet_ports(&mut vm).await { + tracing::warn!("Failed to attach PrismNET ports: {}", e); + // Continue anyway - network attachment is optional + } + // Create VM let handle = match backend.create(&vm, &attached_disks).await { Ok(h) => h, @@ -2102,15 +2160,30 @@ impl VmService for VmServiceImpl { tracing::info!(reservation_id = %res_id, "Released reservation after VM creation failure"); } } - let _ = self.volume_manager.release_vm_volumes(&vm).await; + self.rollback_prepared_vm_resources(&vm, true).await; return Err(Self::to_status_code(e)); } }; - let status = backend - .status(&handle) - .await - .map_err(Self::to_status_code)?; + let status = match backend.status(&handle).await { + Ok(status) => status, + Err(error) => { + if let (Some(ref credit_svc), Some(ref res_id)) = + (&self.credit_service, &reservation_id) + { + let mut client = credit_svc.write().await; + if let Err(release_err) = client + .release_reservation(res_id, format!("VM status failed after creation: {}", error)) + .await + { + tracing::warn!("Failed to release reservation {}: {}", res_id, release_err); + } + } + let _ = backend.delete(&handle).await; + self.rollback_prepared_vm_resources(&vm, true).await; + return Err(Self::to_status_code(error)); + } + }; vm.status = status.clone(); vm.state = status.actual_state; @@ -2381,7 +2454,10 @@ impl VmService for VmServiceImpl { } if self.is_control_plane_scheduler() { - if let Some(vm) = self.ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id).await { + if let Some(vm) = self + .ensure_vm_loaded(&req.org_id, &req.project_id, &req.vm_id) + .await + { if let Some(node_id) = vm.node_id.as_ref() { if let Some(node) = self.ensure_node_loaded(node_id.as_str()).await { if let Some(endpoint) = node.labels.get(NODE_ENDPOINT_LABEL) { @@ -2522,10 +2598,13 @@ impl VmService for VmServiceImpl { "Recreating local VM handle for stopped VM restart" ); let attached_disks = self.volume_manager.prepare_vm_volumes(&mut vm).await?; - handle = backend - .create(&vm, &attached_disks) - .await - .map_err(Self::to_status_code)?; + handle = match backend.create(&vm, &attached_disks).await { + Ok(handle) => handle, + Err(error) => { + let _ = self.volume_manager.release_vm_volumes(&vm).await; + return Err(Self::to_status_code(error)); + } + }; self.handles.insert(key.clone(), handle.clone()); self.persist_handle(&vm.org_id, &vm.project_id, &vm.id.to_string(), &handle) .await; @@ -2620,6 +2699,7 @@ impl VmService for VmServiceImpl { .stop(&handle, timeout) .await .map_err(Self::to_status_code)?; + self.volume_manager.flush_vm_volumes(&vm).await?; let status = backend .status(&handle) .await @@ -2871,11 +2951,7 @@ impl VmService for VmServiceImpl { let Some(backend) = self.hypervisor_registry.get(vm.hypervisor) else { return Err(Status::failed_precondition("Hypervisor not available")); }; - if !backend.capabilities().live_migration { - return Err(Status::failed_precondition( - "Live migration not supported by hypervisor", - )); - } + let backend_supports_live_migration = backend.capabilities().live_migration; if let Some(local_node) = self.local_node_id.as_deref() { if !req.destination_node_id.is_empty() && req.destination_node_id == local_node { return Err(Status::invalid_argument( @@ -2893,11 +2969,6 @@ impl VmService for VmServiceImpl { "Destination node missing plasmavmc_endpoint label", )); }; - if !dest_node.shared_live_migration { - return Err(Status::failed_precondition( - "Destination node does not support shared-storage live migration", - )); - } let (required_drivers, required_storage_classes) = self .required_storage_for_spec(&vm.org_id, &vm.project_id, &vm.spec) .await?; @@ -2910,6 +2981,155 @@ impl VmService for VmServiceImpl { "Destination node does not support the VM's required storage backends", )); } + if !dest_node.shared_live_migration { + if !self + .volume_manager + .supports_cold_relocation_for_vm(&vm) + .await? + { + return Err(Status::failed_precondition( + "Destination node requires cold relocation, but the VM uses non-portable storage", + )); + } + + let restart_on_destination = matches!( + vm.state, + VmState::Running | VmState::Starting | VmState::Migrating + ); + let stop_timeout = Duration::from_secs(if req.timeout_seconds == 0 { + 120 + } else { + req.timeout_seconds as u64 + }); + if restart_on_destination { + backend + .stop(&handle, stop_timeout) + .await + .map_err(Self::to_status_code)?; + self.volume_manager.flush_vm_volumes(&vm).await?; + let status = backend + .status(&handle) + .await + .map_err(Self::to_status_code)?; + vm.status = status.clone(); + vm.state = status.actual_state; + self.vms.insert(key.clone(), vm.clone()); + self.persist_vm(&vm).await; + } + + self.volume_manager.release_vm_volumes(&vm).await?; + backend + .delete(&handle) + .await + .map_err(Self::to_status_code)?; + self.handles.remove(&key); + self.delete_persisted_handle(&vm.org_id, &vm.project_id, &vm.id.to_string()) + .await; + + let mut client = Self::connect_vm_service(endpoint).await?; + let mut recover_req = Request::new(RecoverVmRequest { + org_id: vm.org_id.clone(), + project_id: vm.project_id.clone(), + vm_id: vm.id.to_string(), + name: vm.name.clone(), + spec: Some(Self::types_spec_to_proto(&vm.spec)), + hypervisor: Self::map_hv_proto(vm.hypervisor) as i32, + metadata: vm.metadata.clone(), + labels: vm.labels.clone(), + start: restart_on_destination, + }); + self.attach_internal_auth(&mut recover_req, &vm.org_id, &vm.project_id) + .await?; + + return match client.recover_vm(recover_req).await { + Ok(remote_vm) => { + let remote_vm = remote_vm.into_inner(); + let typed_vm = Self::proto_vm_to_types(&remote_vm)?; + self.vms.insert(key, typed_vm.clone()); + self.persist_vm(&typed_vm).await; + Ok(Response::new(remote_vm)) + } + Err(status) => { + tracing::warn!( + vm_id = %req.vm_id, + destination_node_id = %req.destination_node_id, + error = %status, + "Cold relocation failed; attempting source-side recovery" + ); + let restore_result: Result<(), Status> = async { + let attached_disks = + self.volume_manager.prepare_vm_volumes(&mut vm).await?; + let restored_handle = match backend.create(&vm, &attached_disks).await { + Ok(handle) => handle, + Err(error) => { + let _ = self.volume_manager.release_vm_volumes(&vm).await; + return Err(Self::to_status_code(error)); + } + }; + if restart_on_destination { + if let Err(error) = backend.start(&restored_handle).await { + let _ = backend.delete(&restored_handle).await; + let _ = self.volume_manager.release_vm_volumes(&vm).await; + return Err(Self::to_status_code(error)); + } + } + let restored_status = match backend.status(&restored_handle).await { + Ok(status) => status, + Err(error) => { + let _ = backend.delete(&restored_handle).await; + let _ = self.volume_manager.release_vm_volumes(&vm).await; + return Err(Self::to_status_code(error)); + } + }; + vm.status = restored_status.clone(); + vm.state = restored_status.actual_state; + self.handles.insert(key.clone(), restored_handle.clone()); + self.persist_handle( + &vm.org_id, + &vm.project_id, + &vm.id.to_string(), + &restored_handle, + ) + .await; + Ok(()) + } + .await; + + match restore_result { + Ok(()) => { + vm.status.last_error = Some(format!( + "cold relocation to {} failed: {}", + req.destination_node_id, + status.message() + )); + } + Err(restore_error) => { + vm.state = VmState::Error; + vm.status.actual_state = VmState::Error; + vm.status.last_error = Some(format!( + "cold relocation to {} failed: {}; source recovery failed: {}", + req.destination_node_id, + status.message(), + restore_error.message() + )); + } + } + self.vms.insert(key, vm.clone()); + self.persist_vm(&vm).await; + Err(Status::failed_precondition(format!( + "cold relocation to {} failed: {}", + req.destination_node_id, + status.message() + ))) + } + }; + } + + if !backend_supports_live_migration { + return Err(Status::failed_precondition( + "Live migration not supported by hypervisor", + )); + } let (destination_uri, listen_uri) = Self::derive_migration_uris(endpoint, &req.destination_node_id, &req.vm_id)?; @@ -2973,8 +3193,12 @@ impl VmService for VmServiceImpl { ); } self.handles.remove(&key); - self.delete_persisted_handle(&vm.org_id, &vm.project_id, &vm.id.to_string()) - .await; + self.delete_persisted_handle( + &vm.org_id, + &vm.project_id, + &vm.id.to_string(), + ) + .await; } self.vms.insert(key, vm.clone()); @@ -2992,7 +3216,7 @@ impl VmService for VmServiceImpl { }; } Err(Status::failed_precondition( - "destination_node_id is required for live migration", + "destination_node_id is required for migration", )) } @@ -3149,17 +3373,28 @@ impl VmService for VmServiceImpl { tracing::warn!("Failed to attach PrismNET ports: {}", e); } - let handle = backend - .create(&vm, &attached_disks) - .await - .map_err(Self::to_status_code)?; + let handle = match backend.create(&vm, &attached_disks).await { + Ok(handle) => handle, + Err(error) => { + self.rollback_prepared_vm_resources(&vm, false).await; + return Err(Self::to_status_code(error)); + } + }; if req.start { - backend.start(&handle).await.map_err(Self::to_status_code)?; + if let Err(error) = backend.start(&handle).await { + let _ = backend.delete(&handle).await; + self.rollback_prepared_vm_resources(&vm, false).await; + return Err(Self::to_status_code(error)); + } } - let status = backend - .status(&handle) - .await - .map_err(Self::to_status_code)?; + let status = match backend.status(&handle).await { + Ok(status) => status, + Err(error) => { + let _ = backend.delete(&handle).await; + self.rollback_prepared_vm_resources(&vm, false).await; + return Err(Self::to_status_code(error)); + } + }; vm.status = status.clone(); vm.state = status.actual_state; @@ -3227,22 +3462,28 @@ impl VmService for VmServiceImpl { } let mut staged_vm = vm.clone(); staged_vm.spec.disks = vec![disk_spec.clone()]; - let mut attached_disks = self.volume_manager.prepare_vm_volumes(&mut staged_vm).await?; + let mut attached_disks = self + .volume_manager + .prepare_vm_volumes(&mut staged_vm) + .await?; disk_spec = staged_vm .spec .disks - .into_iter() - .next() + .first() + .cloned() .ok_or_else(|| Status::internal("failed to materialize disk spec"))?; let attached_disk = attached_disks .pop() .ok_or_else(|| Status::internal("failed to resolve attached disk"))?; // Attach disk via backend - backend - .attach_disk(&handle, &attached_disk) - .await - .map_err(Self::to_status_code)?; + if let Err(error) = backend.attach_disk(&handle, &attached_disk).await { + let _ = self + .volume_manager + .rollback_vm_volumes(&staged_vm, true) + .await; + return Err(Self::to_status_code(error)); + } vm.spec.disks.push(disk_spec); self.vms.insert(key.clone(), vm.clone()); @@ -3516,7 +3757,9 @@ impl VolumeService for VmServiceImpl { return Err(Status::invalid_argument("name is required")); } if req.size_gib == 0 { - return Err(Status::invalid_argument("size_gib must be greater than zero")); + return Err(Status::invalid_argument( + "size_gib must be greater than zero", + )); } let driver = Self::map_volume_driver( @@ -3601,7 +3844,10 @@ impl VolumeService for VmServiceImpl { .await?; let req = request.into_inner(); - let mut volumes = self.volume_manager.list_volumes(&org_id, &project_id).await?; + let mut volumes = self + .volume_manager + .list_volumes(&org_id, &project_id) + .await?; volumes.sort_by(|lhs, rhs| lhs.created_at.cmp(&rhs.created_at)); let mut proto_volumes: Vec = volumes.iter().map(Self::types_volume_to_proto).collect(); @@ -3668,7 +3914,9 @@ impl VolumeService for VmServiceImpl { .await?; if req.size_gib == 0 { - return Err(Status::invalid_argument("size_gib must be greater than zero")); + return Err(Status::invalid_argument( + "size_gib must be greater than zero", + )); } let volume = self .volume_manager @@ -3700,7 +3948,9 @@ impl VolumeService for VmServiceImpl { return Err(Status::invalid_argument("name is required")); } if req.size_gib == 0 { - return Err(Status::invalid_argument("size_gib must be greater than zero")); + return Err(Status::invalid_argument( + "size_gib must be greater than zero", + )); } let driver = Self::map_volume_driver( ProtoVolumeDriverKind::try_from(req.driver).unwrap_or(ProtoVolumeDriverKind::CephRbd), @@ -3753,8 +4003,7 @@ impl ImageService for VmServiceImpl { request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; - let (org_id, project_id) = - Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; + let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; self.auth .authorize( &tenant, @@ -3784,8 +4033,9 @@ impl ImageService for VmServiceImpl { image.visibility = Self::map_visibility( ProtoVisibility::try_from(req.visibility).unwrap_or(ProtoVisibility::Private), ); - image.os_type = - Self::map_os_type(ProtoOsType::try_from(req.os_type).unwrap_or(ProtoOsType::Unspecified)); + image.os_type = Self::map_os_type( + ProtoOsType::try_from(req.os_type).unwrap_or(ProtoOsType::Unspecified), + ); image.os_version = req.os_version; image.architecture = Self::map_architecture(req.architecture); image.min_disk_gib = req.min_disk_gib; @@ -3813,10 +4063,9 @@ impl ImageService for VmServiceImpl { image.size_bytes = imported.size_bytes; image.checksum = imported.checksum; image.updated_at = Self::now_epoch(); - image.metadata.insert( - "source_url".to_string(), - req.source_url.clone(), - ); + image + .metadata + .insert("source_url".to_string(), req.source_url.clone()); if source_format != image.format { image.metadata.insert( "source_format".to_string(), @@ -3845,8 +4094,7 @@ impl ImageService for VmServiceImpl { request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; - let (org_id, project_id) = - Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; + let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; let req = request.into_inner(); self.auth .authorize( @@ -3871,8 +4119,7 @@ impl ImageService for VmServiceImpl { request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; - let (org_id, project_id) = - Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; + let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; self.auth .authorize( &tenant, @@ -3885,10 +4132,7 @@ impl ImageService for VmServiceImpl { let mut images: Vec = self .images .iter() - .filter(|entry| { - entry.key().org_id == org_id - && entry.key().project_id == project_id - }) + .filter(|entry| entry.key().org_id == org_id && entry.key().project_id == project_id) .map(|entry| Self::types_image_to_proto(entry.value())) .collect(); images.sort_by(|lhs, rhs| lhs.created_at.cmp(&rhs.created_at)); @@ -3904,8 +4148,7 @@ impl ImageService for VmServiceImpl { request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; - let (org_id, project_id) = - Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; + let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; let req = request.into_inner(); self.auth .authorize( @@ -3946,8 +4189,7 @@ impl ImageService for VmServiceImpl { request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; - let (org_id, project_id) = - Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; + let (org_id, project_id) = Self::resolve_image_tenant(&tenant, &request.get_ref().org_id)?; let req = request.into_inner(); self.auth .authorize( @@ -3967,13 +4209,17 @@ impl ImageService for VmServiceImpl { } if let Some(store) = self.artifact_store.as_ref() { - store.delete_image(&org_id, &project_id, &req.image_id).await?; + store + .delete_image(&org_id, &project_id, &req.image_id) + .await?; } self.images.remove(&key); self.store .delete_image(&org_id, &project_id, &req.image_id) .await - .map_err(|error| Status::internal(format!("failed to delete image metadata: {error}")))?; + .map_err(|error| { + Status::internal(format!("failed to delete image metadata: {error}")) + })?; Ok(Response::new(Empty {})) } diff --git a/plasmavmc/crates/plasmavmc-server/src/volume_manager.rs b/plasmavmc/crates/plasmavmc-server/src/volume_manager.rs index fa10f26..67d3fc9 100644 --- a/plasmavmc/crates/plasmavmc-server/src/volume_manager.rs +++ b/plasmavmc/crates/plasmavmc-server/src/volume_manager.rs @@ -1,10 +1,11 @@ use crate::artifact_store::ArtifactStore; use crate::storage::VmStore; use plasmavmc_types::{ - AttachedDisk, DiskAttachment, DiskCache, DiskSource, DiskSpec, VirtualMachine, Volume, VolumeBacking, + AttachedDisk, DiskAttachment, DiskSource, DiskSpec, VirtualMachine, Volume, VolumeBacking, VolumeDriverKind, VolumeFormat, VolumeStatus, }; use serde::{Deserialize, Serialize}; +use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::process::Command; @@ -14,6 +15,9 @@ use uuid::Uuid; const CORONAFS_IMAGE_CONVERT_PARALLELISM: &str = "16"; const AUTO_DELETE_VOLUME_METADATA_KEY: &str = "plasmavmc.auto_delete"; const AUTO_DELETE_VOLUME_SOURCE_METADATA_KEY: &str = "plasmavmc.auto_delete_source"; +const CORONAFS_IMAGE_SOURCE_ID_METADATA_KEY: &str = "plasmavmc.coronafs_image_source_id"; +const CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY: &str = "plasmavmc.coronafs_image_seed_pending"; +const VOLUME_METADATA_CAS_RETRIES: usize = 16; #[derive(Clone, Debug)] struct CephClusterConfig { @@ -26,18 +30,25 @@ struct CephClusterConfig { #[derive(Clone, Debug)] struct CoronaFsClient { endpoint: String, + endpoints: Vec, http: reqwest::Client, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] struct CoronaFsVolumeResponse { id: String, size_bytes: u64, + #[serde(default)] + format: Option, + #[serde(default)] + node_local: bool, + #[serde(default)] + materialized_from: Option, path: String, export: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] struct CoronaFsExport { uri: String, port: u16, @@ -47,6 +58,12 @@ struct CoronaFsExport { #[derive(Debug, Serialize)] struct CoronaFsCreateRequest { size_bytes: u64, + #[serde(skip_serializing_if = "Option::is_none")] + format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + backing_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + backing_format: Option, } #[derive(Debug, Serialize)] @@ -54,6 +71,32 @@ struct CoronaFsResizeRequest { size_bytes: u64, } +#[derive(Debug, Serialize)] +struct CoronaFsMaterializeRequest { + source_uri: String, + size_bytes: u64, + #[serde(skip_serializing_if = "Option::is_none")] + format: Option, + #[serde(default)] + lazy: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum CoronaFsVolumeFormat { + Raw, + Qcow2, +} + +impl CoronaFsVolumeFormat { + fn as_qemu_arg(self) -> &'static str { + match self { + Self::Raw => "raw", + Self::Qcow2 => "qcow2", + } + } +} + #[derive(Debug, Deserialize)] struct QemuImageInfo { format: String, @@ -61,6 +104,12 @@ struct QemuImageInfo { virtual_size: u64, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct CoronaFsProvisionOutcome { + format: VolumeFormat, + deferred_image_seed: bool, +} + #[derive(Clone)] pub struct VolumeManager { store: Arc, @@ -68,7 +117,10 @@ pub struct VolumeManager { managed_root: PathBuf, supported_storage_classes: Vec, ceph_cluster: Option, - coronafs: Option, + coronafs_controller: Option, + coronafs_node: Option, + coronafs_node_local_attach: bool, + local_node_id: Option, } impl VolumeManager { @@ -88,15 +140,14 @@ impl VolumeManager { .filter(|item| !item.is_empty()) .map(ToOwned::to_owned) .collect(), - user: std::env::var("PLASMAVMC_CEPH_USER") - .unwrap_or_else(|_| "admin".to_string()), + user: std::env::var("PLASMAVMC_CEPH_USER").unwrap_or_else(|_| "admin".to_string()), secret: std::env::var("PLASMAVMC_CEPH_SECRET").ok(), }); - let coronafs = std::env::var("PLASMAVMC_CORONAFS_ENDPOINT") + let (coronafs_controller, coronafs_node) = resolve_coronafs_clients(); + let coronafs_node_local_attach = coronafs_node_local_attach_enabled(); + let local_node_id = std::env::var("PLASMAVMC_NODE_ID") .ok() - .map(|endpoint| endpoint.trim().to_string()) - .filter(|endpoint| !endpoint.is_empty()) - .map(|endpoint| CoronaFsClient::new(endpoint)); + .filter(|value| !value.trim().is_empty()); Self { store, @@ -104,14 +155,17 @@ impl VolumeManager { managed_root, supported_storage_classes: { let mut classes = vec!["managed-default".to_string()]; - if coronafs.is_some() { + if coronafs_controller.is_some() || coronafs_node.is_some() { classes.push("coronafs-managed".to_string()); } classes.push("ceph-rbd".to_string()); classes }, ceph_cluster, - coronafs, + coronafs_controller, + coronafs_node, + coronafs_node_local_attach, + local_node_id, } } @@ -127,6 +181,156 @@ impl VolumeManager { self.supported_storage_classes.clone() } + fn coronafs_provisioner(&self) -> Option<&CoronaFsClient> { + self.coronafs_controller + .as_ref() + .or(self.coronafs_node.as_ref()) + } + + fn coronafs_attachment_backend(&self) -> Option<&CoronaFsClient> { + self.coronafs_node + .as_ref() + .or(self.coronafs_controller.as_ref()) + } + + async fn load_coronafs_volume_for_attachment( + &self, + volume: &Volume, + ) -> Result<(CoronaFsVolumeResponse, CoronaFsClient), Status> { + let volume_id = volume.id.as_str(); + if let Some(node_client) = &self.coronafs_node { + if let Some(controller) = self + .coronafs_controller + .as_ref() + .filter(|controller| !controller.shares_endpoint_with(node_client)) + { + if !self.coronafs_node_local_attach { + if let Some(local_volume) = node_client.get_volume_optional(volume_id).await? { + tracing::warn!( + volume_id, + node_endpoint = %node_client.endpoint, + local_path = %local_volume.path, + "Discarding stale node-local CoronaFS volume because experimental writable local attach is disabled" + ); + if let Err(error) = node_client.delete_volume(volume_id).await { + tracing::warn!( + volume_id, + endpoint = %node_client.endpoint, + error = %error, + "Failed to delete stale node-local CoronaFS volume while falling back to controller-backed attachment" + ); + } + } + let volume = controller + .get_volume_optional(volume_id) + .await? + .ok_or_else(|| { + Status::not_found(format!("CoronaFS volume {volume_id} not found")) + })?; + return Ok((volume, controller.clone())); + } + + match node_client.get_volume_optional(volume_id).await? { + Some(volume) => return Ok((volume, node_client.clone())), + None => { + if volume_has_pending_coronafs_image_seed(volume) { + if let Some(local_seeded_volume) = self + .materialize_pending_coronafs_image_seed_on_node( + volume, + node_client, + ) + .await? + { + return Ok((local_seeded_volume, node_client.clone())); + } + let image_id = + volume_coronafs_image_source_id(volume).ok_or_else(|| { + Status::failed_precondition(format!( + "volume {volume_id} is missing CoronaFS image seed metadata" + )) + })?; + tracing::warn!( + volume_id, + image_id, + controller_endpoint = %controller.endpoint, + node_endpoint = %node_client.endpoint, + "Falling back to eager controller-side CoronaFS image seed" + ); + self.clone_image_into_coronafs( + volume_id, + &volume.org_id, + &volume.project_id, + image_id, + volume.size_gib, + false, + ) + .await?; + self.clear_pending_coronafs_image_seed(volume).await?; + } + let controller_volume = controller + .get_volume_optional(volume_id) + .await? + .ok_or_else(|| { + Status::not_found(format!("CoronaFS volume {volume_id} not found")) + })?; + let local_volume = self + .materialize_coronafs_volume_on_node( + volume_id, + node_client, + controller, + &controller_volume, + ) + .await?; + return Ok((local_volume, node_client.clone())); + } + } + } + + if let Some(volume) = node_client.get_volume_optional(volume_id).await? { + return Ok((volume, node_client.clone())); + } + } + + let controller = self + .coronafs_controller + .as_ref() + .or(self.coronafs_node.as_ref()) + .ok_or_else(|| Status::failed_precondition("coronafs backend is not configured"))?; + let volume = controller + .get_volume_optional(volume_id) + .await? + .ok_or_else(|| Status::not_found(format!("CoronaFS volume {volume_id} not found")))?; + Ok((volume, controller.clone())) + } + + async fn materialize_coronafs_volume_on_node( + &self, + volume_id: &str, + node_client: &CoronaFsClient, + controller: &CoronaFsClient, + controller_volume: &CoronaFsVolumeResponse, + ) -> Result { + tracing::info!( + volume_id, + node_endpoint = %node_client.endpoint, + controller_endpoint = %controller.endpoint, + "Materializing CoronaFS volume on node-local endpoint" + ); + let export = match controller_volume.export.clone() { + Some(export) => export, + None => controller.ensure_export_read_only(volume_id).await?, + }; + node_client + .materialize_from_export( + volume_id, + &export.uri, + controller_volume.size_bytes, + Some(CoronaFsVolumeFormat::Qcow2), + true, + ) + .await + } + pub async fn create_managed_volume( &self, org_id: &str, @@ -175,36 +379,70 @@ impl VolumeManager { } let path = self.managed_volume_path(volume_id); - let provision_result = if let Some(image_id) = image_id { - if self.coronafs.is_some() { - self.clone_image_into_coronafs(volume_id, org_id, project_id, image_id, size_gib) + let mut metadata = metadata; + let provision_result: Result = + if let Some(image_id) = image_id { + if self.coronafs_provisioner().is_some() { + self.clone_image_into_coronafs( + volume_id, org_id, project_id, image_id, size_gib, true, + ) .await + } else { + self.clone_image_into_managed(org_id, project_id, image_id, &path) + .await + .map(|format| CoronaFsProvisionOutcome { + format, + deferred_image_seed: false, + }) + } } else { - self.clone_image_into_managed(org_id, project_id, image_id, &path) - .await - } - } else { - if let Some(coronafs) = &self.coronafs { - coronafs - .create_blank(volume_id, gib_to_bytes(size_gib)) - .await - .map(|_| ()) - } else { - self.create_blank_managed(&path, size_gib, VolumeFormat::Raw) - .await + if let Some(coronafs) = self.coronafs_provisioner() { + coronafs + .create_blank(volume_id, gib_to_bytes(size_gib)) + .await + .map(|_| CoronaFsProvisionOutcome { + format: VolumeFormat::Raw, + deferred_image_seed: false, + }) + } else { + self.create_blank_managed(&path, size_gib, VolumeFormat::Raw) + .await + .map(|format| CoronaFsProvisionOutcome { + format, + deferred_image_seed: false, + }) + } + }; + let provisioned = match provision_result { + Ok(outcome) => outcome, + Err(error) => { + self.cleanup_partial_managed_volume(volume_id, &path).await; + return Err(error); } }; - if let Err(error) = provision_result { - self.cleanup_partial_managed_volume(volume_id, &path).await; - return Err(error); + if let Some(image_id) = image_id.filter(|_| provisioned.deferred_image_seed) { + metadata.insert( + CORONAFS_IMAGE_SOURCE_ID_METADATA_KEY.to_string(), + image_id.to_string(), + ); + metadata.insert( + CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY.to_string(), + "true".to_string(), + ); } - let mut volume = Volume::new(volume_id.to_string(), name.to_string(), org_id, project_id, size_gib); + let mut volume = Volume::new( + volume_id.to_string(), + name.to_string(), + org_id, + project_id, + size_gib, + ); volume.driver = VolumeDriverKind::Managed; volume.storage_class = storage_class .map(ToOwned::to_owned) .unwrap_or_else(|| self.default_managed_storage_class()); - volume.format = VolumeFormat::Raw; + volume.format = provisioned.format; volume.status = VolumeStatus::Available; volume.metadata = metadata; volume.labels = labels; @@ -213,7 +451,7 @@ impl VolumeManager { } async fn cleanup_partial_managed_volume(&self, volume_id: &str, path: &Path) { - if let Some(coronafs) = &self.coronafs { + if let Some(coronafs) = self.coronafs_provisioner() { if let Err(error) = coronafs.delete_volume(volume_id).await { tracing::warn!( volume_id, @@ -315,17 +553,33 @@ impl VolumeManager { return Ok(()); }; if volume.attached_to_vm.is_some() { - return Err(Status::failed_precondition("volume is still attached to a VM")); + return Err(Status::failed_precondition( + "volume is still attached to a VM", + )); } if matches!(volume.backing, VolumeBacking::Managed) { - if let Some(coronafs) = &self.coronafs { + if let Some(coronafs) = self.coronafs_provisioner() { coronafs.delete_volume(volume_id).await?; + if let Some(node_client) = self + .coronafs_node + .as_ref() + .filter(|node_client| !node_client.shares_endpoint_with(coronafs)) + { + if let Err(error) = node_client.delete_volume(volume_id).await { + tracing::warn!( + volume_id, + endpoint = %node_client.endpoint, + error = %error, + "Failed to remove node-local CoronaFS replica while deleting managed volume" + ); + } + } } else { let path = self.managed_volume_path(volume_id); if tokio::fs::try_exists(&path).await.unwrap_or(false) { - tokio::fs::remove_file(&path) - .await - .map_err(|e| Status::internal(format!("failed to remove volume data: {e}")))?; + tokio::fs::remove_file(&path).await.map_err(|e| { + Status::internal(format!("failed to remove volume data: {e}")) + })?; } } } @@ -347,13 +601,17 @@ impl VolumeManager { .await? .ok_or_else(|| Status::not_found("volume not found"))?; if matches!(volume.backing, VolumeBacking::Managed) { - if let Some(coronafs) = &self.coronafs { + if let Some(coronafs) = self.coronafs_provisioner() { coronafs .resize_volume(volume_id, gib_to_bytes(size_gib)) .await?; } else { - self.resize_managed(&self.managed_volume_path(volume_id), volume.format, size_gib) - .await?; + self.resize_managed( + &self.managed_volume_path(volume_id), + volume.format, + size_gib, + ) + .await?; } } volume.size_gib = size_gib; @@ -366,78 +624,98 @@ impl VolumeManager { &self, vm: &mut VirtualMachine, ) -> Result, Status> { + let original_disks = vm.spec.disks.clone(); let vm_id = vm.id.to_string(); - let mut attached = Vec::with_capacity(vm.spec.disks.len()); - for disk in &mut vm.spec.disks { - match &disk.source { - DiskSource::Image { image_id } => { - let volume_id = derived_volume_id(&vm_id, &disk.id); - let mut metadata = std::collections::HashMap::new(); - metadata.insert( - AUTO_DELETE_VOLUME_METADATA_KEY.to_string(), - "true".to_string(), - ); - metadata.insert( - AUTO_DELETE_VOLUME_SOURCE_METADATA_KEY.to_string(), - "image".to_string(), - ); - let volume = self - .create_managed_volume_with_id( - &volume_id, - &vm.org_id, - &vm.project_id, - &format!("{}-{}", vm.name, disk.id), - disk.size_gib, - Some("managed-default"), - Some(image_id), - metadata, - std::collections::HashMap::new(), - ) - .await?; - disk.source = DiskSource::Volume { - volume_id: volume.id.clone(), - }; - attached.push(self.attach_volume_to_vm(&volume, &vm_id, disk).await?); - } - DiskSource::Blank => { - let volume_id = derived_volume_id(&vm_id, &disk.id); - let mut metadata = std::collections::HashMap::new(); - metadata.insert( - AUTO_DELETE_VOLUME_METADATA_KEY.to_string(), - "true".to_string(), - ); - metadata.insert( - AUTO_DELETE_VOLUME_SOURCE_METADATA_KEY.to_string(), - "blank".to_string(), - ); - let volume = self - .create_managed_volume_with_id( - &volume_id, - &vm.org_id, - &vm.project_id, - &format!("{}-{}", vm.name, disk.id), - disk.size_gib, - Some("managed-default"), - None, - metadata, - std::collections::HashMap::new(), - ) - .await?; - disk.source = DiskSource::Volume { - volume_id: volume.id.clone(), - }; - attached.push(self.attach_volume_to_vm(&volume, &vm_id, disk).await?); - } - DiskSource::Volume { volume_id } => { - let volume = self - .get_volume(&vm.org_id, &vm.project_id, volume_id) - .await? - .ok_or_else(|| Status::not_found(format!("volume {volume_id} not found")))?; - attached.push(self.attach_volume_to_vm(&volume, &vm_id, disk).await?); + let result: Result, Status> = async { + let mut attached = Vec::with_capacity(vm.spec.disks.len()); + for disk in &mut vm.spec.disks { + match &disk.source { + DiskSource::Image { image_id } => { + let volume_id = derived_volume_id(&vm_id, &disk.id); + let mut metadata = std::collections::HashMap::new(); + metadata.insert( + AUTO_DELETE_VOLUME_METADATA_KEY.to_string(), + "true".to_string(), + ); + metadata.insert( + AUTO_DELETE_VOLUME_SOURCE_METADATA_KEY.to_string(), + "image".to_string(), + ); + let volume = self + .create_managed_volume_with_id( + &volume_id, + &vm.org_id, + &vm.project_id, + &format!("{}-{}", vm.name, disk.id), + disk.size_gib, + Some("managed-default"), + Some(image_id), + metadata, + std::collections::HashMap::new(), + ) + .await?; + disk.source = DiskSource::Volume { + volume_id: volume.id.clone(), + }; + attached.push(self.attach_volume_to_vm(&volume, &vm_id, disk).await?); + } + DiskSource::Blank => { + let volume_id = derived_volume_id(&vm_id, &disk.id); + let mut metadata = std::collections::HashMap::new(); + metadata.insert( + AUTO_DELETE_VOLUME_METADATA_KEY.to_string(), + "true".to_string(), + ); + metadata.insert( + AUTO_DELETE_VOLUME_SOURCE_METADATA_KEY.to_string(), + "blank".to_string(), + ); + let volume = self + .create_managed_volume_with_id( + &volume_id, + &vm.org_id, + &vm.project_id, + &format!("{}-{}", vm.name, disk.id), + disk.size_gib, + Some("managed-default"), + None, + metadata, + std::collections::HashMap::new(), + ) + .await?; + disk.source = DiskSource::Volume { + volume_id: volume.id.clone(), + }; + attached.push(self.attach_volume_to_vm(&volume, &vm_id, disk).await?); + } + DiskSource::Volume { volume_id } => { + let volume = self + .get_volume(&vm.org_id, &vm.project_id, volume_id) + .await? + .ok_or_else(|| { + Status::not_found(format!("volume {volume_id} not found")) + })?; + attached.push(self.attach_volume_to_vm(&volume, &vm_id, disk).await?); + } } } + Ok(attached) } - Ok(attached) + .await; + + if let Err(error) = result { + if let Err(rollback_error) = self.rollback_vm_volumes(vm, true).await { + tracing::warn!( + vm_id = %vm.id, + error = %rollback_error, + "Failed to roll back partial volume preparation" + ); + } + vm.spec.disks = original_disks; + return Err(error); + } + + result } pub async fn delete_vm_managed_volumes(&self, vm: &VirtualMachine) -> Result<(), Status> { @@ -460,17 +738,111 @@ impl VolumeManager { Ok(()) } + pub async fn rollback_vm_volumes( + &self, + vm: &VirtualMachine, + delete_auto_delete: bool, + ) -> Result<(), Status> { + self.release_vm_volumes(vm).await?; + if delete_auto_delete { + self.delete_vm_managed_volumes(vm).await?; + } + Ok(()) + } + pub async fn release_vm_volumes(&self, vm: &VirtualMachine) -> Result<(), Status> { for disk in &vm.spec.disks { let DiskSource::Volume { volume_id } = &disk.source else { continue; }; - self.release_volume_attachment(&vm.org_id, &vm.project_id, volume_id, &vm.id.to_string()) + self.release_volume_attachment( + &vm.org_id, + &vm.project_id, + volume_id, + &vm.id.to_string(), + ) + .await?; + } + Ok(()) + } + + pub async fn flush_vm_volumes(&self, vm: &VirtualMachine) -> Result<(), Status> { + for disk in &vm.spec.disks { + let DiskSource::Volume { volume_id } = &disk.source else { + continue; + }; + self.flush_volume_attachment(&vm.org_id, &vm.project_id, volume_id, &vm.id.to_string()) .await?; } Ok(()) } + pub async fn flush_volume_attachment( + &self, + org_id: &str, + project_id: &str, + volume_id: &str, + vm_id: &str, + ) -> Result<(), Status> { + let Some(mut volume) = self + .store + .load_volume(org_id, project_id, volume_id) + .await + .map_err(to_status)? + else { + return Ok(()); + }; + if volume.attached_to_vm.as_deref() != Some(vm_id) { + return Ok(()); + } + if !matches!(volume.backing, VolumeBacking::Managed) { + return Ok(()); + } + self.sync_node_local_coronafs_volume_to_controller(volume_id) + .await?; + if volume.last_flushed_attachment_generation != volume.attachment_generation + || volume_has_pending_coronafs_image_seed(&volume) + { + for _ in 0..VOLUME_METADATA_CAS_RETRIES { + let mut updated = volume.clone(); + updated.last_flushed_attachment_generation = updated.attachment_generation; + updated + .metadata + .remove(CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY); + updated.updated_at = now_epoch(); + if self + .store + .compare_and_swap_volume(Some(&volume), &updated) + .await + .map_err(to_status)? + { + return Ok(()); + } + let Some(current) = self + .store + .load_volume(org_id, project_id, volume_id) + .await + .map_err(to_status)? + else { + return Ok(()); + }; + if current.attached_to_vm.as_deref() != Some(vm_id) + || !matches!(current.backing, VolumeBacking::Managed) + || current.last_flushed_attachment_generation == current.attachment_generation + && !volume_has_pending_coronafs_image_seed(¤t) + { + return Ok(()); + } + volume = current; + } + return Err(Status::aborted(format!( + "volume {} metadata was modified concurrently while recording flush state", + volume_id + ))); + } + Ok(()) + } + pub async fn release_volume_attachment( &self, org_id: &str, @@ -487,45 +859,351 @@ impl VolumeManager { return Ok(()); }; if volume.attached_to_vm.as_deref() == Some(vm_id) { - volume.attached_to_vm = None; - volume.status = VolumeStatus::Available; - volume.updated_at = now_epoch(); - self.store.save_volume(&volume).await.map_err(to_status)?; + if matches!(volume.backing, VolumeBacking::Managed) + && volume.last_flushed_attachment_generation != volume.attachment_generation + { + self.sync_node_local_coronafs_volume_to_controller(volume_id) + .await?; + volume.last_flushed_attachment_generation = volume.attachment_generation; + } + let mut detached = volume.clone(); + detached.attached_to_vm = None; + detached.attached_to_node = None; + detached.status = VolumeStatus::Available; + detached + .metadata + .remove(CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY); + detached.updated_at = now_epoch(); + let mut detached_saved = false; + for _ in 0..VOLUME_METADATA_CAS_RETRIES { + if self + .store + .compare_and_swap_volume(Some(&volume), &detached) + .await + .map_err(to_status)? + { + detached_saved = true; + break; + } + let Some(current) = self + .store + .load_volume(org_id, project_id, volume_id) + .await + .map_err(to_status)? + else { + return Ok(()); + }; + if current.attached_to_vm.as_deref() != Some(vm_id) { + return Ok(()); + } + volume = current.clone(); + detached = current; + detached.attached_to_vm = None; + detached.attached_to_node = None; + detached.status = VolumeStatus::Available; + detached.updated_at = now_epoch(); + } + if !detached_saved { + return Err(Status::aborted(format!( + "volume {} metadata was modified concurrently while releasing attachment", + volume_id + ))); + } + if let (Some(node_client), Some(controller)) = + (&self.coronafs_node, &self.coronafs_controller) + { + if !controller.shares_endpoint_with(node_client) { + if let Err(error) = node_client.delete_volume(volume_id).await { + tracing::warn!( + volume_id, + endpoint = %node_client.endpoint, + error = %error, + "Failed to remove node-local CoronaFS volume while releasing attachment" + ); + } + } + } } Ok(()) } + async fn sync_node_local_coronafs_volume_to_controller( + &self, + volume_id: &str, + ) -> Result<(), Status> { + let (Some(node_client), Some(controller)) = + (&self.coronafs_node, &self.coronafs_controller) + else { + return Ok(()); + }; + if controller.shares_endpoint_with(node_client) { + return Ok(()); + } + + let Some(local_volume) = node_client.get_volume_optional(volume_id).await? else { + return Ok(()); + }; + if local_volume.export.is_some() { + node_client.release_export(volume_id).await?; + } + + let controller_volume = match controller.get_volume_optional(volume_id).await? { + Some(volume) => { + if volume.size_bytes < local_volume.size_bytes { + controller + .resize_volume(volume_id, local_volume.size_bytes) + .await?; + controller + .get_volume_optional(volume_id) + .await? + .ok_or_else(|| { + Status::not_found(format!( + "CoronaFS controller volume {volume_id} disappeared after resize" + )) + })? + } else { + volume + } + } + None => { + controller + .create_blank(volume_id, local_volume.size_bytes) + .await? + } + }; + let export = match controller_volume.export { + Some(export) => export, + None => controller.ensure_export(volume_id).await?, + }; + tracing::info!( + volume_id, + node_endpoint = %node_client.endpoint, + controller_endpoint = %controller.endpoint, + local_path = %local_volume.path, + export_uri = %export.uri, + "Syncing node-local CoronaFS volume back to controller" + ); + sync_local_coronafs_volume_to_export(&local_volume, &export.uri).await + } + + async fn materialize_pending_coronafs_image_seed_on_node( + &self, + volume: &Volume, + node_client: &CoronaFsClient, + ) -> Result, Status> { + if !volume_has_pending_coronafs_image_seed(volume) { + return Ok(None); + } + let Some(image_id) = volume_coronafs_image_source_id(volume) else { + return Ok(None); + }; + if !node_client.supports_local_backing_file().await { + return Ok(None); + } + let Some(artifact_store) = &self.artifact_store else { + tracing::warn!( + volume_id = volume.id, + image_id, + "Cannot materialize pending CoronaFS image seed on node because artifact storage is unavailable" + ); + return Ok(None); + }; + let raw_image_path = artifact_store + .materialize_raw_image_cache(&volume.org_id, &volume.project_id, image_id) + .await?; + let requested_size = gib_to_bytes(volume.size_gib); + match node_client + .create_image_backed( + &volume.id, + requested_size, + &raw_image_path, + CoronaFsVolumeFormat::Raw, + ) + .await + { + Ok(local_volume) => { + tracing::info!( + volume_id = volume.id, + image_id, + node_endpoint = %node_client.endpoint, + raw_image_path = %raw_image_path.display(), + requested_size, + volume_path = %local_volume.path, + "Provisioned pending CoronaFS image seed directly on node-local endpoint" + ); + Ok(Some(local_volume)) + } + Err(error) => { + tracing::warn!( + volume_id = volume.id, + image_id, + node_endpoint = %node_client.endpoint, + raw_image_path = %raw_image_path.display(), + error = %error, + "Failed to provision pending CoronaFS image seed on node-local endpoint" + ); + Ok(None) + } + } + } + + async fn clear_pending_coronafs_image_seed(&self, volume: &Volume) -> Result<(), Status> { + if !volume_has_pending_coronafs_image_seed(volume) { + return Ok(()); + } + let mut current = volume.clone(); + for _ in 0..VOLUME_METADATA_CAS_RETRIES { + if !volume_has_pending_coronafs_image_seed(¤t) { + return Ok(()); + } + let mut updated = current.clone(); + updated + .metadata + .remove(CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY); + updated.updated_at = now_epoch(); + if self + .store + .compare_and_swap_volume(Some(¤t), &updated) + .await + .map_err(to_status)? + { + return Ok(()); + } + let Some(next) = self + .store + .load_volume(¤t.org_id, ¤t.project_id, ¤t.id) + .await + .map_err(to_status)? + else { + return Ok(()); + }; + current = next; + } + Err(Status::aborted(format!( + "volume {} metadata was modified concurrently while clearing pending CoronaFS image seed", + volume.id + ))) + } + + pub async fn supports_cold_relocation_for_vm( + &self, + vm: &VirtualMachine, + ) -> Result { + for disk in &vm.spec.disks { + match &disk.source { + DiskSource::Volume { volume_id } => { + let volume = self + .get_volume(&vm.org_id, &vm.project_id, volume_id) + .await? + .ok_or_else(|| { + Status::not_found(format!("volume {volume_id} not found")) + })?; + match &volume.backing { + VolumeBacking::Managed => { + if self.coronafs_provisioner().is_none() { + return Ok(false); + } + } + VolumeBacking::CephRbd { .. } => {} + } + } + DiskSource::Image { .. } | DiskSource::Blank => { + if self.coronafs_provisioner().is_none() { + return Ok(false); + } + } + } + } + Ok(true) + } + async fn attach_volume_to_vm( &self, volume: &Volume, vm_id: &str, disk: &DiskSpec, ) -> Result { - if let Some(attached_to_vm) = volume.attached_to_vm.as_deref() { - if attached_to_vm != vm_id { + let mut current = volume.clone(); + for _ in 0..VOLUME_METADATA_CAS_RETRIES { + if let Some(attached_to_vm) = current.attached_to_vm.as_deref() { + if attached_to_vm != vm_id { + return Err(Status::failed_precondition(format!( + "volume {} is already attached to VM {}", + current.id, attached_to_vm + ))); + } + } + if current.attached_to_vm.as_deref() == Some(vm_id) + && current.attached_to_node.is_some() + && current.attached_to_node != self.local_node_id + { return Err(Status::failed_precondition(format!( - "volume {} is already attached to VM {}", - volume.id, attached_to_vm + "volume {} writer lease is still held by node {}", + current.id, + current.attached_to_node.as_deref().unwrap_or("unknown") ))); } - } - let mut updated = volume.clone(); - updated.attached_to_vm = Some(vm_id.to_string()); - updated.status = VolumeStatus::InUse; - updated.updated_at = now_epoch(); - self.store.save_volume(&updated).await.map_err(to_status)?; - self.attachment_from_volume(&updated, disk).await + let mut updated = current.clone(); + let attachment_changed = current.attached_to_vm.as_deref() != Some(vm_id) + || current.attached_to_node != self.local_node_id; + updated.attached_to_vm = Some(vm_id.to_string()); + updated.attached_to_node = self.local_node_id.clone(); + if attachment_changed { + updated.attachment_generation = updated.attachment_generation.saturating_add(1); + } + updated.status = VolumeStatus::InUse; + updated.updated_at = now_epoch(); + if self + .store + .compare_and_swap_volume(Some(¤t), &updated) + .await + .map_err(to_status)? + { + return self.attachment_from_volume(&updated, disk).await; + } + current = self + .store + .load_volume(¤t.org_id, ¤t.project_id, ¤t.id) + .await + .map_err(to_status)? + .ok_or_else(|| Status::not_found(format!("volume {} not found", current.id)))?; + } + Err(Status::aborted(format!( + "volume {} metadata was modified concurrently while attaching to VM {}", + volume.id, vm_id + ))) } - async fn attachment_from_volume(&self, volume: &Volume, disk: &DiskSpec) -> Result { + async fn attachment_from_volume( + &self, + volume: &Volume, + disk: &DiskSpec, + ) -> Result { let attachment = match &volume.backing { VolumeBacking::Managed => { - if let Some(coronafs) = &self.coronafs { - let export = coronafs.ensure_export(&volume.id).await?; - DiskAttachment::Nbd { - uri: export.uri, - format: volume.format, + if self.coronafs_attachment_backend().is_some() { + let (coronafs_volume, coronafs) = + self.load_coronafs_volume_for_attachment(volume).await?; + if coronafs.supports_local_backing_file().await + && !should_prefer_coronafs_export_attachment(&coronafs_volume) + && coronafs_local_target_ready(&coronafs_volume.path).await + { + DiskAttachment::File { + path: coronafs_volume.path, + format: volume.format, + } + } else { + let export = match coronafs_volume.export { + Some(export) => export, + None => coronafs.ensure_export(&volume.id).await?, + }; + DiskAttachment::Nbd { + uri: export.uri, + // CoronaFS exports present a block device regardless of the + // underlying on-disk format of the node-local backing file. + format: VolumeFormat::Raw, + } } } else { DiskAttachment::File { @@ -539,10 +1217,9 @@ impl VolumeManager { pool, image, } => { - let ceph = self - .ceph_cluster - .as_ref() - .ok_or_else(|| Status::failed_precondition("Ceph RBD backend is not configured"))?; + let ceph = self.ceph_cluster.as_ref().ok_or_else(|| { + Status::failed_precondition("Ceph RBD backend is not configured") + })?; if ceph.cluster_id != *cluster_id { return Err(Status::failed_precondition(format!( "Ceph cluster {} is not configured on this node", @@ -558,17 +1235,11 @@ impl VolumeManager { } } }; - let cache = if matches!(attachment, DiskAttachment::Nbd { .. }) { - DiskCache::None - } else { - disk.cache - }; - Ok(AttachedDisk { id: disk.id.clone(), attachment, bus: disk.bus, - cache, + cache: disk.cache, boot_index: disk.boot_index, read_only: false, }) @@ -580,14 +1251,13 @@ impl VolumeManager { project_id: &str, image_id: &str, target: &Path, - ) -> Result<(), Status> { + ) -> Result { if tokio::fs::try_exists(target).await.unwrap_or(false) { - return Ok(()); + return Ok(VolumeFormat::Raw); } - let artifact_store = self - .artifact_store - .as_ref() - .ok_or_else(|| Status::failed_precondition("image-backed volumes require artifact storage"))?; + let artifact_store = self.artifact_store.as_ref().ok_or_else(|| { + Status::failed_precondition("image-backed volumes require artifact storage") + })?; let image_path = artifact_store .materialize_image_cache(org_id, project_id, image_id) .await?; @@ -608,7 +1278,7 @@ impl VolumeManager { .await .map_err(|e| Status::internal(format!("failed to spawn qemu-img convert: {e}")))?; if status.success() { - Ok(()) + Ok(VolumeFormat::Raw) } else { Err(Status::internal(format!( "qemu-img convert failed for {} with status {status}", @@ -624,21 +1294,18 @@ impl VolumeManager { project_id: &str, image_id: &str, size_gib: u64, - ) -> Result<(), Status> { - let artifact_store = self - .artifact_store - .as_ref() - .ok_or_else(|| Status::failed_precondition("image-backed volumes require artifact storage"))?; + allow_deferred_image_seed: bool, + ) -> Result { + let artifact_store = self.artifact_store.as_ref().ok_or_else(|| { + Status::failed_precondition("image-backed volumes require artifact storage") + })?; let coronafs = self - .coronafs - .as_ref() + .coronafs_provisioner() .ok_or_else(|| Status::failed_precondition("coronafs backend is not configured"))?; + let attachment_backend = self.coronafs_attachment_backend().unwrap_or(coronafs); let image_path = artifact_store .materialize_image_cache(org_id, project_id, image_id) .await?; - let raw_image_path = artifact_store - .materialize_raw_image_cache(org_id, project_id, image_id) - .await?; let requested_size = gib_to_bytes(size_gib); let image_info = inspect_qemu_image(&image_path).await?; if requested_size < image_info.virtual_size { @@ -648,8 +1315,89 @@ impl VolumeManager { ))); } + if allow_deferred_image_seed + && self.coronafs_node_local_attach + && !coronafs.shares_endpoint_with(attachment_backend) + && attachment_backend.supports_local_backing_file().await + { + let volume = coronafs.create_blank(volume_id, requested_size).await?; + tracing::info!( + volume_id, + image_id, + controller_endpoint = %coronafs.endpoint, + attachment_endpoint = %attachment_backend.endpoint, + requested_size, + volume_path = %volume.path, + "Provisioned blank CoronaFS controller volume and deferred image seed to node-local attach" + ); + return Ok(CoronaFsProvisionOutcome { + format: VolumeFormat::Qcow2, + deferred_image_seed: true, + }); + } + + let mut raw_image_path: Option = None; + let supports_local_coronafs_path = coronafs.shares_endpoint_with(attachment_backend) + && coronafs.supports_local_backing_file().await; + if supports_local_coronafs_path { + let materialized_raw = artifact_store + .materialize_raw_image_cache(org_id, project_id, image_id) + .await?; + match coronafs + .create_image_backed( + volume_id, + requested_size, + &materialized_raw, + CoronaFsVolumeFormat::Raw, + ) + .await + { + Ok(volume) => { + tracing::info!( + volume_id, + image_id, + raw_image_path = %materialized_raw.display(), + requested_size, + volume_path = %volume.path, + format = ?volume.format, + "Provisioned CoronaFS-backed VM volume via qcow2 thin clone" + ); + return Ok(CoronaFsProvisionOutcome { + format: VolumeFormat::Qcow2, + deferred_image_seed: false, + }); + } + Err(error) => { + tracing::warn!( + volume_id, + image_id, + raw_image_path = %materialized_raw.display(), + requested_size, + error = %error, + "Falling back to eager CoronaFS image clone" + ); + raw_image_path = Some(materialized_raw); + } + } + } else { + tracing::info!( + volume_id, + image_id, + coronafs_endpoint = %coronafs.endpoint, + "Skipping CoronaFS image-backed thin clone because the CoronaFS endpoint is remote" + ); + } + let volume = coronafs.create_blank(volume_id, requested_size).await?; - let convert_target = if coronafs_local_target_ready(&volume.path).await { + if supports_local_coronafs_path && coronafs_local_target_ready(&volume.path).await { + let raw_image_path = match raw_image_path { + Some(path) => path, + None => { + artifact_store + .materialize_raw_image_cache(org_id, project_id, image_id) + .await? + } + }; tracing::info!( volume_id, image_id, @@ -659,55 +1407,64 @@ impl VolumeManager { image_virtual_size = image_info.virtual_size, requested_size, volume_path = %volume.path, - "Populating CoronaFS-backed VM volume directly via local raw cache" + "Populating CoronaFS-backed VM volume via local raw clone" ); - volume.path + clone_local_raw_into_coronafs_volume( + &raw_image_path, + Path::new(&volume.path), + requested_size, + ) + .await?; + return Ok(CoronaFsProvisionOutcome { + format: VolumeFormat::Raw, + deferred_image_seed: false, + }); } else { let export = coronafs.ensure_export(volume_id).await?; tracing::info!( volume_id, image_id, image_path = %image_path.display(), - raw_image_path = %raw_image_path.display(), image_format = %image_info.format, image_virtual_size = image_info.virtual_size, requested_size, export_uri = %export.uri, - "Populating CoronaFS-backed VM volume over NBD from local raw cache" + "Populating CoronaFS-backed VM volume over NBD from image cache" ); - export.uri - }; - - let status = Command::new("qemu-img") - .args([ - "convert", - "-t", - "none", - "-T", - "none", - "-m", - CORONAFS_IMAGE_CONVERT_PARALLELISM, - "-n", - "-W", - "--target-is-zero", - "-f", - "raw", - "-O", - "raw", - raw_image_path.to_string_lossy().as_ref(), - convert_target.as_str(), - ]) - .status() - .await - .map_err(|e| Status::internal(format!("failed to spawn qemu-img convert: {e}")))?; - if !status.success() { - return Err(Status::internal(format!( - "qemu-img convert into CoronaFS volume {} failed for {} with status {status}", - volume_id, - image_path.display(), - ))); + let status = Command::new("qemu-img") + .args([ + "convert", + "-t", + "none", + "-T", + "none", + "-m", + CORONAFS_IMAGE_CONVERT_PARALLELISM, + "-n", + "-W", + "--target-is-zero", + "-f", + image_info.format.as_str(), + "-O", + "raw", + image_path.to_string_lossy().as_ref(), + export.uri.as_str(), + ]) + .status() + .await + .map_err(|e| Status::internal(format!("failed to spawn qemu-img convert: {e}")))?; + if !status.success() { + return Err(Status::internal(format!( + "qemu-img convert into CoronaFS volume {} failed for {} with status {status}", + volume_id, + image_path.display(), + ))); + } } - Ok(()) + Ok(CoronaFsProvisionOutcome { + format: VolumeFormat::Raw, + deferred_image_seed: false, + }) } async fn create_blank_managed( @@ -715,9 +1472,9 @@ impl VolumeManager { path: &Path, size_gib: u64, format: VolumeFormat, - ) -> Result<(), Status> { + ) -> Result { if tokio::fs::try_exists(path).await.unwrap_or(false) { - return Ok(()); + return Ok(format); } if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent) @@ -736,7 +1493,7 @@ impl VolumeManager { .await .map_err(|e| Status::internal(format!("failed to spawn qemu-img create: {e}")))?; if status.success() { - Ok(()) + Ok(format) } else { Err(Status::internal(format!( "qemu-img create failed for {} with status {status}", @@ -777,7 +1534,7 @@ impl VolumeManager { } fn default_managed_storage_class(&self) -> String { - if self.coronafs.is_some() { + if self.coronafs_provisioner().is_some() { "coronafs-managed".to_string() } else { "managed-default".to_string() @@ -787,12 +1544,7 @@ impl VolumeManager { async fn inspect_qemu_image(path: &Path) -> Result { let output = Command::new("qemu-img") - .args([ - "info", - "--output", - "json", - path.to_string_lossy().as_ref(), - ]) + .args(["info", "--output", "json", path.to_string_lossy().as_ref()]) .output() .await .map_err(|e| Status::internal(format!("failed to spawn qemu-img info: {e}")))?; @@ -808,7 +1560,12 @@ async fn inspect_qemu_image(path: &Path) -> Result { } async fn coronafs_local_target_ready(path: &str) -> bool { - match tokio::fs::OpenOptions::new().read(true).write(true).open(path).await { + match tokio::fs::OpenOptions::new() + .read(true) + .write(true) + .open(path) + .await + { Ok(file) => { drop(file); true @@ -824,6 +1581,134 @@ async fn coronafs_local_target_ready(path: &str) -> bool { } } +async fn clone_local_raw_into_coronafs_volume( + source: &Path, + destination: &Path, + requested_size: u64, +) -> Result<(), Status> { + let temp_path = destination.with_extension("clone.tmp"); + if let Some(parent) = temp_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| Status::internal(format!("failed to create clone dir: {e}")))?; + } + if tokio::fs::try_exists(&temp_path).await.unwrap_or(false) { + let _ = tokio::fs::remove_file(&temp_path).await; + } + + let copy_output = Command::new("cp") + .args([ + "--reflink=auto", + "--sparse=always", + source.to_string_lossy().as_ref(), + temp_path.to_string_lossy().as_ref(), + ]) + .output() + .await + .map_err(|e| Status::internal(format!("failed to spawn raw clone copy: {e}")))?; + if !copy_output.status.success() { + let stderr = String::from_utf8_lossy(©_output.stderr) + .trim() + .to_string(); + return Err(Status::internal(format!( + "failed to clone raw image {} into {} with status {}{}", + source.display(), + temp_path.display(), + copy_output.status, + if stderr.is_empty() { + String::new() + } else { + format!(": {stderr}") + } + ))); + } + + let file = tokio::fs::OpenOptions::new() + .write(true) + .open(&temp_path) + .await + .map_err(|e| { + Status::internal(format!( + "failed to open cloned CoronaFS temp volume {}: {e}", + temp_path.display() + )) + })?; + file.set_len(requested_size).await.map_err(|e| { + Status::internal(format!( + "failed to resize cloned CoronaFS temp volume {}: {e}", + temp_path.display() + )) + })?; + drop(file); + ensure_coronafs_clone_permissions(&temp_path).await?; + + tokio::fs::rename(&temp_path, destination) + .await + .map_err(|e| { + Status::internal(format!( + "failed to finalize cloned CoronaFS volume {}: {e}", + destination.display() + )) + })?; + ensure_coronafs_clone_permissions(destination).await?; + + Ok(()) +} + +async fn sync_local_coronafs_volume_to_export( + local_volume: &CoronaFsVolumeResponse, + export_uri: &str, +) -> Result<(), Status> { + let local_format = local_volume.format.unwrap_or(CoronaFsVolumeFormat::Raw); + let status = Command::new("qemu-img") + .args([ + "convert", + "-t", + "none", + "-T", + "none", + "-m", + CORONAFS_IMAGE_CONVERT_PARALLELISM, + "-n", + "-W", + "-f", + local_format.as_qemu_arg(), + "-O", + "raw", + local_volume.path.as_str(), + export_uri, + ]) + .status() + .await + .map_err(|e| Status::internal(format!("failed to spawn qemu-img sync: {e}")))?; + if status.success() { + Ok(()) + } else { + Err(Status::internal(format!( + "qemu-img sync from {} to {} failed with status {status}", + local_volume.path, export_uri + ))) + } +} + +async fn ensure_coronafs_clone_permissions(path: &Path) -> Result<(), Status> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let permissions = std::fs::Permissions::from_mode(0o660); + tokio::fs::set_permissions(path, permissions) + .await + .map_err(|e| { + Status::internal(format!( + "failed to set CoronaFS clone permissions on {}: {e}", + path.display() + )) + })?; + } + Ok(()) +} + fn volume_format_name(format: VolumeFormat) -> &'static str { match format { VolumeFormat::Raw => "raw", @@ -831,6 +1716,34 @@ fn volume_format_name(format: VolumeFormat) -> &'static str { } } +fn should_prefer_coronafs_export_attachment(volume: &CoronaFsVolumeResponse) -> bool { + volume.node_local + && matches!(volume.format, Some(CoronaFsVolumeFormat::Qcow2)) + && volume + .materialized_from + .as_deref() + .map(|source| source.starts_with("nbd://")) + .unwrap_or(false) +} + +fn volume_coronafs_image_source_id(volume: &Volume) -> Option<&str> { + volume + .metadata + .get(CORONAFS_IMAGE_SOURCE_ID_METADATA_KEY) + .map(String::as_str) + .filter(|value| !value.trim().is_empty()) +} + +fn volume_has_pending_coronafs_image_seed(volume: &Volume) -> bool { + matches!( + volume + .metadata + .get(CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY) + .map(String::as_str), + Some("1" | "true" | "yes" | "on") + ) +} + fn derived_volume_id(vm_id: &str, disk_id: &str) -> String { format!("{vm_id}-{disk_id}") } @@ -860,15 +1773,97 @@ fn gib_to_bytes(size_gib: u64) -> u64 { size_gib.saturating_mul(1024 * 1024 * 1024) } +fn coronafs_node_local_attach_enabled() -> bool { + coronafs_node_local_attach_enabled_from_values( + std::env::var("PLASMAVMC_CORONAFS_NODE_LOCAL_ATTACH") + .ok() + .as_deref(), + std::env::var("PLASMAVMC_CORONAFS_ENABLE_EXPERIMENTAL_NODE_LOCAL_ATTACH") + .ok() + .as_deref(), + ) +} + +fn coronafs_node_local_attach_enabled_from_values( + stable_value: Option<&str>, + legacy_value: Option<&str>, +) -> bool { + stable_value + .map(parse_truthy) + .or_else(|| legacy_value.map(parse_truthy)) + .unwrap_or(false) +} + +fn parse_truthy(value: &str) -> bool { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) +} + +fn resolve_coronafs_clients() -> (Option, Option) { + let (controller_endpoint, node_endpoint) = resolve_coronafs_endpoints( + std::env::var("PLASMAVMC_CORONAFS_CONTROLLER_ENDPOINT") + .ok() + .and_then(normalize_coronafs_endpoint), + std::env::var("PLASMAVMC_CORONAFS_NODE_ENDPOINT") + .ok() + .and_then(normalize_coronafs_endpoint), + std::env::var("PLASMAVMC_CORONAFS_ENDPOINT") + .ok() + .and_then(normalize_coronafs_endpoint), + ); + ( + controller_endpoint.map(CoronaFsClient::new), + node_endpoint.map(CoronaFsClient::new), + ) +} + +fn resolve_coronafs_endpoints( + controller_endpoint: Option, + node_endpoint: Option, + legacy_endpoint: Option, +) -> (Option, Option) { + let controller_endpoint = controller_endpoint.or_else(|| legacy_endpoint.clone()); + let node_endpoint = node_endpoint.or(legacy_endpoint); + (controller_endpoint, node_endpoint) +} + +fn normalize_coronafs_endpoint(endpoint: String) -> Option { + let endpoints = parse_coronafs_endpoints(&endpoint); + if endpoints.is_empty() { + None + } else { + Some(endpoints.join(",")) + } +} + +fn parse_coronafs_endpoints(value: &str) -> Vec { + value + .split(',') + .filter_map(normalize_coronafs_endpoint_candidate) + .collect() +} + +fn normalize_coronafs_endpoint_candidate(endpoint: &str) -> Option { + let endpoint = endpoint.trim().trim_end_matches('/'); + if endpoint.is_empty() { + return None; + } + if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + Some(endpoint.to_string()) + } else { + Some(format!("http://{endpoint}")) + } +} + impl CoronaFsClient { fn new(endpoint: String) -> Self { - let endpoint = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { - endpoint - } else { - format!("http://{endpoint}") - }; + let endpoints = parse_coronafs_endpoints(&endpoint); + let endpoint = endpoints.join(","); Self { endpoint, + endpoints, http: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) .build() @@ -876,65 +1871,531 @@ impl CoronaFsClient { } } - async fn create_blank(&self, volume_id: &str, size_bytes: u64) -> Result { - self.http - .put(format!("{}/v1/volumes/{}", self.endpoint, volume_id)) - .json(&CoronaFsCreateRequest { size_bytes }) - .send() - .await - .map_err(request_error("create CoronaFS volume"))? - .error_for_status() - .map_err(http_status_error("create CoronaFS volume"))? - .json::() - .await - .map_err(|e| Status::internal(format!("failed to decode CoronaFS create response: {e}"))) + async fn supports_local_backing_file(&self) -> bool { + match local_ip_addrs().await { + Ok(local_ips) => self.endpoints.iter().any(|endpoint| { + coronafs_endpoint_host(endpoint) + .map(|endpoint_host| { + endpoint_host.is_loopback() || local_ips.contains(&endpoint_host) + }) + .unwrap_or(false) + }), + Err(error) => { + tracing::warn!( + endpoint = %self.endpoint, + error = %error, + "Failed to resolve local IP addresses for CoronaFS endpoint locality check" + ); + false + } + } + } + + fn shares_endpoint_with(&self, other: &Self) -> bool { + self.endpoints.iter().any(|endpoint| { + other + .endpoints + .iter() + .any(|candidate| candidate == endpoint) + }) + } + + async fn create_blank( + &self, + volume_id: &str, + size_bytes: u64, + ) -> Result { + let request = CoronaFsCreateRequest { + size_bytes, + format: None, + backing_file: None, + backing_format: None, + }; + self.try_request("create CoronaFS volume", |endpoint, http| { + http.put(format!("{endpoint}/v1/volumes/{volume_id}")) + .json(&request) + }) + .await? + .json::() + .await + .map_err(|e| Status::internal(format!("failed to decode CoronaFS create response: {e}"))) + } + + async fn create_image_backed( + &self, + volume_id: &str, + size_bytes: u64, + backing_file: &Path, + backing_format: CoronaFsVolumeFormat, + ) -> Result { + let request = CoronaFsCreateRequest { + size_bytes, + format: Some(CoronaFsVolumeFormat::Qcow2), + backing_file: Some(backing_file.to_string_lossy().into_owned()), + backing_format: Some(backing_format), + }; + self.try_request("create CoronaFS image-backed volume", |endpoint, http| { + http.put(format!("{endpoint}/v1/volumes/{volume_id}")) + .json(&request) + }) + .await? + .json::() + .await + .map_err(|e| { + Status::internal(format!( + "failed to decode CoronaFS image-backed create response: {e}" + )) + }) + } + + async fn get_volume(&self, volume_id: &str) -> Result { + self.try_request("inspect CoronaFS volume", |endpoint, http| { + http.get(format!("{endpoint}/v1/volumes/{volume_id}")) + }) + .await? + .json::() + .await + .map_err(|e| Status::internal(format!("failed to decode CoronaFS inspect response: {e}"))) + } + + async fn get_volume_optional( + &self, + volume_id: &str, + ) -> Result, Status> { + match self.get_volume(volume_id).await { + Ok(volume) => Ok(Some(volume)), + Err(status) if status.code() == tonic::Code::NotFound => Ok(None), + Err(status) => Err(status), + } } async fn ensure_export(&self, volume_id: &str) -> Result { + self.ensure_export_with_mode(volume_id, false).await + } + + async fn release_export(&self, volume_id: &str) -> Result<(), Status> { + self.try_request("release CoronaFS export", |endpoint, http| { + http.delete(format!("{endpoint}/v1/volumes/{volume_id}/export")) + }) + .await?; + Ok(()) + } + + async fn ensure_export_read_only(&self, volume_id: &str) -> Result { + self.ensure_export_with_mode(volume_id, true).await + } + + async fn ensure_export_with_mode( + &self, + volume_id: &str, + read_only: bool, + ) -> Result { let response = self - .http - .post(format!("{}/v1/volumes/{}/export", self.endpoint, volume_id)) - .send() - .await - .map_err(request_error("export CoronaFS volume"))? - .error_for_status() - .map_err(http_status_error("export CoronaFS volume"))? + .try_request("export CoronaFS volume", |endpoint, http| { + http.post(format!("{endpoint}/v1/volumes/{volume_id}/export")) + .query(&[("read_only", read_only)]) + }) + .await? .json::() .await - .map_err(|e| Status::internal(format!("failed to decode CoronaFS export response: {e}")))?; - response - .export - .ok_or_else(|| Status::internal("CoronaFS export response did not include an export URI")) + .map_err(|e| { + Status::internal(format!("failed to decode CoronaFS export response: {e}")) + })?; + response.export.ok_or_else(|| { + Status::internal("CoronaFS export response did not include an export URI") + }) + } + + async fn materialize_from_export( + &self, + volume_id: &str, + source_uri: &str, + size_bytes: u64, + format: Option, + lazy: bool, + ) -> Result { + let request = CoronaFsMaterializeRequest { + source_uri: source_uri.to_string(), + size_bytes, + format, + lazy, + }; + self.try_request("materialize CoronaFS volume", |endpoint, http| { + http.post(format!("{endpoint}/v1/volumes/{volume_id}/materialize")) + .json(&request) + }) + .await? + .json::() + .await + .map_err(|e| { + Status::internal(format!( + "failed to decode CoronaFS materialize response: {e}" + )) + }) } async fn resize_volume(&self, volume_id: &str, size_bytes: u64) -> Result<(), Status> { - self.http - .post(format!("{}/v1/volumes/{}/resize", self.endpoint, volume_id)) - .json(&CoronaFsResizeRequest { size_bytes }) - .send() - .await - .map_err(request_error("resize CoronaFS volume"))? - .error_for_status() - .map_err(http_status_error("resize CoronaFS volume"))?; + let request = CoronaFsResizeRequest { size_bytes }; + self.try_request("resize CoronaFS volume", |endpoint, http| { + http.post(format!("{endpoint}/v1/volumes/{volume_id}/resize")) + .json(&request) + }) + .await?; Ok(()) } async fn delete_volume(&self, volume_id: &str) -> Result<(), Status> { - self.http - .delete(format!("{}/v1/volumes/{}", self.endpoint, volume_id)) - .send() - .await - .map_err(request_error("delete CoronaFS volume"))? - .error_for_status() - .map_err(http_status_error("delete CoronaFS volume"))?; + self.try_request("delete CoronaFS volume", |endpoint, http| { + http.delete(format!("{endpoint}/v1/volumes/{volume_id}")) + }) + .await?; Ok(()) } + + async fn try_request( + &self, + context: &'static str, + mut build_request: F, + ) -> Result + where + F: FnMut(&str, &reqwest::Client) -> reqwest::RequestBuilder, + { + let mut last_retryable_status = None; + let mut last_transport_error = None; + + for endpoint in &self.endpoints { + let response = match build_request(endpoint, &self.http).send().await { + Ok(response) => response, + Err(error) => { + tracing::warn!( + endpoint = %endpoint, + error = %error, + "{context} failed against CoronaFS endpoint" + ); + last_transport_error = Some(error); + continue; + } + }; + + match response.error_for_status() { + Ok(response) => return Ok(response), + Err(error) if retryable_coronafs_status(error.status()) => { + tracing::warn!( + endpoint = %endpoint, + error = %error, + "{context} received retryable response from CoronaFS endpoint" + ); + last_retryable_status = Some(error); + } + Err(error) => return Err(http_status_error(context)(error)), + } + } + + if let Some(error) = last_retryable_status { + return Err(http_status_error(context)(error)); + } + + Err(Status::internal(format!( + "failed to {context}: {}", + last_transport_error + .map(|error| error.to_string()) + .unwrap_or_else(|| "no usable CoronaFS endpoints configured".to_string()) + ))) + } } -fn request_error(context: &'static str) -> impl Fn(reqwest::Error) -> Status { - move |error| Status::internal(format!("failed to {context}: {error}")) +fn retryable_coronafs_status(status: Option) -> bool { + matches!( + status, + Some(reqwest::StatusCode::BAD_GATEWAY) + | Some(reqwest::StatusCode::SERVICE_UNAVAILABLE) + | Some(reqwest::StatusCode::GATEWAY_TIMEOUT) + ) } fn http_status_error(context: &'static str) -> impl Fn(reqwest::Error) -> Status { - move |error| Status::internal(format!("{context} returned an error: {error}")) + move |error| match error.status() { + Some(reqwest::StatusCode::NOT_FOUND) => { + Status::not_found(format!("{context} returned an error: {error}")) + } + Some(reqwest::StatusCode::BAD_REQUEST) + | Some(reqwest::StatusCode::CONFLICT) + | Some(reqwest::StatusCode::PRECONDITION_FAILED) + | Some(reqwest::StatusCode::METHOD_NOT_ALLOWED) => { + Status::failed_precondition(format!("{context} returned an error: {error}")) + } + _ => Status::internal(format!("{context} returned an error: {error}")), + } +} + +fn coronafs_endpoint_host(endpoint: &str) -> Option { + let parsed = reqwest::Url::parse(endpoint).ok()?; + let host = parsed.host_str()?.trim_matches(['[', ']']); + host.parse().ok() +} + +async fn local_ip_addrs() -> Result, Status> { + let mut last_error = None; + let candidates = [ + "/run/current-system/sw/bin/ip", + "/nix/var/nix/profiles/default/bin/ip", + "ip", + ]; + let mut output = None; + + for candidate in candidates { + match Command::new(candidate) + .args(["-o", "addr", "show", "up"]) + .output() + .await + { + Ok(candidate_output) if candidate_output.status.success() => { + output = Some(candidate_output); + break; + } + Ok(candidate_output) => { + last_error = Some(format!( + "{candidate} exited with status {}", + candidate_output.status + )); + } + Err(error) => { + last_error = Some(format!("{candidate}: {error}")); + } + } + } + + let output = output.ok_or_else(|| { + Status::internal(format!( + "failed to enumerate local IP addresses: {}", + last_error.unwrap_or_else(|| "no usable ip command found".to_string()) + )) + })?; + + let stdout = String::from_utf8(output.stdout) + .map_err(|e| Status::internal(format!("failed to parse local IP address output: {e}")))?; + let mut addresses = Vec::new(); + for line in stdout.lines() { + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 4 { + continue; + } + if fields[2] != "inet" && fields[2] != "inet6" { + continue; + } + let Some((ip, _prefix_len)) = fields[3].split_once('/') else { + continue; + }; + if let Ok(addr) = ip.parse::() { + addresses.push(addr); + } + } + Ok(addresses) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::os::unix::fs::PermissionsExt; + use tempfile::tempdir; + + #[cfg(unix)] + #[tokio::test] + async fn local_coronafs_clone_keeps_group_writable_volume_permissions() { + let tempdir = tempdir().unwrap(); + let source = tempdir.path().join("source.raw"); + let destination = tempdir.path().join("volumes").join("dest.raw"); + tokio::fs::create_dir_all(destination.parent().unwrap()) + .await + .unwrap(); + tokio::fs::write(&source, b"raw-seed").await.unwrap(); + + clone_local_raw_into_coronafs_volume(&source, &destination, 4096) + .await + .unwrap(); + + let cloned = tokio::fs::read(&destination).await.unwrap(); + assert_eq!(&cloned[..8], b"raw-seed"); + assert_eq!(cloned.len(), 4096); + + let mode = std::fs::metadata(&destination) + .unwrap() + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o660); + } + + #[test] + fn coronafs_endpoint_host_extracts_ip_hosts_only() { + assert_eq!( + coronafs_endpoint_host("http://10.100.0.11:50088"), + Some("10.100.0.11".parse().unwrap()) + ); + assert_eq!( + coronafs_endpoint_host("https://[::1]:50088"), + Some("::1".parse().unwrap()) + ); + assert_eq!( + coronafs_endpoint_host("http://coronafs.internal:50088"), + None + ); + } + + #[test] + fn resolve_coronafs_endpoints_prefers_split_endpoints_and_falls_back_to_legacy() { + assert_eq!( + resolve_coronafs_endpoints( + Some("http://controller.internal:50088".to_string()), + Some("http://127.0.0.1:50088".to_string()), + Some("http://legacy.internal:50088".to_string()) + ), + ( + Some("http://controller.internal:50088".to_string()), + Some("http://127.0.0.1:50088".to_string()) + ) + ); + + assert_eq!( + resolve_coronafs_endpoints( + Some("http://controller.internal:50088".to_string()), + None, + Some("http://legacy.internal:50088".to_string()) + ), + ( + Some("http://controller.internal:50088".to_string()), + Some("http://legacy.internal:50088".to_string()) + ) + ); + + assert_eq!( + resolve_coronafs_endpoints( + None, + None, + Some("http://legacy.internal:50088".to_string()) + ), + ( + Some("http://legacy.internal:50088".to_string()), + Some("http://legacy.internal:50088".to_string()) + ) + ); + } + + #[test] + fn normalize_coronafs_endpoint_supports_comma_separated_values() { + assert_eq!( + normalize_coronafs_endpoint(" 10.0.0.1:50088, http://10.0.0.2:50088/ ".to_string()), + Some("http://10.0.0.1:50088,http://10.0.0.2:50088".to_string()) + ); + } + + #[test] + fn parse_coronafs_endpoints_normalizes_candidates() { + assert_eq!( + parse_coronafs_endpoints("10.0.0.1:50088, https://10.0.0.2:50088/, ,"), + vec![ + "http://10.0.0.1:50088".to_string(), + "https://10.0.0.2:50088".to_string() + ] + ); + } + + #[test] + fn coronafs_clients_detect_shared_endpoints_from_lists() { + let controller = + CoronaFsClient::new("http://10.0.0.1:50088,http://10.0.0.2:50088".to_string()); + let node = CoronaFsClient::new("http://10.0.0.2:50088".to_string()); + let remote = CoronaFsClient::new("http://10.0.0.3:50088".to_string()); + + assert!(controller.shares_endpoint_with(&node)); + assert!(!controller.shares_endpoint_with(&remote)); + } + + #[test] + fn parse_truthy_recognizes_expected_values() { + assert!(parse_truthy("true")); + assert!(parse_truthy("On")); + assert!(parse_truthy(" yes ")); + assert!(!parse_truthy("0")); + assert!(!parse_truthy("false")); + } + + #[test] + fn coronafs_node_local_attach_prefers_stable_env_flag() { + assert!(!coronafs_node_local_attach_enabled_from_values(None, None)); + assert!(coronafs_node_local_attach_enabled_from_values( + None, + Some("true") + )); + assert!(!coronafs_node_local_attach_enabled_from_values( + Some("false"), + Some("true") + )); + assert!(coronafs_node_local_attach_enabled_from_values( + Some("on"), + Some("false") + )); + } + + #[test] + fn lazy_node_local_qcow2_prefers_export_attachment() { + let lazy_local = CoronaFsVolumeResponse { + id: "vol".to_string(), + size_bytes: 1024, + format: Some(CoronaFsVolumeFormat::Qcow2), + node_local: true, + materialized_from: Some("nbd://10.0.0.1:11000".to_string()), + path: "/tmp/vol.qcow2".to_string(), + export: None, + }; + let plain_local = CoronaFsVolumeResponse { + materialized_from: None, + ..lazy_local.clone() + }; + let raw_local = CoronaFsVolumeResponse { + format: Some(CoronaFsVolumeFormat::Raw), + materialized_from: Some("nbd://10.0.0.1:11000".to_string()), + ..lazy_local.clone() + }; + + assert!(should_prefer_coronafs_export_attachment(&lazy_local)); + assert!(!should_prefer_coronafs_export_attachment(&plain_local)); + assert!(!should_prefer_coronafs_export_attachment(&raw_local)); + } + + #[test] + fn coronafs_export_attachment_is_presented_as_raw_block_device() { + let attachment = DiskAttachment::Nbd { + uri: "nbd://10.0.0.1:11000".to_string(), + format: VolumeFormat::Raw, + }; + match attachment { + DiskAttachment::Nbd { format, .. } => assert_eq!(format, VolumeFormat::Raw), + other => panic!("expected NBD attachment, got {other:?}"), + } + } + + #[test] + fn pending_coronafs_image_seed_metadata_helpers_round_trip() { + let mut volume = Volume::new("vol", "vol", "org", "project", 10); + assert!(volume_coronafs_image_source_id(&volume).is_none()); + assert!(!volume_has_pending_coronafs_image_seed(&volume)); + + volume.metadata.insert( + CORONAFS_IMAGE_SOURCE_ID_METADATA_KEY.to_string(), + "image-123".to_string(), + ); + volume.metadata.insert( + CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY.to_string(), + "true".to_string(), + ); + + assert_eq!(volume_coronafs_image_source_id(&volume), Some("image-123")); + assert!(volume_has_pending_coronafs_image_seed(&volume)); + + volume + .metadata + .remove(CORONAFS_IMAGE_SEED_PENDING_METADATA_KEY); + assert!(!volume_has_pending_coronafs_image_seed(&volume)); + } } diff --git a/plasmavmc/crates/plasmavmc-types/src/vm.rs b/plasmavmc/crates/plasmavmc-types/src/vm.rs index 3e15a34..64f3037 100644 --- a/plasmavmc/crates/plasmavmc-types/src/vm.rs +++ b/plasmavmc/crates/plasmavmc-types/src/vm.rs @@ -233,7 +233,7 @@ impl Default for DiskSpec { source: DiskSource::Blank, size_gib: 10, bus: DiskBus::Virtio, - cache: DiskCache::None, + cache: DiskCache::Writeback, boot_index: None, } } @@ -370,7 +370,7 @@ impl Default for VmStatus { } /// Driver-specific backing for a persistent VM volume. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum VolumeBacking { Managed, @@ -388,7 +388,7 @@ impl Default for VolumeBacking { } /// Persistent VM volume metadata. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Volume { /// Unique identifier pub id: String, @@ -412,6 +412,12 @@ pub struct Volume { pub backing: VolumeBacking, /// Attached VM, if any pub attached_to_vm: Option, + /// Node currently holding the writer attachment, if any + pub attached_to_node: Option, + /// Monotonic generation incremented whenever attachment ownership changes + pub attachment_generation: u64, + /// Attachment generation last flushed back to the authoritative backing store + pub last_flushed_attachment_generation: u64, /// Creation timestamp (Unix epoch) pub created_at: u64, /// Last update timestamp (Unix epoch) @@ -447,6 +453,9 @@ impl Volume { status: VolumeStatus::Pending, backing: VolumeBacking::Managed, attached_to_vm: None, + attached_to_node: None, + attachment_generation: 0, + last_flushed_attachment_generation: 0, created_at: now, updated_at: now, metadata: HashMap::new(), diff --git a/plasmavmc/proto/plasmavmc.proto b/plasmavmc/proto/plasmavmc.proto index 66f6229..1bec549 100644 --- a/plasmavmc/proto/plasmavmc.proto +++ b/plasmavmc/proto/plasmavmc.proto @@ -525,6 +525,9 @@ message Volume { int64 created_at = 13; int64 updated_at = 14; VolumeBacking backing = 15; + string attached_to_node = 16; + uint64 attachment_generation = 17; + uint64 last_flushed_attachment_generation = 18; } message VolumeBacking { diff --git a/prismnet/Cargo.lock b/prismnet/Cargo.lock index 8a64aed..6e0d453 100644 --- a/prismnet/Cargo.lock +++ b/prismnet/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -40,9 +75,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,15 +90,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -90,9 +125,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apigateway-api" @@ -105,6 +140,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -161,9 +208,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -171,9 +218,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -203,7 +250,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -214,7 +261,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "bytes", "form_urlencoded", "futures-util", @@ -236,7 +283,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -264,9 +311,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -288,10 +335,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "2.10.0" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] [[package]] name = "block-buffer" @@ -304,9 +366,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -316,15 +378,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -384,9 +446,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -397,10 +459,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.53" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -408,9 +480,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -420,9 +492,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -432,24 +504,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -531,9 +603,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -550,9 +632,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -646,9 +728,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -719,9 +801,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -734,9 +816,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -744,15 +826,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -772,15 +854,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -789,21 +871,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -813,7 +895,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -829,9 +910,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -854,6 +935,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob-match" version = "0.2.1" @@ -862,9 +953,9 @@ checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -872,7 +963,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1049,7 +1140,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -1067,14 +1158,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1083,7 +1173,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1093,7 +1183,9 @@ dependencies = [ name = "iam-api" version = "0.1.0" dependencies = [ + "aes-gcm", "apigateway-api", + "argon2", "async-trait", "base64", "iam-audit", @@ -1103,6 +1195,7 @@ dependencies = [ "iam-types", "prost", "protoc-bin-vendored", + "rand_core 0.6.4", "serde", "serde_json", "sha2", @@ -1187,6 +1280,8 @@ dependencies = [ "http", "iam-client", "iam-types", + "serde_json", + "tokio", "tonic", "tracing", ] @@ -1222,9 +1317,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1358,19 +1453,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] -name = "ipnet" -version = "2.11.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1408,9 +1512,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1424,9 +1528,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1455,19 +1559,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1483,9 +1588,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1547,9 +1652,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" @@ -1572,7 +1677,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.13.0", "ipnet", "metrics", "metrics-util", @@ -1675,9 +1780,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1686,10 +1791,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking" @@ -1720,6 +1831,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.5" @@ -1743,23 +1865,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.1", + "indexmap 2.13.0", ] [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1768,9 +1890,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1785,10 +1907,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -1876,9 +2016,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2027,8 +2167,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2036,9 +2176,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -2049,7 +2189,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2064,16 +2204,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2131,7 +2271,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2163,18 +2303,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2184,9 +2324,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2195,9 +2335,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -2227,14 +2367,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -2245,7 +2385,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2259,9 +2399,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -2272,9 +2412,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -2288,9 +2428,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2309,9 +2449,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2319,9 +2459,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -2337,15 +2477,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2358,9 +2498,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2371,9 +2511,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2470,10 +2610,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2485,7 +2626,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -2497,9 +2638,9 @@ checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2522,12 +2663,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2569,7 +2710,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -2579,7 +2720,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -2656,7 +2797,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -2680,7 +2821,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", ] @@ -2716,9 +2857,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2747,9 +2888,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2769,11 +2910,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2789,9 +2930,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2850,9 +2991,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2865,9 +3006,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2875,16 +3016,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2903,9 +3044,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2914,9 +3055,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2952,7 +3093,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -3048,9 +3189,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3075,7 +3216,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -3094,9 +3235,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3117,9 +3258,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3138,9 +3279,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3174,9 +3315,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3193,6 +3334,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3270,9 +3421,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -3285,9 +3436,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3298,11 +3449,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3311,9 +3463,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3321,9 +3473,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3334,18 +3486,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3367,14 +3519,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3694,18 +3846,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -3738,18 +3890,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/scripts/refresh-iam-workspace-locks.sh b/scripts/refresh-iam-workspace-locks.sh new file mode 100755 index 0000000..ac2e573 --- /dev/null +++ b/scripts/refresh-iam-workspace-locks.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +workspaces=( + iam + apigateway + creditservice + deployer + fiberlb + flashdns + k8shost + lightningstor + plasmavmc + prismnet +) + +cd "${REPO_ROOT}" + +for ws in "${workspaces[@]}"; do + echo "== ${ws} ==" + cargo generate-lockfile --manifest-path "${ws}/Cargo.toml" +done