├── .github ├── dependabot.yaml └── workflows │ ├── audit.yaml.disabled-for-now │ ├── cli-functionality.yaml │ ├── clippy.yaml │ ├── docs.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── docker-compose.yaml ├── libthere ├── .gitignore ├── Cargo.toml └── src │ ├── executor │ ├── mod.rs │ ├── simple.rs │ └── ssh.rs │ ├── ipc │ ├── http.rs │ └── mod.rs │ ├── lib.rs │ ├── log │ └── mod.rs │ └── plan │ ├── host.rs │ ├── mod.rs │ └── visitor.rs ├── test ├── ci │ ├── hosts.yaml │ ├── plan.yaml │ ├── ssh-hosts.yaml │ └── ssh-plan.yaml ├── hosts.yaml ├── invalid-plan.yaml ├── plan.yaml ├── ssh-hosts.yaml ├── ssh-plan.yaml └── working-ssh-hosts.yaml ├── there-agent ├── .gitignore ├── Cargo.toml └── src │ └── main.rs ├── there-cli ├── .gitignore ├── Cargo.toml └── src │ ├── commands │ ├── mod.rs │ └── plan.rs │ └── main.rs └── there-controller ├── .gitignore ├── Cargo.toml └── src ├── executor.rs ├── http_server.rs ├── keys.rs └── main.rs /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "12:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/audit.yaml.disabled-for-now: -------------------------------------------------------------------------------- 1 | name: "Run cargo audit" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "**.toml" 9 | pull_request: 10 | branches: 11 | - "mistress" 12 | paths: 13 | - "**.rs" 14 | - "**.toml" 15 | 16 | jobs: 17 | run-cargo-audit: 18 | strategy: 19 | matrix: 20 | version: ["stable", "1.66"] 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - uses: "actions/checkout@v2" 24 | - name: "Install latest stable Rust" 25 | uses: "actions-rs/toolchain@v1" 26 | with: 27 | toolchain: "${{ matrix.version }}" 28 | override: true 29 | - name: "Install cargo-audit" 30 | run: "cargo install cargo-audit" 31 | - uses: "Swatinem/rust-cache@v1" 32 | with: 33 | key: "cargo-audit" 34 | - name: "Run cargo-audit" 35 | run: "cargo audit -q" 36 | -------------------------------------------------------------------------------- /.github/workflows/cli-functionality.yaml: -------------------------------------------------------------------------------- 1 | name: "Test CLI functionality" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "test/**" 9 | - "there-cli/**" 10 | pull_request: 11 | branches: 12 | - "mistress" 13 | paths: 14 | - "**.rs" 15 | - "test/**" 16 | - "there-cli/**" 17 | 18 | jobs: 19 | run-tests: 20 | strategy: 21 | matrix: 22 | version: ["stable", "nightly", "1.66"] 23 | runs-on: "ubuntu-latest" 24 | steps: 25 | - uses: "actions/checkout@v2" 26 | - name: "Make sure SSH will work as expected" 27 | run: | 28 | ssh-keygen -t ed25519 -f ~/.ssh/whatever -N '' 29 | echo -n 'from="127.0.0.1" ' | cat - ~/.ssh/whatever.pub > ~/.ssh/authorized_keys 30 | chmod og-rw ~ 31 | - name: "Install latest stable Rust" 32 | uses: "actions-rs/toolchain@v1" 33 | with: 34 | toolchain: "${{ matrix.version }}" 35 | override: true 36 | - uses: "Swatinem/rust-cache@v1" 37 | with: 38 | key: "clippy" 39 | - name: "Make sure the CLI passes valid config" 40 | run: "cargo run -p there-cli -- plan validate -f ./test/ci/plan.yaml --hosts ./test/ci/hosts.yaml" 41 | - name: "Make sure the CLI fails invalid config" 42 | run: "cargo run -p there-cli -- plan validate -f ./test/ci/invalid-plan.yaml --hosts ./test/ci/hosts.yaml || true" 43 | - name: "Make sure that the CLI can run basic configs" 44 | run: "cargo run -p there-cli -- plan apply -f ./test/ci/plan.yaml --hosts ./test/ci/hosts.yaml" 45 | # - name: "Make sure that the CLI can run SSH configs" 46 | # run: "cargo run -p there-cli -- plan apply -f ./test/ci/ssh-plan.yaml --hosts ./test/ci/ssh-hosts.yaml --ssh-key /home/runner/.ssh/id_rsa && sleep 5" # TODO: Figure out why sleep(1) needed here 47 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yaml: -------------------------------------------------------------------------------- 1 | name: "Run clippy lints" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | pull_request: 9 | branches: 10 | - "mistress" 11 | paths: 12 | - "**.rs" 13 | 14 | jobs: 15 | run-clippy: 16 | strategy: 17 | matrix: 18 | version: ["stable", "1.66"] 19 | runs-on: "ubuntu-latest" 20 | steps: 21 | - uses: "actions/checkout@v2" 22 | - name: "Install latest stable Rust" 23 | uses: "actions-rs/toolchain@v1" 24 | with: 25 | toolchain: "${{ matrix.version }}" 26 | override: true 27 | components: "clippy" 28 | - uses: "Swatinem/rust-cache@v1" 29 | with: 30 | key: "clippy" 31 | - name: "Run clippy" 32 | run: "cargo clippy --all-targets --all-features -- -D warnings" 33 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: "Build docs" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | pull_request: 9 | branches: 10 | - "mistress" 11 | paths: 12 | - "**.rs" 13 | 14 | jobs: 15 | run-clippy: 16 | strategy: 17 | matrix: 18 | version: ["stable", "1.66", "nightly"] 19 | if: "github.actor != 'dependabot'" 20 | runs-on: "ubuntu-latest" 21 | steps: 22 | - uses: "actions/checkout@v2" 23 | - name: "Install latest stable Rust" 24 | uses: "actions-rs/toolchain@v1" 25 | with: 26 | toolchain: "${{ matrix.version }}" 27 | override: true 28 | - uses: "Swatinem/rust-cache@v1" 29 | with: 30 | key: "doc" 31 | - name: "Run cargo doc" 32 | run: "cargo doc --workspace --all-features --examples --no-deps --locked" 33 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: "Run all tests" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | pull_request: 9 | branches: 10 | - "mistress" 11 | paths: 12 | - "**.rs" 13 | 14 | jobs: 15 | run-tests: 16 | strategy: 17 | matrix: 18 | version: ["stable", "nightly", "1.66"] 19 | runs-on: "ubuntu-latest" 20 | steps: 21 | - uses: "actions/checkout@v2" 22 | - name: "Install latest stable Rust" 23 | uses: "actions-rs/toolchain@v1" 24 | with: 25 | toolchain: "${{ matrix.version }}" 26 | override: true 27 | - uses: "Swatinem/rust-cache@v1" 28 | with: 29 | key: "clippy" 30 | - name: "Run tests" 31 | run: "cargo test" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tmp 3 | /env/* 4 | agent-token 5 | /there-controller-* 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: "https://github.com/pre-commit/pre-commit-hooks" 4 | rev: "v4.3.0" 5 | hooks: 6 | - id: "check-merge-conflict" 7 | - id: "check-toml" 8 | - id: "check-yaml" 9 | - id: "end-of-file-fixer" 10 | - id: "mixed-line-ending" 11 | - id: "trailing-whitespace" 12 | - repo: "local" 13 | hooks: 14 | - id: "format" 15 | name: "rust: cargo fmt" 16 | entry: "cargo fmt --all --check" 17 | language: "system" 18 | pass_filenames: false 19 | files: ".rs*$" 20 | - id: "clippy" 21 | name: "rust: cargo clippy" 22 | entry: "cargo clippy --all-targets --all-features -- -D warnings" 23 | language: "system" 24 | pass_filenames: false 25 | files: ".rs*$" 26 | - id: "test" 27 | name: "rust: cargo test" 28 | entry: "cargo test" 29 | language: "system" 30 | pass_filenames: false 31 | files: ".rs*$" 32 | - id: "doc" 33 | name: "rust: cargo doc" 34 | entry: "cargo doc --workspace --all-features --examples --no-deps --locked --frozen" 35 | language: "system" 36 | pass_filenames: false 37 | files: ".rs*$" 38 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.17.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aes" 22 | version = "0.7.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" 25 | dependencies = [ 26 | "cfg-if", 27 | "cipher 0.3.0", 28 | "cpufeatures", 29 | "ctr", 30 | "opaque-debug", 31 | ] 32 | 33 | [[package]] 34 | name = "ahash" 35 | version = "0.8.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107" 38 | dependencies = [ 39 | "cfg-if", 40 | "getrandom", 41 | "once_cell", 42 | "version_check", 43 | ] 44 | 45 | [[package]] 46 | name = "aho-corasick" 47 | version = "1.0.1" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 50 | dependencies = [ 51 | "memchr", 52 | ] 53 | 54 | [[package]] 55 | name = "anstream" 56 | version = "0.6.4" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 59 | dependencies = [ 60 | "anstyle", 61 | "anstyle-parse", 62 | "anstyle-query", 63 | "anstyle-wincon", 64 | "colorchoice", 65 | "utf8parse", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle" 70 | version = "1.0.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 73 | 74 | [[package]] 75 | name = "anstyle-parse" 76 | version = "0.2.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 79 | dependencies = [ 80 | "utf8parse", 81 | ] 82 | 83 | [[package]] 84 | name = "anstyle-query" 85 | version = "1.0.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 88 | dependencies = [ 89 | "windows-sys 0.48.0", 90 | ] 91 | 92 | [[package]] 93 | name = "anstyle-wincon" 94 | version = "3.0.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 97 | dependencies = [ 98 | "anstyle", 99 | "windows-sys 0.48.0", 100 | ] 101 | 102 | [[package]] 103 | name = "async-trait" 104 | version = "0.1.74" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" 107 | dependencies = [ 108 | "proc-macro2", 109 | "quote", 110 | "syn 2.0.28", 111 | ] 112 | 113 | [[package]] 114 | name = "autocfg" 115 | version = "1.1.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 118 | 119 | [[package]] 120 | name = "axum" 121 | version = "0.6.20" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" 124 | dependencies = [ 125 | "async-trait", 126 | "axum-core", 127 | "bitflags 1.3.2", 128 | "bytes", 129 | "futures-util", 130 | "http", 131 | "http-body", 132 | "hyper", 133 | "itoa", 134 | "matchit", 135 | "memchr", 136 | "mime", 137 | "percent-encoding", 138 | "pin-project-lite", 139 | "rustversion", 140 | "serde", 141 | "serde_json", 142 | "serde_path_to_error", 143 | "serde_urlencoded", 144 | "sync_wrapper", 145 | "tokio", 146 | "tower", 147 | "tower-layer", 148 | "tower-service", 149 | ] 150 | 151 | [[package]] 152 | name = "axum-core" 153 | version = "0.3.4" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" 156 | dependencies = [ 157 | "async-trait", 158 | "bytes", 159 | "futures-util", 160 | "http", 161 | "http-body", 162 | "mime", 163 | "rustversion", 164 | "tower-layer", 165 | "tower-service", 166 | ] 167 | 168 | [[package]] 169 | name = "backtrace" 170 | version = "0.3.66" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" 173 | dependencies = [ 174 | "addr2line", 175 | "cc", 176 | "cfg-if", 177 | "libc", 178 | "miniz_oxide", 179 | "object", 180 | "rustc-demangle", 181 | ] 182 | 183 | [[package]] 184 | name = "base64" 185 | version = "0.21.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 188 | 189 | [[package]] 190 | name = "base64ct" 191 | version = "1.5.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" 194 | 195 | [[package]] 196 | name = "bcrypt-pbkdf" 197 | version = "0.10.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" 200 | dependencies = [ 201 | "blowfish", 202 | "pbkdf2 0.12.1", 203 | "sha2 0.10.6", 204 | ] 205 | 206 | [[package]] 207 | name = "bit-vec" 208 | version = "0.6.3" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 211 | 212 | [[package]] 213 | name = "bitflags" 214 | version = "1.3.2" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 217 | 218 | [[package]] 219 | name = "bitflags" 220 | version = "2.4.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 223 | 224 | [[package]] 225 | name = "block-buffer" 226 | version = "0.9.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 229 | dependencies = [ 230 | "generic-array", 231 | ] 232 | 233 | [[package]] 234 | name = "block-buffer" 235 | version = "0.10.4" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 238 | dependencies = [ 239 | "generic-array", 240 | ] 241 | 242 | [[package]] 243 | name = "block-modes" 244 | version = "0.8.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" 247 | dependencies = [ 248 | "block-padding", 249 | "cipher 0.3.0", 250 | ] 251 | 252 | [[package]] 253 | name = "block-padding" 254 | version = "0.2.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" 257 | 258 | [[package]] 259 | name = "blowfish" 260 | version = "0.9.1" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" 263 | dependencies = [ 264 | "byteorder", 265 | "cipher 0.4.4", 266 | ] 267 | 268 | [[package]] 269 | name = "bumpalo" 270 | version = "3.11.1" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 273 | 274 | [[package]] 275 | name = "byteorder" 276 | version = "1.4.3" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 279 | 280 | [[package]] 281 | name = "bytes" 282 | version = "1.2.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 285 | 286 | [[package]] 287 | name = "cc" 288 | version = "1.0.73" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 291 | 292 | [[package]] 293 | name = "cfg-if" 294 | version = "1.0.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 297 | 298 | [[package]] 299 | name = "cipher" 300 | version = "0.3.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" 303 | dependencies = [ 304 | "generic-array", 305 | ] 306 | 307 | [[package]] 308 | name = "cipher" 309 | version = "0.4.4" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 312 | dependencies = [ 313 | "crypto-common", 314 | "inout", 315 | ] 316 | 317 | [[package]] 318 | name = "clap" 319 | version = "4.4.11" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" 322 | dependencies = [ 323 | "clap_builder", 324 | ] 325 | 326 | [[package]] 327 | name = "clap_builder" 328 | version = "4.4.11" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" 331 | dependencies = [ 332 | "anstream", 333 | "anstyle", 334 | "clap_lex", 335 | "strsim", 336 | ] 337 | 338 | [[package]] 339 | name = "clap_lex" 340 | version = "0.6.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 343 | 344 | [[package]] 345 | name = "color-eyre" 346 | version = "0.6.2" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" 349 | dependencies = [ 350 | "backtrace", 351 | "color-spantrace", 352 | "eyre", 353 | "indenter", 354 | "once_cell", 355 | "owo-colors", 356 | "tracing-error", 357 | "url", 358 | ] 359 | 360 | [[package]] 361 | name = "color-spantrace" 362 | version = "0.2.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" 365 | dependencies = [ 366 | "once_cell", 367 | "owo-colors", 368 | "tracing-core", 369 | "tracing-error", 370 | ] 371 | 372 | [[package]] 373 | name = "colorchoice" 374 | version = "1.0.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 377 | 378 | [[package]] 379 | name = "console" 380 | version = "0.15.2" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" 383 | dependencies = [ 384 | "encode_unicode", 385 | "lazy_static", 386 | "libc", 387 | "terminal_size", 388 | "unicode-width", 389 | "winapi", 390 | ] 391 | 392 | [[package]] 393 | name = "core-foundation" 394 | version = "0.9.3" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 397 | dependencies = [ 398 | "core-foundation-sys", 399 | "libc", 400 | ] 401 | 402 | [[package]] 403 | name = "core-foundation-sys" 404 | version = "0.8.3" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 407 | 408 | [[package]] 409 | name = "cpufeatures" 410 | version = "0.2.5" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 413 | dependencies = [ 414 | "libc", 415 | ] 416 | 417 | [[package]] 418 | name = "crc32fast" 419 | version = "1.3.2" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 422 | dependencies = [ 423 | "cfg-if", 424 | ] 425 | 426 | [[package]] 427 | name = "crypto-common" 428 | version = "0.1.6" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 431 | dependencies = [ 432 | "generic-array", 433 | "typenum", 434 | ] 435 | 436 | [[package]] 437 | name = "crypto-mac" 438 | version = "0.11.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" 441 | dependencies = [ 442 | "generic-array", 443 | "subtle", 444 | ] 445 | 446 | [[package]] 447 | name = "cryptovec" 448 | version = "0.6.1" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "ccc7fa13a6bbb2322d325292c57f4c8e7291595506f8289968a0eb61c3130bdf" 451 | dependencies = [ 452 | "libc", 453 | "winapi", 454 | ] 455 | 456 | [[package]] 457 | name = "ctr" 458 | version = "0.8.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" 461 | dependencies = [ 462 | "cipher 0.3.0", 463 | ] 464 | 465 | [[package]] 466 | name = "data-encoding" 467 | version = "2.3.2" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" 470 | 471 | [[package]] 472 | name = "deranged" 473 | version = "0.3.8" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" 476 | 477 | [[package]] 478 | name = "derive-getters" 479 | version = "0.3.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" 482 | dependencies = [ 483 | "proc-macro2", 484 | "quote", 485 | "syn 1.0.104", 486 | ] 487 | 488 | [[package]] 489 | name = "dialoguer" 490 | version = "0.11.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" 493 | dependencies = [ 494 | "console", 495 | "fuzzy-matcher", 496 | "shell-words", 497 | "tempfile", 498 | "thiserror", 499 | "zeroize", 500 | ] 501 | 502 | [[package]] 503 | name = "digest" 504 | version = "0.9.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 507 | dependencies = [ 508 | "generic-array", 509 | ] 510 | 511 | [[package]] 512 | name = "digest" 513 | version = "0.10.6" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 516 | dependencies = [ 517 | "block-buffer 0.10.4", 518 | "crypto-common", 519 | "subtle", 520 | ] 521 | 522 | [[package]] 523 | name = "directories" 524 | version = "5.0.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 527 | dependencies = [ 528 | "dirs-sys 0.4.1", 529 | ] 530 | 531 | [[package]] 532 | name = "dirs" 533 | version = "3.0.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" 536 | dependencies = [ 537 | "dirs-sys 0.3.7", 538 | ] 539 | 540 | [[package]] 541 | name = "dirs-sys" 542 | version = "0.3.7" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 545 | dependencies = [ 546 | "libc", 547 | "redox_users", 548 | "winapi", 549 | ] 550 | 551 | [[package]] 552 | name = "dirs-sys" 553 | version = "0.4.1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 556 | dependencies = [ 557 | "libc", 558 | "option-ext", 559 | "redox_users", 560 | "windows-sys 0.48.0", 561 | ] 562 | 563 | [[package]] 564 | name = "either" 565 | version = "1.8.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 568 | 569 | [[package]] 570 | name = "encode_unicode" 571 | version = "0.3.6" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 574 | 575 | [[package]] 576 | name = "encoding_rs" 577 | version = "0.8.31" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 580 | dependencies = [ 581 | "cfg-if", 582 | ] 583 | 584 | [[package]] 585 | name = "equivalent" 586 | version = "1.0.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" 589 | 590 | [[package]] 591 | name = "errno" 592 | version = "0.3.3" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" 595 | dependencies = [ 596 | "errno-dragonfly", 597 | "libc", 598 | "windows-sys 0.48.0", 599 | ] 600 | 601 | [[package]] 602 | name = "errno-dragonfly" 603 | version = "0.1.2" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 606 | dependencies = [ 607 | "cc", 608 | "libc", 609 | ] 610 | 611 | [[package]] 612 | name = "eyre" 613 | version = "0.6.8" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" 616 | dependencies = [ 617 | "indenter", 618 | "once_cell", 619 | ] 620 | 621 | [[package]] 622 | name = "fastrand" 623 | version = "1.8.0" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 626 | dependencies = [ 627 | "instant", 628 | ] 629 | 630 | [[package]] 631 | name = "flate2" 632 | version = "1.0.24" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 635 | dependencies = [ 636 | "crc32fast", 637 | "miniz_oxide", 638 | ] 639 | 640 | [[package]] 641 | name = "fnv" 642 | version = "1.0.7" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 645 | 646 | [[package]] 647 | name = "foreign-types" 648 | version = "0.3.2" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 651 | dependencies = [ 652 | "foreign-types-shared", 653 | ] 654 | 655 | [[package]] 656 | name = "foreign-types-shared" 657 | version = "0.1.1" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 660 | 661 | [[package]] 662 | name = "form_urlencoded" 663 | version = "1.1.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 666 | dependencies = [ 667 | "percent-encoding", 668 | ] 669 | 670 | [[package]] 671 | name = "futures" 672 | version = "0.3.28" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 675 | dependencies = [ 676 | "futures-channel", 677 | "futures-core", 678 | "futures-executor", 679 | "futures-io", 680 | "futures-sink", 681 | "futures-task", 682 | "futures-util", 683 | ] 684 | 685 | [[package]] 686 | name = "futures-channel" 687 | version = "0.3.28" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 690 | dependencies = [ 691 | "futures-core", 692 | "futures-sink", 693 | ] 694 | 695 | [[package]] 696 | name = "futures-core" 697 | version = "0.3.28" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 700 | 701 | [[package]] 702 | name = "futures-executor" 703 | version = "0.3.28" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 706 | dependencies = [ 707 | "futures-core", 708 | "futures-task", 709 | "futures-util", 710 | ] 711 | 712 | [[package]] 713 | name = "futures-io" 714 | version = "0.3.28" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 717 | 718 | [[package]] 719 | name = "futures-macro" 720 | version = "0.3.28" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 723 | dependencies = [ 724 | "proc-macro2", 725 | "quote", 726 | "syn 2.0.28", 727 | ] 728 | 729 | [[package]] 730 | name = "futures-sink" 731 | version = "0.3.28" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 734 | 735 | [[package]] 736 | name = "futures-task" 737 | version = "0.3.28" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 740 | 741 | [[package]] 742 | name = "futures-util" 743 | version = "0.3.28" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 746 | dependencies = [ 747 | "futures-channel", 748 | "futures-core", 749 | "futures-io", 750 | "futures-macro", 751 | "futures-sink", 752 | "futures-task", 753 | "memchr", 754 | "pin-project-lite", 755 | "pin-utils", 756 | "slab", 757 | ] 758 | 759 | [[package]] 760 | name = "fuzzy-matcher" 761 | version = "0.3.7" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 764 | dependencies = [ 765 | "thread_local", 766 | ] 767 | 768 | [[package]] 769 | name = "generic-array" 770 | version = "0.14.6" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 773 | dependencies = [ 774 | "typenum", 775 | "version_check", 776 | ] 777 | 778 | [[package]] 779 | name = "getrandom" 780 | version = "0.2.8" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 783 | dependencies = [ 784 | "cfg-if", 785 | "libc", 786 | "wasi", 787 | ] 788 | 789 | [[package]] 790 | name = "gimli" 791 | version = "0.26.2" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" 794 | 795 | [[package]] 796 | name = "h2" 797 | version = "0.3.17" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" 800 | dependencies = [ 801 | "bytes", 802 | "fnv", 803 | "futures-core", 804 | "futures-sink", 805 | "futures-util", 806 | "http", 807 | "indexmap 1.9.1", 808 | "slab", 809 | "tokio", 810 | "tokio-util", 811 | "tracing", 812 | ] 813 | 814 | [[package]] 815 | name = "hashbrown" 816 | version = "0.12.3" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 819 | 820 | [[package]] 821 | name = "hashbrown" 822 | version = "0.13.2" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 825 | 826 | [[package]] 827 | name = "hashbrown" 828 | version = "0.14.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 831 | 832 | [[package]] 833 | name = "hermit-abi" 834 | version = "0.1.19" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 837 | dependencies = [ 838 | "libc", 839 | ] 840 | 841 | [[package]] 842 | name = "hmac" 843 | version = "0.11.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" 846 | dependencies = [ 847 | "crypto-mac", 848 | "digest 0.9.0", 849 | ] 850 | 851 | [[package]] 852 | name = "home" 853 | version = "0.5.5" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 856 | dependencies = [ 857 | "windows-sys 0.48.0", 858 | ] 859 | 860 | [[package]] 861 | name = "http" 862 | version = "0.2.9" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 865 | dependencies = [ 866 | "bytes", 867 | "fnv", 868 | "itoa", 869 | ] 870 | 871 | [[package]] 872 | name = "http-body" 873 | version = "0.4.5" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 876 | dependencies = [ 877 | "bytes", 878 | "http", 879 | "pin-project-lite", 880 | ] 881 | 882 | [[package]] 883 | name = "httparse" 884 | version = "1.8.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 887 | 888 | [[package]] 889 | name = "httpdate" 890 | version = "1.0.2" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 893 | 894 | [[package]] 895 | name = "hyper" 896 | version = "0.14.27" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" 899 | dependencies = [ 900 | "bytes", 901 | "futures-channel", 902 | "futures-core", 903 | "futures-util", 904 | "h2", 905 | "http", 906 | "http-body", 907 | "httparse", 908 | "httpdate", 909 | "itoa", 910 | "pin-project-lite", 911 | "socket2 0.4.9", 912 | "tokio", 913 | "tower-service", 914 | "tracing", 915 | "want", 916 | ] 917 | 918 | [[package]] 919 | name = "hyper-tls" 920 | version = "0.5.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 923 | dependencies = [ 924 | "bytes", 925 | "hyper", 926 | "native-tls", 927 | "tokio", 928 | "tokio-native-tls", 929 | ] 930 | 931 | [[package]] 932 | name = "idna" 933 | version = "0.3.0" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 936 | dependencies = [ 937 | "unicode-bidi", 938 | "unicode-normalization", 939 | ] 940 | 941 | [[package]] 942 | name = "indenter" 943 | version = "0.3.3" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 946 | 947 | [[package]] 948 | name = "indexmap" 949 | version = "1.9.1" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 952 | dependencies = [ 953 | "autocfg", 954 | "hashbrown 0.12.3", 955 | ] 956 | 957 | [[package]] 958 | name = "indexmap" 959 | version = "2.0.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 962 | dependencies = [ 963 | "equivalent", 964 | "hashbrown 0.14.0", 965 | ] 966 | 967 | [[package]] 968 | name = "inout" 969 | version = "0.1.3" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 972 | dependencies = [ 973 | "generic-array", 974 | ] 975 | 976 | [[package]] 977 | name = "instant" 978 | version = "0.1.12" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 981 | dependencies = [ 982 | "cfg-if", 983 | ] 984 | 985 | [[package]] 986 | name = "ipnet" 987 | version = "2.5.0" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" 990 | 991 | [[package]] 992 | name = "itoa" 993 | version = "1.0.5" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 996 | 997 | [[package]] 998 | name = "js-sys" 999 | version = "0.3.60" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 1002 | dependencies = [ 1003 | "wasm-bindgen", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "lazy_static" 1008 | version = "1.4.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 1011 | 1012 | [[package]] 1013 | name = "libc" 1014 | version = "0.2.151" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" 1017 | 1018 | [[package]] 1019 | name = "libsodium-sys" 1020 | version = "0.2.7" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" 1023 | dependencies = [ 1024 | "cc", 1025 | "libc", 1026 | "pkg-config", 1027 | "walkdir", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "linux-raw-sys" 1032 | version = "0.4.7" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" 1035 | 1036 | [[package]] 1037 | name = "lock_api" 1038 | version = "0.4.9" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 1041 | dependencies = [ 1042 | "autocfg", 1043 | "scopeguard", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "log" 1048 | version = "0.4.17" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 1051 | dependencies = [ 1052 | "cfg-if", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "matchers" 1057 | version = "0.1.0" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1060 | dependencies = [ 1061 | "regex-automata 0.1.10", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "matchit" 1066 | version = "0.7.0" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" 1069 | 1070 | [[package]] 1071 | name = "md5" 1072 | version = "0.7.0" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 1075 | 1076 | [[package]] 1077 | name = "memchr" 1078 | version = "2.6.3" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 1081 | 1082 | [[package]] 1083 | name = "mime" 1084 | version = "0.3.16" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 1087 | 1088 | [[package]] 1089 | name = "miniz_oxide" 1090 | version = "0.5.4" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" 1093 | dependencies = [ 1094 | "adler", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "mio" 1099 | version = "0.8.10" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 1102 | dependencies = [ 1103 | "libc", 1104 | "wasi", 1105 | "windows-sys 0.48.0", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "nanoid" 1110 | version = "0.4.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" 1113 | dependencies = [ 1114 | "rand", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "nanoid-dictionary" 1119 | version = "0.4.3" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "3c6e9131c818f6826271c255f7323d1fcc9332c54b7cfb6c664ec506595d5d8e" 1122 | 1123 | [[package]] 1124 | name = "native-tls" 1125 | version = "0.2.10" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" 1128 | dependencies = [ 1129 | "lazy_static", 1130 | "libc", 1131 | "log", 1132 | "openssl", 1133 | "openssl-probe", 1134 | "openssl-sys", 1135 | "schannel", 1136 | "security-framework", 1137 | "security-framework-sys", 1138 | "tempfile", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "network-interface" 1143 | version = "1.0.3" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "afd878f93491173e5d272d0dd3124ff89739e50f276c7b9857ac0d97cc024f96" 1146 | dependencies = [ 1147 | "cc", 1148 | "libc", 1149 | "thiserror", 1150 | "winapi", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "nu-ansi-term" 1155 | version = "0.46.0" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1158 | dependencies = [ 1159 | "overload", 1160 | "winapi", 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "num-bigint" 1165 | version = "0.4.3" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" 1168 | dependencies = [ 1169 | "autocfg", 1170 | "num-integer", 1171 | "num-traits", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "num-integer" 1176 | version = "0.1.45" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 1179 | dependencies = [ 1180 | "autocfg", 1181 | "num-traits", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "num-traits" 1186 | version = "0.2.15" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 1189 | dependencies = [ 1190 | "autocfg", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "num_cpus" 1195 | version = "1.13.1" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 1198 | dependencies = [ 1199 | "hermit-abi", 1200 | "libc", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "num_threads" 1205 | version = "0.1.6" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 1208 | dependencies = [ 1209 | "libc", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "object" 1214 | version = "0.29.0" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" 1217 | dependencies = [ 1218 | "memchr", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "once_cell" 1223 | version = "1.15.0" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" 1226 | 1227 | [[package]] 1228 | name = "opaque-debug" 1229 | version = "0.3.0" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 1232 | 1233 | [[package]] 1234 | name = "openssl" 1235 | version = "0.10.42" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" 1238 | dependencies = [ 1239 | "bitflags 1.3.2", 1240 | "cfg-if", 1241 | "foreign-types", 1242 | "libc", 1243 | "once_cell", 1244 | "openssl-macros", 1245 | "openssl-sys", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "openssl-macros" 1250 | version = "0.1.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 1253 | dependencies = [ 1254 | "proc-macro2", 1255 | "quote", 1256 | "syn 1.0.104", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "openssl-probe" 1261 | version = "0.1.5" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1264 | 1265 | [[package]] 1266 | name = "openssl-sys" 1267 | version = "0.9.77" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" 1270 | dependencies = [ 1271 | "autocfg", 1272 | "cc", 1273 | "libc", 1274 | "pkg-config", 1275 | "vcpkg", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "option-ext" 1280 | version = "0.2.0" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1283 | 1284 | [[package]] 1285 | name = "overload" 1286 | version = "0.1.1" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1289 | 1290 | [[package]] 1291 | name = "owo-colors" 1292 | version = "3.5.0" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 1295 | 1296 | [[package]] 1297 | name = "parking_lot" 1298 | version = "0.12.1" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1301 | dependencies = [ 1302 | "lock_api", 1303 | "parking_lot_core", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "parking_lot_core" 1308 | version = "0.9.4" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" 1311 | dependencies = [ 1312 | "cfg-if", 1313 | "libc", 1314 | "redox_syscall", 1315 | "smallvec", 1316 | "windows-sys 0.42.0", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "password-hash" 1321 | version = "0.2.3" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" 1324 | dependencies = [ 1325 | "base64ct", 1326 | "rand_core", 1327 | "subtle", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "pbkdf2" 1332 | version = "0.8.0" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" 1335 | dependencies = [ 1336 | "base64ct", 1337 | "crypto-mac", 1338 | "hmac", 1339 | "password-hash", 1340 | "sha2 0.9.9", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "pbkdf2" 1345 | version = "0.12.1" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31" 1348 | dependencies = [ 1349 | "digest 0.10.6", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "percent-encoding" 1354 | version = "2.2.0" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 1357 | 1358 | [[package]] 1359 | name = "pin-project" 1360 | version = "1.0.12" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 1363 | dependencies = [ 1364 | "pin-project-internal", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "pin-project-internal" 1369 | version = "1.0.12" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 1372 | dependencies = [ 1373 | "proc-macro2", 1374 | "quote", 1375 | "syn 1.0.104", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "pin-project-lite" 1380 | version = "0.2.12" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" 1383 | 1384 | [[package]] 1385 | name = "pin-utils" 1386 | version = "0.1.0" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1389 | 1390 | [[package]] 1391 | name = "pkg-config" 1392 | version = "0.3.25" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 1395 | 1396 | [[package]] 1397 | name = "ppv-lite86" 1398 | version = "0.2.16" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 1401 | 1402 | [[package]] 1403 | name = "proc-macro2" 1404 | version = "1.0.63" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" 1407 | dependencies = [ 1408 | "unicode-ident", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "quick_cache" 1413 | version = "0.3.0" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "5253a3a0d56548d5b0be25414171dc780cc6870727746d05bd2bde352eee96c5" 1416 | dependencies = [ 1417 | "ahash", 1418 | "hashbrown 0.13.2", 1419 | "parking_lot", 1420 | ] 1421 | 1422 | [[package]] 1423 | name = "quote" 1424 | version = "1.0.29" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 1427 | dependencies = [ 1428 | "proc-macro2", 1429 | ] 1430 | 1431 | [[package]] 1432 | name = "rand" 1433 | version = "0.8.5" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1436 | dependencies = [ 1437 | "libc", 1438 | "rand_chacha", 1439 | "rand_core", 1440 | ] 1441 | 1442 | [[package]] 1443 | name = "rand_chacha" 1444 | version = "0.3.1" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1447 | dependencies = [ 1448 | "ppv-lite86", 1449 | "rand_core", 1450 | ] 1451 | 1452 | [[package]] 1453 | name = "rand_core" 1454 | version = "0.6.4" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1457 | dependencies = [ 1458 | "getrandom", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "redox_syscall" 1463 | version = "0.2.16" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1466 | dependencies = [ 1467 | "bitflags 1.3.2", 1468 | ] 1469 | 1470 | [[package]] 1471 | name = "redox_users" 1472 | version = "0.4.3" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 1475 | dependencies = [ 1476 | "getrandom", 1477 | "redox_syscall", 1478 | "thiserror", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "regex" 1483 | version = "1.10.2" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 1486 | dependencies = [ 1487 | "aho-corasick", 1488 | "memchr", 1489 | "regex-automata 0.4.3", 1490 | "regex-syntax 0.8.2", 1491 | ] 1492 | 1493 | [[package]] 1494 | name = "regex-automata" 1495 | version = "0.1.10" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1498 | dependencies = [ 1499 | "regex-syntax 0.6.29", 1500 | ] 1501 | 1502 | [[package]] 1503 | name = "regex-automata" 1504 | version = "0.4.3" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 1507 | dependencies = [ 1508 | "aho-corasick", 1509 | "memchr", 1510 | "regex-syntax 0.8.2", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "regex-syntax" 1515 | version = "0.6.29" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1518 | 1519 | [[package]] 1520 | name = "regex-syntax" 1521 | version = "0.8.2" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1524 | 1525 | [[package]] 1526 | name = "remove_dir_all" 1527 | version = "0.5.3" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1530 | dependencies = [ 1531 | "winapi", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "reqwest" 1536 | version = "0.11.20" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" 1539 | dependencies = [ 1540 | "base64", 1541 | "bytes", 1542 | "encoding_rs", 1543 | "futures-core", 1544 | "futures-util", 1545 | "h2", 1546 | "http", 1547 | "http-body", 1548 | "hyper", 1549 | "hyper-tls", 1550 | "ipnet", 1551 | "js-sys", 1552 | "log", 1553 | "mime", 1554 | "native-tls", 1555 | "once_cell", 1556 | "percent-encoding", 1557 | "pin-project-lite", 1558 | "serde", 1559 | "serde_json", 1560 | "serde_urlencoded", 1561 | "tokio", 1562 | "tokio-native-tls", 1563 | "tower-service", 1564 | "url", 1565 | "wasm-bindgen", 1566 | "wasm-bindgen-futures", 1567 | "web-sys", 1568 | "winreg", 1569 | ] 1570 | 1571 | [[package]] 1572 | name = "rustc-demangle" 1573 | version = "0.1.21" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" 1576 | 1577 | [[package]] 1578 | name = "rustix" 1579 | version = "0.38.13" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" 1582 | dependencies = [ 1583 | "bitflags 2.4.0", 1584 | "errno", 1585 | "libc", 1586 | "linux-raw-sys", 1587 | "windows-sys 0.48.0", 1588 | ] 1589 | 1590 | [[package]] 1591 | name = "rustversion" 1592 | version = "1.0.11" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" 1595 | 1596 | [[package]] 1597 | name = "ryu" 1598 | version = "1.0.11" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 1601 | 1602 | [[package]] 1603 | name = "same-file" 1604 | version = "1.0.6" 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" 1606 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1607 | dependencies = [ 1608 | "winapi-util", 1609 | ] 1610 | 1611 | [[package]] 1612 | name = "schannel" 1613 | version = "0.1.20" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 1616 | dependencies = [ 1617 | "lazy_static", 1618 | "windows-sys 0.36.1", 1619 | ] 1620 | 1621 | [[package]] 1622 | name = "scopeguard" 1623 | version = "1.1.0" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1626 | 1627 | [[package]] 1628 | name = "security-framework" 1629 | version = "2.7.0" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" 1632 | dependencies = [ 1633 | "bitflags 1.3.2", 1634 | "core-foundation", 1635 | "core-foundation-sys", 1636 | "libc", 1637 | "security-framework-sys", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "security-framework-sys" 1642 | version = "2.6.1" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 1645 | dependencies = [ 1646 | "core-foundation-sys", 1647 | "libc", 1648 | ] 1649 | 1650 | [[package]] 1651 | name = "serde" 1652 | version = "1.0.193" 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" 1654 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 1655 | dependencies = [ 1656 | "serde_derive", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "serde_derive" 1661 | version = "1.0.193" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 1664 | dependencies = [ 1665 | "proc-macro2", 1666 | "quote", 1667 | "syn 2.0.28", 1668 | ] 1669 | 1670 | [[package]] 1671 | name = "serde_json" 1672 | version = "1.0.107" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 1675 | dependencies = [ 1676 | "itoa", 1677 | "ryu", 1678 | "serde", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "serde_path_to_error" 1683 | version = "0.1.9" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341" 1686 | dependencies = [ 1687 | "serde", 1688 | ] 1689 | 1690 | [[package]] 1691 | name = "serde_urlencoded" 1692 | version = "0.7.1" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1695 | dependencies = [ 1696 | "form_urlencoded", 1697 | "itoa", 1698 | "ryu", 1699 | "serde", 1700 | ] 1701 | 1702 | [[package]] 1703 | name = "serde_yaml" 1704 | version = "0.9.25" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" 1707 | dependencies = [ 1708 | "indexmap 2.0.0", 1709 | "itoa", 1710 | "ryu", 1711 | "serde", 1712 | "unsafe-libyaml", 1713 | ] 1714 | 1715 | [[package]] 1716 | name = "sha2" 1717 | version = "0.9.9" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" 1720 | dependencies = [ 1721 | "block-buffer 0.9.0", 1722 | "cfg-if", 1723 | "cpufeatures", 1724 | "digest 0.9.0", 1725 | "opaque-debug", 1726 | ] 1727 | 1728 | [[package]] 1729 | name = "sha2" 1730 | version = "0.10.6" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" 1733 | dependencies = [ 1734 | "cfg-if", 1735 | "cpufeatures", 1736 | "digest 0.10.6", 1737 | ] 1738 | 1739 | [[package]] 1740 | name = "sharded-slab" 1741 | version = "0.1.4" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1744 | dependencies = [ 1745 | "lazy_static", 1746 | ] 1747 | 1748 | [[package]] 1749 | name = "shell-words" 1750 | version = "1.1.0" 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" 1752 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 1753 | 1754 | [[package]] 1755 | name = "signal-hook-registry" 1756 | version = "1.4.0" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1759 | dependencies = [ 1760 | "libc", 1761 | ] 1762 | 1763 | [[package]] 1764 | name = "slab" 1765 | version = "0.4.7" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1768 | dependencies = [ 1769 | "autocfg", 1770 | ] 1771 | 1772 | [[package]] 1773 | name = "smallvec" 1774 | version = "1.10.0" 1775 | source = "registry+https://github.com/rust-lang/crates.io-index" 1776 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1777 | 1778 | [[package]] 1779 | name = "socket2" 1780 | version = "0.4.9" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1783 | dependencies = [ 1784 | "libc", 1785 | "winapi", 1786 | ] 1787 | 1788 | [[package]] 1789 | name = "socket2" 1790 | version = "0.5.5" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 1793 | dependencies = [ 1794 | "libc", 1795 | "windows-sys 0.48.0", 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "strsim" 1800 | version = "0.10.0" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1803 | 1804 | [[package]] 1805 | name = "subtle" 1806 | version = "2.4.1" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 1809 | 1810 | [[package]] 1811 | name = "syn" 1812 | version = "1.0.104" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" 1815 | dependencies = [ 1816 | "proc-macro2", 1817 | "quote", 1818 | "unicode-ident", 1819 | ] 1820 | 1821 | [[package]] 1822 | name = "syn" 1823 | version = "2.0.28" 1824 | source = "registry+https://github.com/rust-lang/crates.io-index" 1825 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 1826 | dependencies = [ 1827 | "proc-macro2", 1828 | "quote", 1829 | "unicode-ident", 1830 | ] 1831 | 1832 | [[package]] 1833 | name = "sync_wrapper" 1834 | version = "0.1.1" 1835 | source = "registry+https://github.com/rust-lang/crates.io-index" 1836 | checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" 1837 | 1838 | [[package]] 1839 | name = "tempfile" 1840 | version = "3.3.0" 1841 | source = "registry+https://github.com/rust-lang/crates.io-index" 1842 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1843 | dependencies = [ 1844 | "cfg-if", 1845 | "fastrand", 1846 | "libc", 1847 | "redox_syscall", 1848 | "remove_dir_all", 1849 | "winapi", 1850 | ] 1851 | 1852 | [[package]] 1853 | name = "terminal_size" 1854 | version = "0.1.17" 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" 1856 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 1857 | dependencies = [ 1858 | "libc", 1859 | "winapi", 1860 | ] 1861 | 1862 | [[package]] 1863 | name = "there" 1864 | version = "0.1.2" 1865 | dependencies = [ 1866 | "async-trait", 1867 | "color-eyre", 1868 | "derive-getters", 1869 | "futures", 1870 | "serde", 1871 | "shell-words", 1872 | "thrussh", 1873 | "thrussh-keys", 1874 | "tokio", 1875 | "tokio-stream", 1876 | "tokio-util", 1877 | "tracing", 1878 | "which", 1879 | ] 1880 | 1881 | [[package]] 1882 | name = "there-agent" 1883 | version = "0.1.0" 1884 | dependencies = [ 1885 | "color-eyre", 1886 | "directories", 1887 | "reqwest", 1888 | "there", 1889 | "time", 1890 | "tracing-subscriber", 1891 | ] 1892 | 1893 | [[package]] 1894 | name = "there-cli" 1895 | version = "0.1.0" 1896 | dependencies = [ 1897 | "async-trait", 1898 | "clap", 1899 | "color-eyre", 1900 | "derive-getters", 1901 | "dialoguer", 1902 | "futures", 1903 | "regex", 1904 | "reqwest", 1905 | "serde_json", 1906 | "serde_yaml", 1907 | "there", 1908 | "thiserror", 1909 | "thrussh", 1910 | "thrussh-keys", 1911 | "time", 1912 | "tokio", 1913 | "tokio-stream", 1914 | "tokio-util", 1915 | "tracing", 1916 | "tracing-subscriber", 1917 | ] 1918 | 1919 | [[package]] 1920 | name = "there-controller" 1921 | version = "0.1.0" 1922 | dependencies = [ 1923 | "axum", 1924 | "color-eyre", 1925 | "futures", 1926 | "hyper", 1927 | "nanoid", 1928 | "nanoid-dictionary", 1929 | "network-interface", 1930 | "quick_cache", 1931 | "rand", 1932 | "serde", 1933 | "serde_json", 1934 | "there", 1935 | "thiserror", 1936 | "thrussh", 1937 | "thrussh-keys", 1938 | "time", 1939 | "tokio", 1940 | "tracing", 1941 | "tracing-subscriber", 1942 | ] 1943 | 1944 | [[package]] 1945 | name = "thiserror" 1946 | version = "1.0.50" 1947 | source = "registry+https://github.com/rust-lang/crates.io-index" 1948 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 1949 | dependencies = [ 1950 | "thiserror-impl", 1951 | ] 1952 | 1953 | [[package]] 1954 | name = "thiserror-impl" 1955 | version = "1.0.50" 1956 | source = "registry+https://github.com/rust-lang/crates.io-index" 1957 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 1958 | dependencies = [ 1959 | "proc-macro2", 1960 | "quote", 1961 | "syn 2.0.28", 1962 | ] 1963 | 1964 | [[package]] 1965 | name = "thread_local" 1966 | version = "1.1.4" 1967 | source = "registry+https://github.com/rust-lang/crates.io-index" 1968 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1969 | dependencies = [ 1970 | "once_cell", 1971 | ] 1972 | 1973 | [[package]] 1974 | name = "thrussh" 1975 | version = "0.34.0" 1976 | source = "registry+https://github.com/rust-lang/crates.io-index" 1977 | checksum = "0eb7f634184fe86d7a9fd587d9350137508cba7b77626a7785db2ca695ebc503" 1978 | dependencies = [ 1979 | "bitflags 1.3.2", 1980 | "byteorder", 1981 | "cryptovec", 1982 | "digest 0.9.0", 1983 | "flate2", 1984 | "futures", 1985 | "generic-array", 1986 | "log", 1987 | "openssl", 1988 | "rand", 1989 | "sha2 0.9.9", 1990 | "thiserror", 1991 | "thrussh-keys", 1992 | "thrussh-libsodium", 1993 | "tokio", 1994 | ] 1995 | 1996 | [[package]] 1997 | name = "thrussh-keys" 1998 | version = "0.22.1" 1999 | source = "registry+https://github.com/rust-lang/crates.io-index" 2000 | checksum = "c43d59b13e4c08db0e379bced99bda596ac5ed33651d919bf3916d34ad4259bb" 2001 | dependencies = [ 2002 | "aes", 2003 | "bcrypt-pbkdf", 2004 | "bit-vec", 2005 | "block-modes", 2006 | "byteorder", 2007 | "cryptovec", 2008 | "data-encoding", 2009 | "dirs", 2010 | "futures", 2011 | "hmac", 2012 | "log", 2013 | "md5", 2014 | "num-bigint", 2015 | "num-integer", 2016 | "openssl", 2017 | "pbkdf2 0.8.0", 2018 | "rand", 2019 | "serde", 2020 | "serde_derive", 2021 | "sha2 0.9.9", 2022 | "thiserror", 2023 | "thrussh-libsodium", 2024 | "tokio", 2025 | "tokio-stream", 2026 | "yasna", 2027 | ] 2028 | 2029 | [[package]] 2030 | name = "thrussh-libsodium" 2031 | version = "0.2.1" 2032 | source = "registry+https://github.com/rust-lang/crates.io-index" 2033 | checksum = "cfe89c70d27b1cb92e13bc8af63493e890d0de46dae4df0e28233f62b4ed9500" 2034 | dependencies = [ 2035 | "lazy_static", 2036 | "libc", 2037 | "libsodium-sys", 2038 | "pkg-config", 2039 | "vcpkg", 2040 | ] 2041 | 2042 | [[package]] 2043 | name = "time" 2044 | version = "0.3.29" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" 2047 | dependencies = [ 2048 | "deranged", 2049 | "itoa", 2050 | "libc", 2051 | "num_threads", 2052 | "serde", 2053 | "time-core", 2054 | "time-macros", 2055 | ] 2056 | 2057 | [[package]] 2058 | name = "time-core" 2059 | version = "0.1.2" 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" 2061 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 2062 | 2063 | [[package]] 2064 | name = "time-macros" 2065 | version = "0.2.15" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" 2068 | dependencies = [ 2069 | "time-core", 2070 | ] 2071 | 2072 | [[package]] 2073 | name = "tinyvec" 2074 | version = "1.6.0" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 2077 | dependencies = [ 2078 | "tinyvec_macros", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "tinyvec_macros" 2083 | version = "0.1.0" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 2086 | 2087 | [[package]] 2088 | name = "tokio" 2089 | version = "1.35.0" 2090 | source = "registry+https://github.com/rust-lang/crates.io-index" 2091 | checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" 2092 | dependencies = [ 2093 | "backtrace", 2094 | "bytes", 2095 | "libc", 2096 | "mio", 2097 | "num_cpus", 2098 | "parking_lot", 2099 | "pin-project-lite", 2100 | "signal-hook-registry", 2101 | "socket2 0.5.5", 2102 | "tokio-macros", 2103 | "windows-sys 0.48.0", 2104 | ] 2105 | 2106 | [[package]] 2107 | name = "tokio-macros" 2108 | version = "2.2.0" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 2111 | dependencies = [ 2112 | "proc-macro2", 2113 | "quote", 2114 | "syn 2.0.28", 2115 | ] 2116 | 2117 | [[package]] 2118 | name = "tokio-native-tls" 2119 | version = "0.3.0" 2120 | source = "registry+https://github.com/rust-lang/crates.io-index" 2121 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 2122 | dependencies = [ 2123 | "native-tls", 2124 | "tokio", 2125 | ] 2126 | 2127 | [[package]] 2128 | name = "tokio-stream" 2129 | version = "0.1.14" 2130 | source = "registry+https://github.com/rust-lang/crates.io-index" 2131 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 2132 | dependencies = [ 2133 | "futures-core", 2134 | "pin-project-lite", 2135 | "tokio", 2136 | ] 2137 | 2138 | [[package]] 2139 | name = "tokio-util" 2140 | version = "0.7.10" 2141 | source = "registry+https://github.com/rust-lang/crates.io-index" 2142 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 2143 | dependencies = [ 2144 | "bytes", 2145 | "futures-core", 2146 | "futures-sink", 2147 | "pin-project-lite", 2148 | "tokio", 2149 | "tracing", 2150 | ] 2151 | 2152 | [[package]] 2153 | name = "tower" 2154 | version = "0.4.13" 2155 | source = "registry+https://github.com/rust-lang/crates.io-index" 2156 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 2157 | dependencies = [ 2158 | "futures-core", 2159 | "futures-util", 2160 | "pin-project", 2161 | "pin-project-lite", 2162 | "tokio", 2163 | "tower-layer", 2164 | "tower-service", 2165 | "tracing", 2166 | ] 2167 | 2168 | [[package]] 2169 | name = "tower-layer" 2170 | version = "0.3.2" 2171 | source = "registry+https://github.com/rust-lang/crates.io-index" 2172 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 2173 | 2174 | [[package]] 2175 | name = "tower-service" 2176 | version = "0.3.2" 2177 | source = "registry+https://github.com/rust-lang/crates.io-index" 2178 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 2179 | 2180 | [[package]] 2181 | name = "tracing" 2182 | version = "0.1.38" 2183 | source = "registry+https://github.com/rust-lang/crates.io-index" 2184 | checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d" 2185 | dependencies = [ 2186 | "log", 2187 | "pin-project-lite", 2188 | "tracing-attributes", 2189 | "tracing-core", 2190 | ] 2191 | 2192 | [[package]] 2193 | name = "tracing-attributes" 2194 | version = "0.1.24" 2195 | source = "registry+https://github.com/rust-lang/crates.io-index" 2196 | checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" 2197 | dependencies = [ 2198 | "proc-macro2", 2199 | "quote", 2200 | "syn 2.0.28", 2201 | ] 2202 | 2203 | [[package]] 2204 | name = "tracing-core" 2205 | version = "0.1.30" 2206 | source = "registry+https://github.com/rust-lang/crates.io-index" 2207 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 2208 | dependencies = [ 2209 | "once_cell", 2210 | "valuable", 2211 | ] 2212 | 2213 | [[package]] 2214 | name = "tracing-error" 2215 | version = "0.2.0" 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" 2217 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 2218 | dependencies = [ 2219 | "tracing", 2220 | "tracing-subscriber", 2221 | ] 2222 | 2223 | [[package]] 2224 | name = "tracing-log" 2225 | version = "0.1.3" 2226 | source = "registry+https://github.com/rust-lang/crates.io-index" 2227 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 2228 | dependencies = [ 2229 | "lazy_static", 2230 | "log", 2231 | "tracing-core", 2232 | ] 2233 | 2234 | [[package]] 2235 | name = "tracing-serde" 2236 | version = "0.1.3" 2237 | source = "registry+https://github.com/rust-lang/crates.io-index" 2238 | checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" 2239 | dependencies = [ 2240 | "serde", 2241 | "tracing-core", 2242 | ] 2243 | 2244 | [[package]] 2245 | name = "tracing-subscriber" 2246 | version = "0.3.17" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 2249 | dependencies = [ 2250 | "matchers", 2251 | "nu-ansi-term", 2252 | "once_cell", 2253 | "regex", 2254 | "serde", 2255 | "serde_json", 2256 | "sharded-slab", 2257 | "smallvec", 2258 | "thread_local", 2259 | "time", 2260 | "tracing", 2261 | "tracing-core", 2262 | "tracing-log", 2263 | "tracing-serde", 2264 | ] 2265 | 2266 | [[package]] 2267 | name = "try-lock" 2268 | version = "0.2.3" 2269 | source = "registry+https://github.com/rust-lang/crates.io-index" 2270 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 2271 | 2272 | [[package]] 2273 | name = "typenum" 2274 | version = "1.15.0" 2275 | source = "registry+https://github.com/rust-lang/crates.io-index" 2276 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 2277 | 2278 | [[package]] 2279 | name = "unicode-bidi" 2280 | version = "0.3.8" 2281 | source = "registry+https://github.com/rust-lang/crates.io-index" 2282 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 2283 | 2284 | [[package]] 2285 | name = "unicode-ident" 2286 | version = "1.0.5" 2287 | source = "registry+https://github.com/rust-lang/crates.io-index" 2288 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 2289 | 2290 | [[package]] 2291 | name = "unicode-normalization" 2292 | version = "0.1.22" 2293 | source = "registry+https://github.com/rust-lang/crates.io-index" 2294 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 2295 | dependencies = [ 2296 | "tinyvec", 2297 | ] 2298 | 2299 | [[package]] 2300 | name = "unicode-width" 2301 | version = "0.1.10" 2302 | source = "registry+https://github.com/rust-lang/crates.io-index" 2303 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 2304 | 2305 | [[package]] 2306 | name = "unsafe-libyaml" 2307 | version = "0.2.7" 2308 | source = "registry+https://github.com/rust-lang/crates.io-index" 2309 | checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" 2310 | 2311 | [[package]] 2312 | name = "url" 2313 | version = "2.3.1" 2314 | source = "registry+https://github.com/rust-lang/crates.io-index" 2315 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 2316 | dependencies = [ 2317 | "form_urlencoded", 2318 | "idna", 2319 | "percent-encoding", 2320 | ] 2321 | 2322 | [[package]] 2323 | name = "utf8parse" 2324 | version = "0.2.1" 2325 | source = "registry+https://github.com/rust-lang/crates.io-index" 2326 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 2327 | 2328 | [[package]] 2329 | name = "valuable" 2330 | version = "0.1.0" 2331 | source = "registry+https://github.com/rust-lang/crates.io-index" 2332 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2333 | 2334 | [[package]] 2335 | name = "vcpkg" 2336 | version = "0.2.15" 2337 | source = "registry+https://github.com/rust-lang/crates.io-index" 2338 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2339 | 2340 | [[package]] 2341 | name = "version_check" 2342 | version = "0.9.4" 2343 | source = "registry+https://github.com/rust-lang/crates.io-index" 2344 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 2345 | 2346 | [[package]] 2347 | name = "walkdir" 2348 | version = "2.3.2" 2349 | source = "registry+https://github.com/rust-lang/crates.io-index" 2350 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 2351 | dependencies = [ 2352 | "same-file", 2353 | "winapi", 2354 | "winapi-util", 2355 | ] 2356 | 2357 | [[package]] 2358 | name = "want" 2359 | version = "0.3.0" 2360 | source = "registry+https://github.com/rust-lang/crates.io-index" 2361 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 2362 | dependencies = [ 2363 | "log", 2364 | "try-lock", 2365 | ] 2366 | 2367 | [[package]] 2368 | name = "wasi" 2369 | version = "0.11.0+wasi-snapshot-preview1" 2370 | source = "registry+https://github.com/rust-lang/crates.io-index" 2371 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2372 | 2373 | [[package]] 2374 | name = "wasm-bindgen" 2375 | version = "0.2.83" 2376 | source = "registry+https://github.com/rust-lang/crates.io-index" 2377 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 2378 | dependencies = [ 2379 | "cfg-if", 2380 | "wasm-bindgen-macro", 2381 | ] 2382 | 2383 | [[package]] 2384 | name = "wasm-bindgen-backend" 2385 | version = "0.2.83" 2386 | source = "registry+https://github.com/rust-lang/crates.io-index" 2387 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 2388 | dependencies = [ 2389 | "bumpalo", 2390 | "log", 2391 | "once_cell", 2392 | "proc-macro2", 2393 | "quote", 2394 | "syn 1.0.104", 2395 | "wasm-bindgen-shared", 2396 | ] 2397 | 2398 | [[package]] 2399 | name = "wasm-bindgen-futures" 2400 | version = "0.4.33" 2401 | source = "registry+https://github.com/rust-lang/crates.io-index" 2402 | checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" 2403 | dependencies = [ 2404 | "cfg-if", 2405 | "js-sys", 2406 | "wasm-bindgen", 2407 | "web-sys", 2408 | ] 2409 | 2410 | [[package]] 2411 | name = "wasm-bindgen-macro" 2412 | version = "0.2.83" 2413 | source = "registry+https://github.com/rust-lang/crates.io-index" 2414 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 2415 | dependencies = [ 2416 | "quote", 2417 | "wasm-bindgen-macro-support", 2418 | ] 2419 | 2420 | [[package]] 2421 | name = "wasm-bindgen-macro-support" 2422 | version = "0.2.83" 2423 | source = "registry+https://github.com/rust-lang/crates.io-index" 2424 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 2425 | dependencies = [ 2426 | "proc-macro2", 2427 | "quote", 2428 | "syn 1.0.104", 2429 | "wasm-bindgen-backend", 2430 | "wasm-bindgen-shared", 2431 | ] 2432 | 2433 | [[package]] 2434 | name = "wasm-bindgen-shared" 2435 | version = "0.2.83" 2436 | source = "registry+https://github.com/rust-lang/crates.io-index" 2437 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 2438 | 2439 | [[package]] 2440 | name = "web-sys" 2441 | version = "0.3.60" 2442 | source = "registry+https://github.com/rust-lang/crates.io-index" 2443 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 2444 | dependencies = [ 2445 | "js-sys", 2446 | "wasm-bindgen", 2447 | ] 2448 | 2449 | [[package]] 2450 | name = "which" 2451 | version = "5.0.0" 2452 | source = "registry+https://github.com/rust-lang/crates.io-index" 2453 | checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" 2454 | dependencies = [ 2455 | "either", 2456 | "home", 2457 | "once_cell", 2458 | "rustix", 2459 | "windows-sys 0.48.0", 2460 | ] 2461 | 2462 | [[package]] 2463 | name = "winapi" 2464 | version = "0.3.9" 2465 | source = "registry+https://github.com/rust-lang/crates.io-index" 2466 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2467 | dependencies = [ 2468 | "winapi-i686-pc-windows-gnu", 2469 | "winapi-x86_64-pc-windows-gnu", 2470 | ] 2471 | 2472 | [[package]] 2473 | name = "winapi-i686-pc-windows-gnu" 2474 | version = "0.4.0" 2475 | source = "registry+https://github.com/rust-lang/crates.io-index" 2476 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2477 | 2478 | [[package]] 2479 | name = "winapi-util" 2480 | version = "0.1.5" 2481 | source = "registry+https://github.com/rust-lang/crates.io-index" 2482 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2483 | dependencies = [ 2484 | "winapi", 2485 | ] 2486 | 2487 | [[package]] 2488 | name = "winapi-x86_64-pc-windows-gnu" 2489 | version = "0.4.0" 2490 | source = "registry+https://github.com/rust-lang/crates.io-index" 2491 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2492 | 2493 | [[package]] 2494 | name = "windows-sys" 2495 | version = "0.36.1" 2496 | source = "registry+https://github.com/rust-lang/crates.io-index" 2497 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 2498 | dependencies = [ 2499 | "windows_aarch64_msvc 0.36.1", 2500 | "windows_i686_gnu 0.36.1", 2501 | "windows_i686_msvc 0.36.1", 2502 | "windows_x86_64_gnu 0.36.1", 2503 | "windows_x86_64_msvc 0.36.1", 2504 | ] 2505 | 2506 | [[package]] 2507 | name = "windows-sys" 2508 | version = "0.42.0" 2509 | source = "registry+https://github.com/rust-lang/crates.io-index" 2510 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 2511 | dependencies = [ 2512 | "windows_aarch64_gnullvm 0.42.1", 2513 | "windows_aarch64_msvc 0.42.1", 2514 | "windows_i686_gnu 0.42.1", 2515 | "windows_i686_msvc 0.42.1", 2516 | "windows_x86_64_gnu 0.42.1", 2517 | "windows_x86_64_gnullvm 0.42.1", 2518 | "windows_x86_64_msvc 0.42.1", 2519 | ] 2520 | 2521 | [[package]] 2522 | name = "windows-sys" 2523 | version = "0.48.0" 2524 | source = "registry+https://github.com/rust-lang/crates.io-index" 2525 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2526 | dependencies = [ 2527 | "windows-targets", 2528 | ] 2529 | 2530 | [[package]] 2531 | name = "windows-targets" 2532 | version = "0.48.0" 2533 | source = "registry+https://github.com/rust-lang/crates.io-index" 2534 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 2535 | dependencies = [ 2536 | "windows_aarch64_gnullvm 0.48.0", 2537 | "windows_aarch64_msvc 0.48.0", 2538 | "windows_i686_gnu 0.48.0", 2539 | "windows_i686_msvc 0.48.0", 2540 | "windows_x86_64_gnu 0.48.0", 2541 | "windows_x86_64_gnullvm 0.48.0", 2542 | "windows_x86_64_msvc 0.48.0", 2543 | ] 2544 | 2545 | [[package]] 2546 | name = "windows_aarch64_gnullvm" 2547 | version = "0.42.1" 2548 | source = "registry+https://github.com/rust-lang/crates.io-index" 2549 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 2550 | 2551 | [[package]] 2552 | name = "windows_aarch64_gnullvm" 2553 | version = "0.48.0" 2554 | source = "registry+https://github.com/rust-lang/crates.io-index" 2555 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 2556 | 2557 | [[package]] 2558 | name = "windows_aarch64_msvc" 2559 | version = "0.36.1" 2560 | source = "registry+https://github.com/rust-lang/crates.io-index" 2561 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 2562 | 2563 | [[package]] 2564 | name = "windows_aarch64_msvc" 2565 | version = "0.42.1" 2566 | source = "registry+https://github.com/rust-lang/crates.io-index" 2567 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 2568 | 2569 | [[package]] 2570 | name = "windows_aarch64_msvc" 2571 | version = "0.48.0" 2572 | source = "registry+https://github.com/rust-lang/crates.io-index" 2573 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 2574 | 2575 | [[package]] 2576 | name = "windows_i686_gnu" 2577 | version = "0.36.1" 2578 | source = "registry+https://github.com/rust-lang/crates.io-index" 2579 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 2580 | 2581 | [[package]] 2582 | name = "windows_i686_gnu" 2583 | version = "0.42.1" 2584 | source = "registry+https://github.com/rust-lang/crates.io-index" 2585 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 2586 | 2587 | [[package]] 2588 | name = "windows_i686_gnu" 2589 | version = "0.48.0" 2590 | source = "registry+https://github.com/rust-lang/crates.io-index" 2591 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 2592 | 2593 | [[package]] 2594 | name = "windows_i686_msvc" 2595 | version = "0.36.1" 2596 | source = "registry+https://github.com/rust-lang/crates.io-index" 2597 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 2598 | 2599 | [[package]] 2600 | name = "windows_i686_msvc" 2601 | version = "0.42.1" 2602 | source = "registry+https://github.com/rust-lang/crates.io-index" 2603 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 2604 | 2605 | [[package]] 2606 | name = "windows_i686_msvc" 2607 | version = "0.48.0" 2608 | source = "registry+https://github.com/rust-lang/crates.io-index" 2609 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 2610 | 2611 | [[package]] 2612 | name = "windows_x86_64_gnu" 2613 | version = "0.36.1" 2614 | source = "registry+https://github.com/rust-lang/crates.io-index" 2615 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 2616 | 2617 | [[package]] 2618 | name = "windows_x86_64_gnu" 2619 | version = "0.42.1" 2620 | source = "registry+https://github.com/rust-lang/crates.io-index" 2621 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 2622 | 2623 | [[package]] 2624 | name = "windows_x86_64_gnu" 2625 | version = "0.48.0" 2626 | source = "registry+https://github.com/rust-lang/crates.io-index" 2627 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 2628 | 2629 | [[package]] 2630 | name = "windows_x86_64_gnullvm" 2631 | version = "0.42.1" 2632 | source = "registry+https://github.com/rust-lang/crates.io-index" 2633 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 2634 | 2635 | [[package]] 2636 | name = "windows_x86_64_gnullvm" 2637 | version = "0.48.0" 2638 | source = "registry+https://github.com/rust-lang/crates.io-index" 2639 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 2640 | 2641 | [[package]] 2642 | name = "windows_x86_64_msvc" 2643 | version = "0.36.1" 2644 | source = "registry+https://github.com/rust-lang/crates.io-index" 2645 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 2646 | 2647 | [[package]] 2648 | name = "windows_x86_64_msvc" 2649 | version = "0.42.1" 2650 | source = "registry+https://github.com/rust-lang/crates.io-index" 2651 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 2652 | 2653 | [[package]] 2654 | name = "windows_x86_64_msvc" 2655 | version = "0.48.0" 2656 | source = "registry+https://github.com/rust-lang/crates.io-index" 2657 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 2658 | 2659 | [[package]] 2660 | name = "winreg" 2661 | version = "0.50.0" 2662 | source = "registry+https://github.com/rust-lang/crates.io-index" 2663 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 2664 | dependencies = [ 2665 | "cfg-if", 2666 | "windows-sys 0.48.0", 2667 | ] 2668 | 2669 | [[package]] 2670 | name = "yasna" 2671 | version = "0.4.0" 2672 | source = "registry+https://github.com/rust-lang/crates.io-index" 2673 | checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" 2674 | dependencies = [ 2675 | "bit-vec", 2676 | "num-bigint", 2677 | ] 2678 | 2679 | [[package]] 2680 | name = "zeroize" 2681 | version = "1.3.0" 2682 | source = "registry+https://github.com/rust-lang/crates.io-index" 2683 | checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" 2684 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "libthere", 4 | "there-cli", 5 | "there-agent", 6 | "there-controller" 7 | ] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # that goes there 2 | 3 | *that goes there* (hereafter "there" or `there`) is: 4 | 5 | - A library for general planning and execution of tasks on local and remote 6 | hosts. 7 | - [crates.io](https://crates.io/crates/there) 8 | - Tasks are compiled down to `sh(1)`-compatible commands for execution. 9 | - Non-raw-command tasks attempt to validate prerequists before execution. 10 | - A CLI for applying plans via: 11 | - Local execution 12 | - Remote SSH 13 | - Agent-controller SSH 14 | - An agent-controller pair for remote SSH execution. 15 | 16 | Minimum supported Rust version: `1.66` 17 | 18 | ## features 19 | 20 | ### Library: 21 | 22 | - Tokio-based, fully-async 23 | - Pluggable execution backends 24 | - [x] Local 25 | - [x] SSH 26 | - [ ] Implement `Executor` yourself! 27 | - Async log streaming 28 | - Pluggable log streaming backends 29 | - [x] In-memory 30 | - [ ] Implement `LogSource`/`LogSink` yourself! 31 | - Agent-controller shared IPC structs 32 | - Plan validation 33 | - Compile tasks down to `sh(1)`-compatible commands 34 | - Host files + host groups 35 | - Fully instrumented with `tracing::instrument` 36 | - `tracing`-based logging that can be consumed with `tracing-subscriber` 37 | - `color_eyre`-based error reporting 38 | - Used to share error implementation between CLI/agent/controller 39 | 40 | ### CLI: 41 | 42 | - Log streaming by default 43 | - [x] Local execution 44 | - [x] SSH execution 45 | - [x] Agent-controller execution 46 | - Plan validation 47 | 48 | ## setup 49 | 50 | Install [pre-commit](https://pre-commit.com/). 51 | 52 | ```bash 53 | pre-commit install 54 | pre-commit autoupdate 55 | cargo install cargo-audit 56 | ``` 57 | 58 | ## usage 59 | 60 | kinda undocumented for now. refer to `test/*.yaml` for some plan/hostfile 61 | examples. 62 | 63 | ## todo 64 | 65 | - [ ] Pretty tui with [makeup](https://crates.io/crates/makeup) for log streaming 66 | - [ ] Concurrency guards to ensure that certain tasks have to run across all 67 | hosts at the same time. 68 | 69 | ## show me something pretty 70 | 71 | ``` 72 | git:(mistress) | ▶ cargo run -p there-cli -- plan apply --dry -f ./test/ssh-plan.yaml --hosts ./test/ssh-hosts.yaml # ... 73 | *** plan: test plan *** 74 | 75 | * metadata 76 | ** hosts: 77 | *** group: ssh-group 78 | **** broken-ssh: localhost:2222 (ssh) 79 | **** ssh-localhost: localhost:22 (ssh) 80 | ** test command: echo hello world! 81 | *** ExeExists { exe: "echo" } 82 | ** test command 2: echo hello world!! :D 83 | *** ExeExists { exe: "echo" } 84 | ** test command 2: echo wow!!!!! 85 | *** ExeExists { exe: "echo" } 86 | ** create some file: touch /tmp/some-file 87 | *** ExeExists { exe: "touch" } 88 | *** DirectoryExists { path: "/tmp" } 89 | git:(mistress) 1 | ▶ cargo run -p there-cli -- plan apply -f ./test/ssh-plan.yaml --hosts ./test/ssh-hosts.yaml # ... 90 | *** applying plan to group: ssh-group *** 91 | *** prepared plan for host: broken-ssh 92 | *** prepared plan for host: ssh-localhost 93 | broken-ssh: * steps: 4 94 | ssh-localhost: * steps: 4 95 | broken-ssh: ssh authentication failed! 96 | *** failed plan: test plan for host: broken-ssh: 0/4 *** 97 | *** error: ssh executor failed to apply plan test plan to host broken-ssh: 0/4 tasks finished: ssh authentication failed! 98 | ssh-localhost: ** executing task: test command 99 | ssh-localhost: ensuring ExeExists { exe: "echo" } 100 | ssh-localhost: /bin/echo 101 | ssh-localhost: 102 | ssh-localhost: hello world! 103 | ssh-localhost: 104 | ssh-localhost: 105 | ssh-localhost: ** executing task: test command 2 106 | ssh-localhost: ensuring ExeExists { exe: "echo" } 107 | ssh-localhost: /bin/echo 108 | ssh-localhost: 109 | ssh-localhost: hello world!! :D 110 | ssh-localhost: 111 | ssh-localhost: 112 | ssh-localhost: ** executing task: test command 2 113 | ssh-localhost: ensuring ExeExists { exe: "echo" } 114 | ssh-localhost: /bin/echo 115 | ssh-localhost: 116 | ssh-localhost: wow!!!!! 117 | ssh-localhost: 118 | ssh-localhost: 119 | ssh-localhost: ** executing task: create some file 120 | ssh-localhost: ensuring ExeExists { exe: "touch" } 121 | ssh-localhost: /bin/touch 122 | ssh-localhost: 123 | ssh-localhost: ensuring DirectoryExists { path: "/tmp" } 124 | ssh-localhost: 125 | ssh-localhost: *** finished applying plan: test plan -> ssh-localhost (4/4) 126 | *** completed plan: test plan for host: ssh-localhost: 4/4 *** 127 | git:(mistress) 1 | ▶ 128 | ``` 129 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # ssh -q -C -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null root@localhost -p 2222 3 | version: '3.3' 4 | services: 5 | ssh: 6 | ports: 7 | - '2222:22' 8 | # volumes: 9 | # - './env/authorized_keys:/root/.ssh/authorized_keys:ro' 10 | # environment: 11 | # - SSH_ENABLE_ROOT=true 12 | image: 'docker.io/panubo/sshd:1.4.0' 13 | -------------------------------------------------------------------------------- /libthere/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /libthere/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "there" 3 | version = "0.1.2" 4 | edition = "2021" 5 | repository = "https://github.com/queer/that-goes-there" 6 | description = "A library for planning and executing commands on local and remote hosts." 7 | license = "MIT" 8 | readme = "../README.md" 9 | keywords = ["ssh", "remote", "command", "execution", "tasks"] 10 | categories = ["asynchronous", "concurrency", "network-programming"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | async-trait = "0.1.74" 16 | color-eyre = { version = "0.6.2", features = ["issue-url"] } 17 | derive-getters = "0.3.0" 18 | futures = "0.3.28" 19 | serde = { version = "1.0.193", features = ["derive"] } 20 | shell-words = "1.1.0" 21 | thrussh = "0.34.0" 22 | thrussh-keys = "0.22.1" 23 | tokio = { version = "1.35.0", features = ["full"] } 24 | tokio-stream = "0.1.14" 25 | tokio-util = { version = "0.7.10", features = ["codec"] } 26 | tracing = { version = "0.1.38", features = ["async-await"] } 27 | which = "5.0.0" 28 | -------------------------------------------------------------------------------- /libthere/src/executor/mod.rs: -------------------------------------------------------------------------------- 1 | //! Executors for running tasks on local or remote hosts. 2 | //! 3 | //! Note that [`LogSink`] and [`LogSource`] **do** return `Result`s, but 4 | //! callers are expected to handle errors themselves. Failure to handle such 5 | //! errors will cause unexpected execution failures; functions calling these 6 | //! should be written defensively so as to handle sink/source errors 7 | //! appropriately. 8 | 9 | use async_trait::async_trait; 10 | use color_eyre::eyre::Result; 11 | 12 | use tokio::sync::Mutex; 13 | 14 | use crate::plan::host::Host; 15 | use crate::plan::Plan; 16 | 17 | pub mod simple; 18 | pub mod ssh; 19 | 20 | /// A set of logs. 21 | pub type Logs = Vec; 22 | 23 | /// A partial log stream. Used for controlling how logs are streamed through 24 | /// the [`Executor`] -> [`LogSink`] -> [`LogSource`] pipeline. 25 | #[derive(Debug, PartialEq, Eq, Clone)] 26 | pub enum PartialLogStream { 27 | /// The next logs to emit. 28 | Next(Logs), 29 | /// The end of the stream. Must cause the [`LogSink`] and [`LogSource`] to 30 | /// close. 31 | End, 32 | } 33 | 34 | /// An `Executor` is responsible for executing a given [`Plan`] on its target 35 | /// hosts. It is possible, but not necessarily required, that the `Plan` passed 36 | /// into the [`ExecutionContext`] contains more than one [`Host`]. 37 | #[async_trait] 38 | pub trait Executor<'a, T: ExecutionContext + std::fmt::Debug = simple::SimpleExecutionContext<'a>>: 39 | std::fmt::Debug + Send + Sync 40 | { 41 | /// Execute the [`Plan`] in the context on the hosts that this executor 42 | /// knows about. This may be a single host, or multiple hosts; the way that 43 | /// hosts are assigned to executors is entirely up to the caller. 44 | async fn execute(&mut self, ctx: Mutex<&'a mut T>) -> Result<()>; 45 | 46 | /// Get the number of tasks completed by this executor. 47 | fn tasks_completed(&self) -> Result; 48 | } 49 | 50 | /// The context for a given [`Executor`]'s execution of its plan. Contains the 51 | /// plan being executed by the `Executor`. 52 | #[async_trait] 53 | pub trait ExecutionContext: std::fmt::Debug { 54 | /// The name of this execution. Usually the name of the [`Plan`]. 55 | fn name(&self) -> &str; 56 | 57 | /// The plan being executed. 58 | fn plan(&self) -> &Plan; 59 | } 60 | 61 | /// A sink for logs from an [`Executor`]. The `Executor` will push logs into 62 | /// its `LogSink`, and the code calling the `Executor` is responsible for 63 | /// pulling those logs out of the [`LogSource`] on the other end. 64 | #[async_trait] 65 | pub trait LogSink: std::fmt::Debug { 66 | /// Sink a [`PartialLogStream`] into this sink. Returns the number of logs 67 | /// successfully sunk. 68 | async fn sink(&mut self, logs: PartialLogStream) -> Result; 69 | 70 | /// Sink a single [`String`] into the log. Returns the number of logs 71 | /// successfully sunk (probably just `1`). 72 | #[tracing::instrument(skip(self))] 73 | async fn sink_one + Send + std::fmt::Debug>( 74 | &mut self, 75 | log: S, 76 | ) -> Result { 77 | self.sink(PartialLogStream::Next(vec![log.into()])).await 78 | } 79 | } 80 | 81 | /// A source for logs from an [`Executor`]. This end of the logging pipeline is 82 | /// responsible for pulling logs out of the [`LogSink`] on the other end. 83 | #[async_trait] 84 | pub trait LogSource: std::fmt::Debug { 85 | /// Read the next [`PartialLogStream`] from the logs streaming into this 86 | /// source from the [`LogSink`] on the other end. 87 | async fn source(&mut self) -> Result; 88 | } 89 | -------------------------------------------------------------------------------- /libthere/src/executor/simple.rs: -------------------------------------------------------------------------------- 1 | //! Local command executor. 2 | 3 | use std::cell::RefCell; 4 | use std::sync::Arc; 5 | use std::time::Duration; 6 | use std::{future::Future, marker::PhantomData}; 7 | 8 | use async_trait::async_trait; 9 | use color_eyre::eyre::Result; 10 | use derive_getters::Getters; 11 | use tokio::fs; 12 | use tokio::sync::{mpsc, Mutex}; 13 | 14 | use super::{ExecutionContext, Executor, LogSink, LogSource, Logs, PartialLogStream}; 15 | use crate::log::*; 16 | use crate::plan::{Ensure, Plan, PlannedTask, Task}; 17 | 18 | /// An mpsc channel for in-memory logging. 19 | pub type SimpleLogTx = mpsc::Sender; 20 | /// An mpsc channel for in-memory logging. 21 | pub type SimpleLogRx = mpsc::Receiver; 22 | 23 | /// A simple [`Executor`] implementation that executes [`Task`]s on 24 | /// `localhost`. 25 | #[derive(Getters, Debug, Clone)] 26 | pub struct SimpleExecutor<'a> { 27 | #[getter(skip)] 28 | log_sink: Arc>>, 29 | tasks_completed: u32, 30 | } 31 | 32 | impl<'a> SimpleExecutor<'a> { 33 | /// Create a new [`SimpleExecutor`]. 34 | pub fn new(tx: &'a SimpleLogTx) -> Self { 35 | Self { 36 | log_sink: Arc::new(Mutex::new(SimpleLogSink::new(tx))), 37 | tasks_completed: 0, 38 | } 39 | } 40 | 41 | #[tracing::instrument(skip(self))] 42 | async fn sink_one + std::fmt::Debug>(&mut self, msg: S) -> Result { 43 | self.log_sink 44 | .lock() 45 | .await 46 | .sink_one(msg.into().clone()) 47 | .await 48 | } 49 | 50 | #[tracing::instrument(skip(self))] 51 | async fn ensure_task(&mut self, task: &'a PlannedTask) -> Result> { 52 | let mut failures = vec![]; 53 | for ensure in task.ensures() { 54 | self.sink_one(format!("ensuring {:?}", ensure)).await?; 55 | let pass = match ensure { 56 | Ensure::DirectoryDoesntExist { path } => fs::metadata(path).await.is_err(), 57 | Ensure::DirectoryExists { path } => { 58 | fs::metadata(path).await.is_ok() && fs::metadata(path).await?.is_dir() 59 | } 60 | Ensure::FileDoesntExist { path } => fs::metadata(path).await.is_err(), 61 | Ensure::FileExists { path } => { 62 | fs::metadata(path).await.is_ok() && fs::metadata(path).await?.is_file() 63 | } 64 | Ensure::ExeExists { exe } => which::which(exe).is_ok(), 65 | }; 66 | if !pass { 67 | failures.push(ensure); 68 | } 69 | } 70 | Ok(failures) 71 | } 72 | 73 | #[tracing::instrument(skip(self))] 74 | async fn execute_task( 75 | &mut self, 76 | task: &'a PlannedTask, 77 | ctx: &mut SimpleExecutionContext<'a>, 78 | ) -> Result<()> { 79 | use std::ops::Deref; 80 | use std::process::Stdio; 81 | 82 | use tokio::io::{AsyncRead, AsyncReadExt}; 83 | use tokio::process::Command; 84 | use tokio_stream::StreamExt; 85 | use tokio_util::codec::{BytesCodec, FramedRead}; 86 | 87 | { 88 | let mut locked_sink = self.log_sink.lock().await; 89 | locked_sink.sink_one(format!("** executing task: {}", task.name())); 90 | // drop the lock to release and avoid deadlock 91 | drop(locked_sink); 92 | } 93 | let failed_ensures = self.ensure_task(task).await?; 94 | if !failed_ensures.is_empty() { 95 | self.sink_one(format!("ensures failed: {:?}", failed_ensures)) 96 | .await?; 97 | return Err(eyre!("ensures failed: {:?}", failed_ensures)); 98 | } 99 | info!("executing task: {}", task.name()); 100 | let cmd = &task.command()[0]; 101 | let args = task.command()[1..].to_vec(); 102 | 103 | let mut builder = Command::new(cmd); 104 | let mut builder = builder 105 | .stdout(Stdio::piped()) 106 | .stderr(Stdio::piped()) 107 | .args(args); 108 | // TODO: env etc 109 | 110 | let mut child = builder.spawn()?; 111 | 112 | let mut stdout = FramedRead::new(child.stdout.take().unwrap(), BytesCodec::new()); 113 | let mut stderr = FramedRead::new(child.stderr.take().unwrap(), BytesCodec::new()); 114 | let sink_clone = self.log_sink.clone(); 115 | 116 | while let Ok(None) = child.try_wait() { 117 | let mut sink = sink_clone.lock().await; 118 | tokio::select! { 119 | Some(next) = stdout.next() => { 120 | if let Ok(logs) = next { 121 | let logs = vec![String::from_utf8(logs.to_vec()).unwrap_or_else(|d| format!("got: {:#?}", d))]; 122 | match sink.sink(PartialLogStream::Next(logs)).await { 123 | Ok(_) => {} 124 | Err(err) => { 125 | error!("error sinking logs: {}", err); 126 | } 127 | } 128 | } 129 | } 130 | Some(next) = stderr.next() => { 131 | if let Ok(logs) = next { 132 | let logs = vec![String::from_utf8(logs.to_vec()).unwrap_or_else(|d| format!("got: {:#?}", d))]; 133 | match sink.sink(PartialLogStream::Next(logs)).await { 134 | Ok(_) => {} 135 | Err(err) => { 136 | error!("error sinking logs: {}", err); 137 | } 138 | } 139 | } 140 | } 141 | else => { 142 | break; 143 | } 144 | } 145 | } 146 | 147 | info!("task '{}' finished", task.name()); 148 | let mut sink = self.log_sink.lock().await; 149 | sink.sink(PartialLogStream::Next(vec![String::new()])) 150 | .await?; 151 | self.tasks_completed += 1; 152 | 153 | Ok(()) 154 | } 155 | } 156 | 157 | #[async_trait] 158 | impl<'a> Executor<'a, SimpleExecutionContext<'a>> for SimpleExecutor<'a> { 159 | #[tracing::instrument(skip(self))] 160 | async fn execute(&mut self, ctx: Mutex<&'a mut SimpleExecutionContext>) -> Result<()> { 161 | let mut ctx = ctx.lock().await; 162 | let clone = ctx.clone(); 163 | self.sink_one(format!("* applying plan: {}", ctx.plan().name())) 164 | .await?; 165 | self.sink_one(format!("* steps: {}", ctx.plan().blueprint().len())) 166 | .await?; 167 | info!("applying plan: {}", ctx.plan().name()); 168 | for task in ctx.plan.blueprint().iter() { 169 | debug!("simple executor: executing task: {}", task.name()); 170 | self.execute_task(task, &mut clone.clone()).await?; 171 | } 172 | info!("plan applied: {}", ctx.plan().name()); 173 | self.sink_one(format!( 174 | "*** finished applying plan: {} ({}/{})", 175 | ctx.plan().name(), 176 | self.tasks_completed(), 177 | ctx.plan().blueprint().len() 178 | )) 179 | .await?; 180 | self.log_sink 181 | .lock() 182 | .await 183 | .sink(PartialLogStream::End) 184 | .await?; 185 | Ok(()) 186 | } 187 | 188 | fn tasks_completed(&self) -> Result { 189 | Ok(self.tasks_completed) 190 | } 191 | } 192 | 193 | /// A basic [`ExecutionContext`] implementation that holds a reference to the 194 | /// current [`Plan`] and the name of the execution. 195 | #[derive(Getters, Debug, Clone)] 196 | pub struct SimpleExecutionContext<'a> { 197 | name: &'a str, 198 | plan: &'a Plan, 199 | } 200 | 201 | impl<'a> SimpleExecutionContext<'a> { 202 | /// Create a new [`SimpleExecutionContext`]. 203 | pub fn new(name: &'a str, plan: &'a Plan) -> Self { 204 | Self { name, plan } 205 | } 206 | } 207 | 208 | #[async_trait] 209 | impl<'a> ExecutionContext for SimpleExecutionContext<'a> { 210 | fn name(&self) -> &str { 211 | self.name 212 | } 213 | 214 | fn plan(&self) -> &Plan { 215 | self.plan 216 | } 217 | } 218 | 219 | /// A basic [`LogSink`] implementation that pushes logs into a 220 | /// [`tokio::sync::mpsc::Sender`]. 221 | #[derive(Getters, Debug, Clone)] 222 | pub struct SimpleLogSink<'a> { 223 | tx: &'a SimpleLogTx, 224 | } 225 | 226 | impl<'a> SimpleLogSink<'a> { 227 | /// Create a new [`SimpleLogSink`]. 228 | pub fn new(tx: &'a SimpleLogTx) -> Self { 229 | Self { tx } 230 | } 231 | } 232 | 233 | #[async_trait] 234 | impl<'a> LogSink for SimpleLogSink<'a> { 235 | #[tracing::instrument(skip(self))] 236 | async fn sink(&mut self, logs: PartialLogStream) -> Result { 237 | let out = match logs { 238 | PartialLogStream::Next(ref logs) => Ok(logs.len()), 239 | PartialLogStream::End => Ok(0), 240 | }; 241 | self.tx.send(logs).await?; 242 | out 243 | } 244 | } 245 | 246 | /// A basic [`LogSource`] implementation that pulls logs from a 247 | /// [`tokio::sync::mpsc::Receiver`]. 248 | #[derive(Debug)] 249 | pub struct SimpleLogSource { 250 | rx: SimpleLogRx, 251 | ended: bool, 252 | } 253 | 254 | impl SimpleLogSource { 255 | /// Create a new [`SimpleLogSource`]. 256 | pub fn new(rx: SimpleLogRx) -> Self { 257 | Self { rx, ended: false } 258 | } 259 | } 260 | 261 | #[async_trait] 262 | impl LogSource for SimpleLogSource { 263 | #[tracing::instrument(skip(self))] 264 | async fn source(&mut self) -> Result { 265 | if self.ended { 266 | return Err(eyre!("log source already ended")); 267 | } 268 | let mut out = vec![]; 269 | match &self.rx.try_recv() { 270 | Ok(partial_stream) => match partial_stream { 271 | PartialLogStream::Next(logs) => { 272 | for log in logs { 273 | out.push(log.clone()); 274 | } 275 | } 276 | PartialLogStream::End => { 277 | self.ended = true; 278 | } 279 | }, 280 | Err(mpsc::error::TryRecvError::Empty) => {} 281 | Err(mpsc::error::TryRecvError::Disconnected) => { 282 | return Err(eyre!("sink lost")); 283 | } 284 | } 285 | if self.ended { 286 | Ok(PartialLogStream::End) 287 | } else { 288 | Ok(PartialLogStream::Next(out)) 289 | } 290 | } 291 | } 292 | 293 | #[cfg(test)] 294 | mod test { 295 | use crate::executor::simple::*; 296 | use crate::executor::*; 297 | use crate::plan::host::HostConfig; 298 | use crate::plan::*; 299 | 300 | use tokio::sync::mpsc; 301 | 302 | #[tokio::test] 303 | async fn test_simple_executor() -> Result<()> { 304 | let (mut tx, mut rx) = mpsc::channel(69); 305 | let mut log_source = SimpleLogSource::new(rx); 306 | 307 | let mut taskset = TaskSet::new("test"); 308 | taskset.add_task(Task::Command { 309 | name: "test".into(), 310 | command: "echo 'hello'".into(), 311 | hosts: vec![], 312 | }); 313 | let mut plan = taskset.plan().await?; 314 | let hosts = HostConfig::default(); 315 | let mut ctx = SimpleExecutionContext::new("test", &plan); 316 | let mut executor = SimpleExecutor::new(&tx); 317 | executor.execute(Mutex::new(&mut ctx)).await?; 318 | assert_eq!( 319 | PartialLogStream::Next(vec!["* applying plan: test".into()]), 320 | log_source.source().await? 321 | ); 322 | assert_eq!( 323 | PartialLogStream::Next(vec!["* steps: 1".into()]), 324 | log_source.source().await? 325 | ); 326 | assert_eq!( 327 | PartialLogStream::Next(vec!["ensuring ExeExists { exe: \"echo\" }".into()]), 328 | log_source.source().await? 329 | ); 330 | assert_eq!( 331 | PartialLogStream::Next(vec!["hello\n".into()]), 332 | log_source.source().await? 333 | ); 334 | assert_eq!( 335 | PartialLogStream::Next(vec![String::new()]), 336 | log_source.source().await? 337 | ); 338 | assert_eq!( 339 | PartialLogStream::Next(vec!["*** finished applying plan: test (1/1)".into()]), 340 | log_source.source().await? 341 | ); 342 | assert_eq!(PartialLogStream::End, log_source.source().await?); 343 | Ok(()) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /libthere/src/executor/ssh.rs: -------------------------------------------------------------------------------- 1 | //! SSH command executor for remote hosts. 2 | 3 | use std::sync::Arc; 4 | 5 | use crate::executor::simple::{SimpleLogSink, SimpleLogTx}; 6 | use crate::executor::{ExecutionContext, Executor, LogSink, PartialLogStream}; 7 | use crate::log::*; 8 | use crate::plan; 9 | use crate::plan::host::Host; 10 | use async_trait::async_trait; 11 | use color_eyre::eyre::Result; 12 | use derive_getters::Getters; 13 | use tokio::sync::Mutex; 14 | 15 | /// An SSH command executor for remote hosts. This executor will execute tasks 16 | /// on the remote host by connecting to it via SSH, and executing the commands 17 | /// in the [`Plan`] by compiling them to `sh(1)`-compatible commands. 18 | #[derive(Getters, Debug)] 19 | pub struct SshExecutor<'a> { 20 | #[getter(skip)] 21 | log_sink: Arc>>, 22 | keypair: Arc, 23 | host: &'a Host, 24 | hostname: &'a str, 25 | tasks_completed: u32, 26 | } 27 | 28 | impl<'a> SshExecutor<'a> { 29 | /// Create a new SSH executor for the given host. SSH key is required, SSH 30 | /// key passphrase is optional. 31 | #[tracing::instrument(skip(ssh_key, ssh_key_passphrase))] 32 | pub fn new( 33 | host: &'a Host, 34 | hostname: &'a str, 35 | tx: &'a SimpleLogTx, 36 | ssh_key: &'a str, 37 | ssh_key_passphrase: Option, 38 | ) -> Result { 39 | let keypair = match ssh_key_passphrase { 40 | Some(passphrase) => thrussh_keys::decode_secret_key(ssh_key, Some(&passphrase)), 41 | None => thrussh_keys::decode_secret_key(ssh_key, None), 42 | }?; 43 | 44 | Ok(Self { 45 | log_sink: Arc::new(Mutex::new(SimpleLogSink::new(tx))), 46 | keypair: Arc::new(keypair), 47 | host, 48 | hostname, 49 | tasks_completed: 0, 50 | }) 51 | } 52 | 53 | /// Create a new SSH executor for the given host, using an existing keypair. 54 | #[tracing::instrument(skip(keypair))] 55 | pub fn new_with_existing_key( 56 | host: &'a Host, 57 | hostname: &'a str, 58 | tx: &'a SimpleLogTx, 59 | keypair: thrussh_keys::key::KeyPair, 60 | ) -> Result { 61 | Ok(Self { 62 | log_sink: Arc::new(Mutex::new(SimpleLogSink::new(tx))), 63 | keypair: Arc::new(keypair), 64 | host, 65 | hostname, 66 | tasks_completed: 0, 67 | }) 68 | } 69 | 70 | #[tracing::instrument(skip(self))] 71 | async fn sink_one + std::fmt::Debug>(&mut self, msg: S) -> Result { 72 | self.log_sink 73 | .lock() 74 | .await 75 | .sink_one(msg.into().clone()) 76 | .await 77 | } 78 | 79 | #[tracing::instrument(skip(self))] 80 | async fn sink_partial(&mut self, partial: PartialLogStream) -> Result { 81 | self.log_sink.lock().await.sink(partial).await 82 | } 83 | 84 | #[tracing::instrument(skip(self, session))] 85 | async fn ensure_task( 86 | &mut self, 87 | task: &'a plan::PlannedTask, 88 | session: &mut thrussh::client::Handle, 89 | ) -> Result> { 90 | let mut failures = vec![]; 91 | for ensure in task.ensures() { 92 | self.sink_one(format!("* ensuring {:?}", ensure)).await?; 93 | let pass = match ensure { 94 | plan::Ensure::DirectoryDoesntExist { path } => { 95 | self.execute_command(format!("test -d \"{}\"", path), session) 96 | .await? 97 | != 0 98 | } 99 | plan::Ensure::DirectoryExists { path } => { 100 | self.execute_command(format!("test -d \"{}\"", path), session) 101 | .await? 102 | == 0 103 | } 104 | plan::Ensure::FileDoesntExist { path } => { 105 | self.execute_command(format!("test -f \"{}\"", path), session) 106 | .await? 107 | != 0 108 | } 109 | plan::Ensure::FileExists { path } => { 110 | self.execute_command(format!("test -f \"{}\"", path), session) 111 | .await? 112 | == 0 113 | } 114 | plan::Ensure::ExeExists { exe } => { 115 | self.execute_command(format!("which \"{}\"", exe), session) 116 | .await? 117 | == 0 118 | } 119 | }; 120 | if !pass { 121 | failures.push(ensure); 122 | } 123 | } 124 | Ok(failures) 125 | } 126 | 127 | #[tracing::instrument(skip(self, session))] 128 | async fn execute_command>( 129 | &mut self, 130 | command: S, 131 | session: &mut thrussh::client::Handle, 132 | ) -> Result { 133 | // TODO: Figure out env etc... 134 | // NOTE: It SEEMS that you can only push one command down each channel 135 | // before the server closes it. This should be verified at some point. 136 | // To handle this, we hold open the session for as long as possible, 137 | // and open new channels for each command. 138 | let mut channel = session.channel_open_session().await?; 139 | channel.exec(true, command.clone().into()).await?; 140 | 141 | while let Some(frame) = channel.wait().await { 142 | let mut sink = self.log_sink.lock().await; 143 | match frame { 144 | thrussh::ChannelMsg::Data { data } => { 145 | sink.sink(PartialLogStream::Next( 146 | String::from_utf8(data[..].to_vec())? 147 | .split('\n') 148 | .map(|s| s.to_string()) 149 | .collect(), 150 | )) 151 | .await?; 152 | } 153 | thrussh::ChannelMsg::ExtendedData { data, ext: _ } => { 154 | sink.sink(PartialLogStream::Next( 155 | String::from_utf8(data[..].to_vec())? 156 | .split('\n') 157 | .map(|s| s.to_string()) 158 | .collect(), 159 | )) 160 | .await?; 161 | } 162 | thrussh::ChannelMsg::Eof => {} // TODO: ??? 163 | thrussh::ChannelMsg::Close => { 164 | break; 165 | } 166 | thrussh::ChannelMsg::XonXoff { client_can_do: _ } => {} // TODO 167 | thrussh::ChannelMsg::ExitStatus { exit_status } => { 168 | if exit_status == 0 { 169 | return Ok(0); 170 | } else { 171 | return Ok(exit_status); 172 | } 173 | } 174 | thrussh::ChannelMsg::ExitSignal { 175 | // TODO 176 | signal_name: _, 177 | core_dumped: _, 178 | error_message: _, 179 | lang_tag: _, 180 | } => {} 181 | thrussh::ChannelMsg::WindowAdjusted { new_size: _ } => {} 182 | thrussh::ChannelMsg::Success => {} 183 | } 184 | } 185 | Ok(511) 186 | } 187 | 188 | #[tracing::instrument(skip(self, session))] 189 | async fn execute_task( 190 | &mut self, 191 | task: &'a plan::PlannedTask, 192 | ctx: &mut SshExecutionContext<'a>, 193 | session: &mut thrussh::client::Handle, 194 | ) -> Result<()> { 195 | self.sink_one(format!("** executing task: {}", task.name())) 196 | .await?; 197 | info!("executing task: {}", task.name()); 198 | let failed_ensures = self.ensure_task(task, session).await?; 199 | if !failed_ensures.is_empty() { 200 | self.sink_one(format!("ensures failed: {:?}", failed_ensures)) 201 | .await?; 202 | return Err(eyre!("ensures failed: {:?}", failed_ensures)); 203 | } 204 | self.sink_one(format!("* executing task: {}", task.name())) 205 | .await?; 206 | self.execute_command(task.command().join(" "), session) 207 | .await?; 208 | info!("task '{}' finished", task.name()); 209 | self.sink_one(String::new()).await?; 210 | 211 | Ok(()) 212 | } 213 | } 214 | 215 | #[async_trait] 216 | impl<'a> Executor<'a, SshExecutionContext<'a>> for SshExecutor<'a> { 217 | #[tracing::instrument(skip(self, ctx))] 218 | async fn execute(&mut self, ctx: Mutex<&'a mut SshExecutionContext>) -> Result<()> { 219 | debug!("awaiting ctx lock..."); 220 | let ctx = ctx.lock().await; 221 | debug!("got it!"); 222 | self.sink_one(format!("* steps: {}", ctx.plan().blueprint().len())) 223 | .await?; 224 | 225 | // Attempt to get a working SSH client first; don't waste time. 226 | let sh = SshClient; 227 | let config = thrussh::client::Config { 228 | connection_timeout: Some(std::time::Duration::from_secs(5)), 229 | ..Default::default() 230 | }; 231 | let config = Arc::new(config); 232 | let addr = format!("{}:{}", self.host.host(), self.host.port()); 233 | debug!("connecting to {}", &addr); 234 | let mut session = thrussh::client::connect(config, addr, sh).await?; 235 | let auth_res = session 236 | .authenticate_publickey(self.host.real_remote_user(), self.keypair.clone()) 237 | .await; 238 | if auth_res? { 239 | debug!("successfully authenticated!"); 240 | 241 | // Actually apply the plan. 242 | let clone = ctx.clone(); 243 | info!("applying plan: {}", ctx.plan().name()); 244 | for task in ctx.plan.blueprint().iter() { 245 | debug!("ssh executor: executing task: {}", task.name()); 246 | self.execute_task(task, &mut clone.clone(), &mut session) 247 | .await?; 248 | self.tasks_completed += 1; 249 | } 250 | info!("plan applied: {}", ctx.plan().name()); 251 | self.sink_one(format!( 252 | "*** finished applying plan: {} -> {} ({}/{})", 253 | ctx.plan().name(), 254 | &self.hostname, 255 | self.tasks_completed, 256 | ctx.plan().blueprint().len(), 257 | )) 258 | .await?; 259 | self.sink_partial(PartialLogStream::End).await?; 260 | Ok(()) 261 | } else { 262 | self.sink_one("ssh authentication failed!".to_string()) 263 | .await?; 264 | self.sink_partial(PartialLogStream::End).await?; 265 | // TODO: Does this error need to be repeated here *and* in the stream? 266 | return Err(eyre!("ssh authentication failed!")); 267 | } 268 | } 269 | 270 | fn tasks_completed(&self) -> Result { 271 | Ok(self.tasks_completed) 272 | } 273 | } 274 | 275 | /// An execution context for SSH. 276 | #[derive(Getters, Debug, Clone)] 277 | pub struct SshExecutionContext<'a> { 278 | name: &'a str, 279 | plan: &'a plan::Plan, 280 | } 281 | 282 | impl<'a> SshExecutionContext<'a> { 283 | /// Create a new SSH execution context. 284 | pub fn new(name: &'a str, plan: &'a plan::Plan) -> Self { 285 | Self { name, plan } 286 | } 287 | } 288 | 289 | #[async_trait] 290 | impl<'a> ExecutionContext for SshExecutionContext<'a> { 291 | fn name(&self) -> &str { 292 | self.name 293 | } 294 | 295 | fn plan(&self) -> &plan::Plan { 296 | self.plan 297 | } 298 | } 299 | 300 | struct SshClient; 301 | 302 | impl thrussh::client::Handler for SshClient { 303 | type Error = color_eyre::eyre::Report; 304 | type FutureUnit = 305 | futures::future::Ready>; 306 | type FutureBool = futures::future::Ready>; 307 | 308 | #[tracing::instrument(skip(self))] 309 | fn finished_bool(self, b: bool) -> Self::FutureBool { 310 | futures::future::ready(Ok((self, b))) 311 | } 312 | 313 | #[tracing::instrument(skip(self, session))] 314 | fn finished(self, session: thrussh::client::Session) -> Self::FutureUnit { 315 | futures::future::ready(Ok((self, session))) 316 | } 317 | 318 | #[tracing::instrument(skip(self))] 319 | fn check_server_key( 320 | self, 321 | _server_public_key: &thrussh_keys::key::PublicKey, 322 | ) -> Self::FutureBool { 323 | self.finished_bool(true) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /libthere/src/ipc/http.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::plan::host::HostConfig; 6 | use crate::plan::Plan; 7 | 8 | #[doc(hidden)] 9 | #[derive(Serialize, Deserialize, Debug, Clone)] 10 | pub struct JobStartRequest { 11 | pub plan: Plan, 12 | pub hosts: HostConfig, 13 | } 14 | 15 | #[derive(Clone, Debug, Serialize, Deserialize)] 16 | pub struct JobState { 17 | pub logs: HashMap>, 18 | pub plan: Plan, 19 | pub hosts: HostConfig, 20 | pub status: JobStatus, 21 | } 22 | 23 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash)] 24 | pub enum JobStatus { 25 | Running, 26 | Completed, 27 | Failed, 28 | } 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize)] 31 | pub struct LogEntry { 32 | pub hostname: String, 33 | pub log: String, 34 | } 35 | -------------------------------------------------------------------------------- /libthere/src/ipc/mod.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | pub mod http; 3 | -------------------------------------------------------------------------------- /libthere/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | #![forbid(unsafe_code)] 3 | 4 | //! # there 5 | //! 6 | //! The shared code for that-goes-there. Encapsulates abstractions for things 7 | //! like: 8 | //! 9 | //! - Command execution and log aggregation 10 | //! - Logging and tracing 11 | //! - Command planning and validations for targets 12 | //! - Compile various pieces of functionality down to sh scripts as much as 13 | //! possible, such as ensuring files/directories do/not exist. 14 | 15 | pub mod executor; 16 | #[doc(hidden)] 17 | pub mod ipc; 18 | pub mod log; 19 | pub mod plan; 20 | -------------------------------------------------------------------------------- /libthere/src/log/mod.rs: -------------------------------------------------------------------------------- 1 | //! Simple re-export of logging-related macros. 2 | pub use color_eyre::eyre::eyre; 3 | pub use tracing::{debug, error, info, span, trace, warn}; 4 | 5 | /// Install color_eyre as the global error handler. 6 | #[tracing::instrument] 7 | pub fn install_color_eyre() -> color_eyre::eyre::Result<()> { 8 | color_eyre::config::HookBuilder::default() 9 | .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) 10 | .add_default_filters() 11 | .add_frame_filter(Box::new(|frames| { 12 | let filters = &["tokio::", "tracing::", "color_eyre::", " Port { 14 | 22 15 | } 16 | 17 | /// A set of hosts and groups that can be used to execute a [`Plan`]. 18 | #[derive(Getters, Debug, Clone, Serialize, Deserialize, Default)] 19 | pub struct HostConfig { 20 | hosts: HashMap, 21 | groups: HashMap>, 22 | } 23 | 24 | /// A single host in a [`HostConfig`]. 25 | #[derive(Getters, Debug, Clone, Serialize, Deserialize)] 26 | pub struct Host { 27 | host: String, 28 | #[serde(default = "self::default_ssh_port")] 29 | port: Port, 30 | executor: String, 31 | remote_user: Option, 32 | } 33 | 34 | impl Host { 35 | /// Get the real remote user for this host. If the remote user is not set, 36 | /// return `root`. 37 | pub fn real_remote_user(&self) -> String { 38 | #[allow(clippy::or_fun_call)] 39 | self.remote_user.clone().unwrap_or("root".to_string()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libthere/src/plan/mod.rs: -------------------------------------------------------------------------------- 1 | //! Create and validate plans for future execution. 2 | 3 | use std::marker::PhantomData; 4 | use std::sync::Arc; 5 | 6 | use color_eyre::eyre::Result; 7 | use derive_getters::Getters; 8 | use serde::{Deserialize, Serialize}; 9 | use tokio::sync::Mutex; 10 | 11 | use crate::log::*; 12 | 13 | pub mod host; 14 | pub mod visitor; 15 | 16 | pub use visitor::TaskVisitor; 17 | 18 | use self::host::{Host, HostConfig}; 19 | 20 | /// An unplanned set of [`Task`]s to be executed. [`TaskSet`]s have no 21 | /// validations applied to them outside of ensuring that they parse into 22 | /// `Task`s. Validations necessary for applying a `Task` are generated during 23 | /// the planning phase. 24 | #[derive(Getters, Debug, Clone, Serialize, Deserialize)] 25 | pub struct TaskSet { 26 | name: String, 27 | tasks: Vec, 28 | } 29 | 30 | impl TaskSet { 31 | /// Create a new `TaskSet` with the given name. 32 | pub fn new>(name: S) -> Self { 33 | Self { 34 | name: name.into(), 35 | tasks: vec![], 36 | } 37 | } 38 | 39 | /// Add a task to this `TaskSet`. 40 | #[tracing::instrument] 41 | pub fn add_task(&mut self, task: Task) { 42 | debug!("task set: added task to plan: {}", &task.name()); 43 | self.tasks.push(task); 44 | } 45 | 46 | /// Generate a [`Plan`] from this `TaskSet`. Consumes this `TaskSet`. 47 | #[tracing::instrument] 48 | pub async fn plan(mut self) -> Result { 49 | debug!("task set: planning tasks"); 50 | let name = self.name.clone(); 51 | let mut visitor = visitor::PlanningTaskVisitor::new(&self.name); 52 | for task in self.tasks.iter_mut() { 53 | task.accept(&mut visitor).await?; 54 | } 55 | debug!("task set: finished planning tasks"); 56 | Ok(Plan::new(name, visitor.plan().clone())) 57 | } 58 | } 59 | 60 | /// A `Task` is a potential command that can be executed. `Task`s are compiled 61 | /// into `sh(1)`-compatible commands prior to execution on a remote host. 62 | #[derive(Debug, Clone, Serialize, Deserialize)] 63 | #[serde(tag = "type")] 64 | pub enum Task { 65 | /// A command to be executed. 66 | Command { 67 | name: String, 68 | command: String, 69 | hosts: Vec, 70 | }, 71 | /// A directory to be created. 72 | CreateDirectory { 73 | name: String, 74 | path: String, 75 | hosts: Vec, 76 | }, 77 | /// A file to be created. 78 | TouchFile { 79 | name: String, 80 | path: String, 81 | hosts: Vec, 82 | }, 83 | } 84 | 85 | impl Task { 86 | /// Accept a [`TaskVisitor`] to visit this `Task`. 87 | #[tracing::instrument] 88 | pub async fn accept(&mut self, visitor: &mut dyn visitor::TaskVisitor) -> Result<()> { 89 | visitor.visit_task(self) 90 | } 91 | 92 | /// Extract the name of this task from the `Task` enum. 93 | pub fn name(&self) -> &str { 94 | match self { 95 | Task::Command { name, .. } => name, 96 | Task::CreateDirectory { name, .. } => name, 97 | Task::TouchFile { name, .. } => name, 98 | _ => "", 99 | } 100 | } 101 | 102 | /// Extract the hosts that this task applies to from the `Task` enum. 103 | pub fn hosts(&self) -> Vec { 104 | match self { 105 | Task::Command { hosts, .. } => hosts.clone(), 106 | Task::CreateDirectory { hosts, .. } => hosts.clone(), 107 | Task::TouchFile { hosts, .. } => hosts.clone(), 108 | _ => vec![], 109 | } 110 | } 111 | } 112 | 113 | /// A planned [`TaskSet`]. A `Plan` is a set of [`PlannedTask`]s that have had 114 | /// their [`Ensure`]s generated for execution on the remote hosts. 115 | #[derive(Getters, Debug, Clone, Deserialize, Serialize)] 116 | pub struct Plan { 117 | name: String, 118 | blueprint: Vec, 119 | } 120 | 121 | impl Plan { 122 | /// Create a new `Plan` with the given name and blueprint. 123 | pub fn new(name: String, blueprint: Vec) -> Self { 124 | Self { name, blueprint } 125 | } 126 | 127 | /// Generate a new plan for the given host. Does not consume this plan. Any 128 | /// [`Task`]s in this plan that do not apply to the specified host will be 129 | /// excluded from the resulting plan. 130 | #[tracing::instrument] 131 | pub fn plan_for_host(&self, host: &String, hosts: &HostConfig) -> Plan { 132 | Plan { 133 | name: self.name.clone(), 134 | blueprint: self 135 | .blueprint 136 | .iter() 137 | .filter_map(|task| { 138 | let group_names: Vec = hosts 139 | .groups() 140 | .iter() 141 | .filter(|(_name, hosts)| hosts.contains(host)) 142 | .map(|(name, _hosts)| name.clone()) 143 | .collect(); 144 | if task.hosts().contains(host) 145 | || task.hosts().iter().any(|host| group_names.contains(host)) 146 | { 147 | Some(task.clone()) 148 | } else { 149 | None 150 | } 151 | }) 152 | .collect(), 153 | } 154 | } 155 | } 156 | 157 | /// A planned [`Task`]. Contains information like the [`Ensure`]s necessary to 158 | /// validate this command. 159 | #[derive(Getters, Debug, Clone, Deserialize, Serialize)] 160 | pub struct PlannedTask { 161 | name: String, 162 | command: Vec, 163 | ensures: Vec, 164 | hosts: Vec, 165 | } 166 | 167 | impl PlannedTask { 168 | /// Create a new `PlannedTask` from a shell command string 169 | /// (ex. `echo "hi"`). 170 | #[tracing::instrument] 171 | pub fn from_shell_command + std::fmt::Debug>( 172 | name: S, 173 | command: S, 174 | hosts: Vec, 175 | ) -> Result { 176 | let split = shell_words::split(command.into().as_str())?; 177 | let head = split[0].clone(); 178 | Ok(Self { 179 | name: name.into(), 180 | ensures: vec![Ensure::ExeExists { exe: head }], 181 | command: split, 182 | hosts, 183 | }) 184 | } 185 | } 186 | 187 | /// Validations for a [`Task`]. These are generated during the planning phase. 188 | /// [`Ensure`]s are compiled down to shell commands executed on the remote host 189 | /// prior to any `Task` commands, to ensure that the `Task` can be executed. 190 | #[derive(Debug, Clone, Deserialize, Serialize)] 191 | pub enum Ensure { 192 | /// Ensure that the specified path exists on the host and is a file. 193 | FileExists { path: String }, 194 | /// Ensure that the specified path exists on the host and is a directory. 195 | DirectoryExists { path: String }, 196 | /// Ensure that the specified file does not exist on the host. Will fail if 197 | /// the path exists and is a directory. 198 | FileDoesntExist { path: String }, 199 | /// Ensure that the specified directory does not exist on the host. Will 200 | /// fail if the path exists and is a file. 201 | DirectoryDoesntExist { path: String }, 202 | /// Ensure that the specified executable exists on the host. 203 | ExeExists { exe: String }, 204 | } 205 | 206 | #[cfg(test)] 207 | mod tests { 208 | use color_eyre::eyre::Result; 209 | 210 | use crate::plan::host::HostConfig; 211 | 212 | use super::{Task, TaskSet}; 213 | 214 | #[tokio::test] 215 | async fn test_that_tasks_can_be_planned() -> Result<()> { 216 | let mut taskset = TaskSet::new("test"); 217 | taskset.add_task(Task::Command { 218 | name: "test".into(), 219 | command: "echo hello".into(), 220 | hosts: vec![], 221 | }); 222 | let mut plan = taskset.plan().await?; 223 | assert_eq!(1, plan.blueprint().len()); 224 | assert_eq!("test", plan.blueprint()[0].name()); 225 | assert_eq!("echo hello", plan.blueprint()[0].command().join(" ")); 226 | Ok(()) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /libthere/src/plan/visitor.rs: -------------------------------------------------------------------------------- 1 | //! A visitor for a [`Task`] in a [`TaskSet`]. Task visitors are used to do 2 | //! things like compile the [`Plan`] for a `TaskSet`. 3 | 4 | use std::path::Path; 5 | 6 | use async_trait::async_trait; 7 | use color_eyre::eyre::Result; 8 | use derive_getters::Getters; 9 | 10 | use super::{Ensure, Plan, PlannedTask, Task, TaskSet}; 11 | use crate::log::*; 12 | 13 | /// A visitor for a [`Task`] in a [`TaskSet`]. Task visitors are used to do 14 | /// things like compile the [`Plan`] for a `TaskSet`. 15 | pub trait TaskVisitor: Send + std::fmt::Debug { 16 | /// The output type of this visitor. 17 | type Out; 18 | 19 | /// Visit the given task. This is used for things like compiling a `Plan`. 20 | fn visit_task(&mut self, task: &Task) -> Result; 21 | } 22 | 23 | /// An implementation of [`TaskVisitor`] that compiles a [`Task`] into a 24 | /// `Vec`. 25 | #[derive(Getters, Debug, Clone)] 26 | pub struct PlanningTaskVisitor<'a> { 27 | name: &'a str, 28 | plan: Vec, 29 | } 30 | 31 | impl<'a> PlanningTaskVisitor<'a> { 32 | /// Create a new visitor. 33 | pub fn new(name: &'a str) -> Self { 34 | Self { name, plan: vec![] } 35 | } 36 | } 37 | 38 | impl<'a> TaskVisitor for PlanningTaskVisitor<'a> { 39 | type Out = (); 40 | 41 | /// Visits the given task and compiles it into a [`PlannedTask`] that is 42 | /// stored in the visitor's state. 43 | #[tracing::instrument] 44 | fn visit_task(&mut self, task: &Task) -> Result { 45 | debug!("planning task visitor: visiting task: {}", &task.name()); 46 | 47 | match task { 48 | Task::Command { name, command, .. } => { 49 | let mut final_command = vec![]; 50 | for shell_word in shell_words::split(command)? { 51 | final_command.push(shell_word.clone()); 52 | } 53 | self.plan.push(PlannedTask::from_shell_command( 54 | name, 55 | command, 56 | task.hosts(), 57 | )?); 58 | } 59 | Task::CreateDirectory { name, path, .. } => { 60 | self.plan.push(PlannedTask { 61 | name: name.to_string(), 62 | command: vec!["mkdir".into(), path.to_string()], 63 | ensures: vec![Ensure::DirectoryExists { 64 | path: path.to_string(), 65 | }], 66 | hosts: task.hosts(), 67 | }); 68 | } 69 | Task::TouchFile { name, path, .. } => { 70 | self.plan.push(PlannedTask { 71 | name: name.to_string(), 72 | command: vec!["touch".into(), path.to_string()], 73 | ensures: vec![ 74 | Ensure::ExeExists { 75 | exe: "touch".into(), 76 | }, 77 | Ensure::DirectoryExists { 78 | path: Path::new(path).parent().unwrap().display().to_string(), 79 | }, 80 | ], 81 | hosts: task.hosts(), 82 | }); 83 | } 84 | } 85 | 86 | debug!( 87 | "planning task visitor: finished planning task: {}", 88 | &task.name() 89 | ); 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/ci/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hosts: 3 | localhost: 4 | host: "localhost" 5 | executor: "simple" 6 | 7 | groups: 8 | test-group: 9 | - "localhost" 10 | -------------------------------------------------------------------------------- /test/ci/plan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "test plan" 3 | tasks: 4 | - type: "Command" 5 | name: "test command" 6 | command: "echo 'hello world!'" 7 | hosts: 8 | - "test-group" 9 | -------------------------------------------------------------------------------- /test/ci/ssh-hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hosts: 3 | localhost: 4 | host: "localhost" 5 | executor: "ssh" 6 | remote_user: "runner" 7 | 8 | groups: 9 | ssh: 10 | - "localhost" 11 | -------------------------------------------------------------------------------- /test/ci/ssh-plan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "test plan" 3 | tasks: 4 | - type: "Command" 5 | name: "test command" 6 | command: "echo 'hello world!'" 7 | hosts: 8 | - "ssh" 9 | -------------------------------------------------------------------------------- /test/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hosts: 3 | localhost: 4 | host: "localhost" 5 | executor: "simple" 6 | 7 | groups: 8 | test-group: 9 | - "localhost" 10 | -------------------------------------------------------------------------------- /test/invalid-plan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "test plan" 3 | tasks: 4 | - type: 69 5 | name: 420 6 | command: 42069 7 | -------------------------------------------------------------------------------- /test/plan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "test plan" 3 | tasks: 4 | - type: "Command" 5 | name: "test command" 6 | command: "echo 'hello world!'" 7 | hosts: 8 | - "test-group" 9 | -------------------------------------------------------------------------------- /test/ssh-hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hosts: 3 | broken-ssh: 4 | host: "localhost" 5 | port: 2222 6 | executor: "ssh" 7 | remote_user: "root" 8 | ssh-localhost: 9 | host: "localhost" 10 | executor: "ssh" 11 | remote_user: "amy" 12 | 13 | groups: 14 | ssh-group: 15 | - "broken-ssh" 16 | - "ssh-localhost" 17 | -------------------------------------------------------------------------------- /test/ssh-plan.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "test plan" 3 | tasks: 4 | - type: "Command" 5 | name: "test command" 6 | command: "echo 'hello world!'" 7 | hosts: 8 | - "ssh-group" 9 | - type: "Command" 10 | name: "test command 2" 11 | command: "echo 'hello world!! :D'" 12 | hosts: 13 | - "ssh-group" 14 | - type: "Command" 15 | name: "test command 2" 16 | command: "echo 'wow!!!!!'" 17 | hosts: 18 | - "ssh-group" 19 | - type: "TouchFile" 20 | name: "create some file" 21 | path: "/tmp/some-file" 22 | hosts: 23 | - "ssh-group" 24 | -------------------------------------------------------------------------------- /test/working-ssh-hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hosts: 3 | ssh-localhost: 4 | host: "localhost" 5 | executor: "ssh" 6 | remote_user: "amy" 7 | 8 | groups: 9 | ssh-group: 10 | - "ssh-localhost" 11 | -------------------------------------------------------------------------------- /there-agent/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /there-agent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "there-agent" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | color-eyre = { version = "0.6.2", features = ["issue-url"] } 10 | directories = "5.0.1" 11 | there = { path = "../libthere" } 12 | reqwest = { version = "0.11.20", features = ["blocking"] } 13 | time = { version = "0.3.29", features = ["macros"] } 14 | tracing-subscriber = { version = "0.3.17", features = ["json", "time"] } 15 | -------------------------------------------------------------------------------- /there-agent/src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use std::fs::{self, OpenOptions}; 4 | use std::io::Write; 5 | use std::time::Duration; 6 | 7 | use color_eyre::eyre; 8 | use color_eyre::eyre::Result; 9 | use there::log::*; 10 | use tracing_subscriber::util::SubscriberInitExt; 11 | 12 | fn main() -> Result<()> { 13 | install_color_eyre()?; 14 | 15 | tracing_subscriber::fmt::SubscriberBuilder::default() 16 | .with_timer(tracing_subscriber::fmt::time::UtcTime::new( 17 | time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), 18 | )) 19 | .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE) 20 | .json() 21 | .finish() 22 | .init(); 23 | 24 | let controller_dsn = std::env::var("THERE_CONTROLLER_BOOTSTRAP_DSN")?; 25 | info!("connecting to: {controller_dsn}"); 26 | 27 | loop { 28 | let controller_key = reqwest::blocking::get(controller_dsn.clone())?.text()?; 29 | 30 | // Read ~/.ssh/authorized_keys, check if controller_key is in it, and add it if it's not. 31 | let ssh_key_path = directories::UserDirs::new() 32 | .ok_or_else(|| eyre::eyre!("could not get user directories"))? 33 | .home_dir() 34 | .join(".ssh/authorized_keys"); 35 | 36 | let maybe_existing_key = fs::read_to_string(&ssh_key_path)?; 37 | let maybe_existing_key = maybe_existing_key 38 | .lines() 39 | .find(|line| line == &controller_key); 40 | 41 | if maybe_existing_key.is_none() { 42 | // Append ssh key to the end of file. 43 | let mut file = OpenOptions::new() 44 | .write(true) 45 | .append(true) 46 | .open(&ssh_key_path) 47 | .unwrap(); 48 | 49 | if let Err(e) = writeln!(file, "{controller_key}") { 50 | panic!("{}", e); 51 | } else { 52 | info!("added controller key to {}", ssh_key_path.display()); 53 | } 54 | } 55 | 56 | // Poll every 5 minutes 57 | std::thread::sleep(Duration::from_secs(60 * 5)); 58 | } 59 | 60 | #[allow(unreachable_code)] 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /there-cli/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /there-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "there-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | there = { path = "../libthere" } 10 | 11 | async-trait = "0.1.74" 12 | clap = { version = "4.4.11", features = ["cargo"] } 13 | color-eyre = { version = "0.6.2", features = ["issue-url"] } 14 | dialoguer = { version = "0.11.0", features = ["completion", "fuzzy-matcher", "fuzzy-select", "history"] } 15 | derive-getters = "0.3.0" 16 | futures = "0.3.28" 17 | regex = "1.10.2" 18 | reqwest = { version = "0.11.20", features = ["json"] } 19 | serde_yaml = "0.9.25" 20 | thiserror = "1.0.50" 21 | thrussh = { version = "0.34.0", features = ["openssl"] } 22 | thrussh-keys = { version = "0.22.1", features = ["openssl"] } 23 | time = { version = "0.3.29", features = ["macros"] } 24 | tokio = { version = "1.35.0", features = ["full"] } 25 | tokio-stream = "0.1.14" 26 | tokio-util = "0.7.10" 27 | tracing-subscriber = { version = "0.3.17", features = ["json", "serde", "serde_json", "time", "tracing", "env-filter", "local-time"] } 28 | tracing = { version = "0.1.38", features = ["async-await"] } 29 | serde_json = "1.0.107" 30 | -------------------------------------------------------------------------------- /there-cli/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use clap::ArgMatches; 3 | use color_eyre::eyre::Result; 4 | use dialoguer::Input; 5 | use regex::Regex; 6 | use thiserror::Error; 7 | 8 | pub mod plan; 9 | 10 | #[allow(unused)] 11 | #[derive(Error, Debug)] 12 | pub enum CommandErrors { 13 | #[error("Name must be at least 5 characters and contain only lowercase letters, numbers, and dashes (got: {0}).")] 14 | InvalidNameFormat(String), 15 | #[error("Prompt interaction failed.")] 16 | PromptInteractionFailed( 17 | #[from] 18 | #[source] 19 | std::io::Error, 20 | ), 21 | #[error("Required user input `{0}` is missing.")] 22 | RequiredUserInputMissing(String), 23 | #[error("Argument `{0}` failed validation `{1}`")] 24 | InputValidationFailure(String, String), 25 | #[error("Invalid HTTP method `{0}`")] 26 | InvalidHttpMethod(String), 27 | #[error("Invalid subcommand `{0}`.")] 28 | InvalidSubcommand(String), 29 | #[error("No subcommand provided.")] 30 | NoSubcommandProvided, 31 | } 32 | 33 | pub struct CliContext<'a> { 34 | pub client: reqwest::Client, 35 | pub matches: &'a ArgMatches, 36 | } 37 | 38 | impl<'a> CliContext<'a> { 39 | pub fn new(matches: &'a ArgMatches) -> Self { 40 | Self { 41 | client: reqwest::Client::new(), 42 | matches, 43 | } 44 | } 45 | 46 | #[allow(unused)] 47 | pub fn with_matches(&self, matches: &'a ArgMatches) -> Self { 48 | Self { 49 | client: self.client.clone(), 50 | matches, 51 | } 52 | } 53 | } 54 | 55 | #[async_trait] 56 | pub trait Command<'a> { 57 | fn new() -> Self 58 | where 59 | Self: Sized; 60 | 61 | async fn run(&self, context: &'a CliContext) -> Result<()>; 62 | } 63 | 64 | pub trait Interactive<'a> { 65 | #[tracing::instrument(skip(self))] 66 | fn prompt_for_input(&self, message: &'a str) -> Result { 67 | Input::::new() 68 | .with_prompt(message) 69 | .interact() 70 | .map_err(CommandErrors::PromptInteractionFailed) 71 | .map_err(color_eyre::eyre::Report::new) 72 | } 73 | 74 | #[tracing::instrument(skip(self))] 75 | fn prompt_for_input_with_default + std::fmt::Debug>( 76 | &self, 77 | message: &str, 78 | default: S, 79 | ) -> Result { 80 | Input::::new() 81 | .with_prompt(message) 82 | .default(default.into()) 83 | .interact() 84 | .map_err(CommandErrors::PromptInteractionFailed) 85 | .map_err(color_eyre::eyre::Report::new) 86 | } 87 | 88 | #[tracing::instrument(skip(self, validator))] 89 | fn prompt_for_input_with_validator(&self, message: &'a str, validator: V) -> Result 90 | where 91 | V: FnMut(&String) -> Result<(), CommandErrors>, 92 | { 93 | Input::::new() 94 | .with_prompt(message) 95 | .validate_with(validator) 96 | .interact() 97 | .map_err(CommandErrors::PromptInteractionFailed) 98 | .map_err(color_eyre::eyre::Report::new) 99 | } 100 | 101 | #[tracing::instrument(skip(self))] 102 | fn prompt_for_input_with_regex_validation( 103 | &self, 104 | message: &'a str, 105 | regex: &'a Regex, 106 | ) -> Result { 107 | self.prompt_for_input_with_validator(message, |input: &String| { 108 | if regex.is_match(input) { 109 | Ok(()) 110 | } else { 111 | Err(CommandErrors::InputValidationFailure( 112 | message.to_string(), 113 | regex.as_str().to_string(), 114 | )) 115 | } 116 | }) 117 | } 118 | 119 | /// Read argument from the CLI args with a validation function. 120 | #[tracing::instrument(skip(self, validator))] 121 | fn read_argument_with_validator( 122 | &self, 123 | arg_matches: &'a ArgMatches, 124 | id: &'a str, 125 | validator: &mut V, 126 | ) -> Result 127 | where 128 | V: FnMut(&String) -> Result<(), CommandErrors>, 129 | { 130 | if let Some(arg) = arg_matches.get_one::(id) { 131 | validator(arg)?; 132 | Ok(arg.clone()) 133 | } else { 134 | Err(CommandErrors::RequiredUserInputMissing(id.into()))? 135 | } 136 | } 137 | 138 | /// Read argument from the CLI args with regex validation. 139 | #[tracing::instrument(skip(self))] 140 | fn read_argument_with_regex_validation( 141 | &self, 142 | arg_matches: &'a ArgMatches, 143 | id: &'a str, 144 | regex: &'a Regex, 145 | ) -> Result { 146 | self.read_argument_with_validator(arg_matches, id, &mut |input| { 147 | if regex.is_match(input) { 148 | Ok(()) 149 | } else { 150 | Err(CommandErrors::InputValidationFailure( 151 | id.into(), 152 | regex.as_str().into(), 153 | )) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /there-cli/src/commands/plan.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::Path; 3 | use std::time::Duration; 4 | 5 | use super::Interactive; 6 | use async_trait::async_trait; 7 | use clap::ArgMatches; 8 | use color_eyre::eyre::Result; 9 | use futures::stream::FuturesUnordered; 10 | use there::executor::{simple, ssh, Executor, LogSource, PartialLogStream}; 11 | use there::ipc::http::{JobState, JobStatus}; 12 | use there::plan::host::{Host, HostConfig}; 13 | use there::{log::*, plan}; 14 | use tokio::fs; 15 | use tokio::sync::{mpsc, Mutex}; 16 | use tokio_stream::StreamExt; 17 | 18 | #[derive(Clone, Debug)] 19 | pub enum ExecutorType { 20 | Local, 21 | Ssh, 22 | } 23 | 24 | pub struct PlanCommand; 25 | 26 | impl PlanCommand { 27 | pub fn new() -> Self { 28 | Self 29 | } 30 | } 31 | 32 | impl PlanCommand { 33 | #[tracing::instrument(skip(self))] 34 | async fn read_hosts_config(&self, path: &Path) -> Result { 35 | let hosts = fs::read_to_string(path).await?; 36 | serde_yaml::from_str(hosts.as_str()).map_err(color_eyre::eyre::Report::new) 37 | } 38 | 39 | #[tracing::instrument(skip(self, _context))] 40 | async fn subcommand_validate<'a>( 41 | &self, 42 | _context: &'a super::CliContext<'a>, 43 | matches: &ArgMatches, 44 | ) -> Result<()> { 45 | let file = self.read_argument_with_validator(matches, "file", &mut |_| Ok(()))?; 46 | let hosts_file = &self.read_argument_with_validator(matches, "hosts", &mut |_| Ok(()))?; 47 | let hosts_file = Path::new(hosts_file); 48 | let hosts = self.read_hosts_config(hosts_file).await?; 49 | 50 | let plan = fs::read_to_string(file).await?; 51 | let task_set: there::plan::TaskSet = serde_yaml::from_str(plan.as_str())?; 52 | task_set.plan().await?; 53 | info!("plan is valid."); 54 | println!("* plan is valid."); 55 | println!("** hosts:"); 56 | for (group_name, group_hosts) in hosts.groups() { 57 | self.inspect_host_group(hosts.hosts(), group_name, group_hosts)?; 58 | } 59 | Ok(()) 60 | } 61 | 62 | #[tracing::instrument(skip(self))] 63 | fn inspect_host_group( 64 | &self, 65 | hosts: &HashMap, 66 | group_name: &String, 67 | group_hosts: &Vec, 68 | ) -> Result<()> { 69 | println!("*** group: {}", group_name); 70 | for hostname in group_hosts { 71 | let host = hosts 72 | .get(hostname) 73 | .ok_or_else(|| eyre!("host not found: {}", hostname))?; 74 | println!( 75 | "**** {}: {}:{} ({})", 76 | hostname, 77 | host.host(), 78 | host.port(), 79 | host.executor() 80 | ); 81 | } 82 | Ok(()) 83 | } 84 | 85 | #[tracing::instrument(skip(self, _context))] 86 | async fn subcommand_apply<'a>( 87 | &self, 88 | _context: &'a super::CliContext<'a>, 89 | matches: &ArgMatches, 90 | ) -> Result<()> { 91 | let file = self.read_argument_with_validator(matches, "file", &mut |_| Ok(()))?; 92 | let plan = fs::read_to_string(file).await?; 93 | let task_set: there::plan::TaskSet = serde_yaml::from_str(plan.as_str())?; 94 | let hosts_file = self.read_argument_with_validator(matches, "hosts", &mut |_| Ok(()))?; 95 | let hosts_file = Path::new(&hosts_file); 96 | let hosts = self.read_hosts_config(hosts_file).await?; 97 | 98 | let plan = task_set.plan().await?; 99 | let controller = matches.get_one::("controller"); 100 | 101 | if *matches.get_one::("dry").unwrap() { 102 | println!("*** plan: {} ***\n", plan.name()); 103 | println!("* metadata"); 104 | println!("** hosts:"); 105 | for (group_name, group_hosts) in hosts.groups() { 106 | self.inspect_host_group(hosts.hosts(), group_name, group_hosts)?; 107 | } 108 | for task in plan.blueprint() { 109 | println!("** {}: {}", task.name(), task.command().join(" ")); 110 | for ensure in task.ensures() { 111 | println!("*** {:?}", ensure); 112 | } 113 | } 114 | } else { 115 | info!("applying plan..."); 116 | let mut futures = FuturesUnordered::new(); 117 | if let Some(controller) = controller { 118 | info!("applying plan via controller {controller}!"); 119 | // TODO: reqwest::post(controller).json({plan, hosts}).await?; stream_logs(); 120 | let client = reqwest::Client::new(); 121 | let res = client 122 | .post(format!("{controller}/api/plan/run")) 123 | .json(&serde_json::json!({ "plan": plan, "hosts": hosts })) 124 | .send() 125 | .await?; 126 | let job_id = res.text().await?; 127 | debug!("job_id: {job_id}"); 128 | println!("* plan assigned controller job id: {job_id}"); 129 | 130 | let mut log_offsets = HashMap::new(); 131 | loop { 132 | let job_state = client 133 | .get(format!("{controller}/api/plan/{job_id}/status")) 134 | .send() 135 | .await?; 136 | let job_state = job_state.json::().await?; 137 | 138 | for (hostname, logs) in job_state.logs { 139 | if logs.len() > *log_offsets.get(&hostname).unwrap_or(&0usize) { 140 | for log in logs 141 | .iter() 142 | .skip(*log_offsets.get(&hostname).unwrap_or(&0usize)) 143 | { 144 | println!("{}: {}", log.hostname, log.log); 145 | } 146 | log_offsets.insert(hostname, logs.len()); 147 | } 148 | } 149 | 150 | if job_state.status == JobStatus::Completed { 151 | println!("* plan completed!"); 152 | info!("finished execution for plan {job_id}"); 153 | break; 154 | } 155 | 156 | if job_state.status == JobStatus::Failed { 157 | println!("* plan execution failed!"); 158 | info!("failed execution for plan {job_id}"); 159 | break; 160 | } 161 | 162 | tokio::time::sleep(Duration::from_millis(50)).await; 163 | } 164 | } else { 165 | info!("applying plan via executor!"); 166 | for (group_name, group_hosts) in hosts.groups() { 167 | println!("*** applying plan to group: {} ***", group_name); 168 | for hostname in group_hosts { 169 | let plan = plan.plan_for_host(hostname, &hosts); 170 | if !plan.blueprint().is_empty() { 171 | let host = &hosts 172 | .hosts() 173 | .get(hostname) 174 | .ok_or_else(|| eyre!("hostname not in host map: {}", hostname))?; 175 | let executor = host.executor(); 176 | let executor_type: ExecutorType = match executor.as_str() { 177 | "simple" => ExecutorType::Local, 178 | "local" => ExecutorType::Local, 179 | "ssh" => ExecutorType::Ssh, 180 | _ => return Err(eyre!("unknown executor type: {}", executor)), 181 | }; 182 | futures.push(self.do_apply( 183 | plan, 184 | hostname.clone(), 185 | host, 186 | executor_type, 187 | matches, 188 | )); 189 | println!("*** prepared plan for host: {}", &hostname); 190 | } else { 191 | println!("*** skipping host, no tasks: {}", &hostname); 192 | } 193 | } 194 | } 195 | } 196 | while let Some(result) = futures.next().await { 197 | match result { 198 | Ok((host, tasks_completed)) => { 199 | println!( 200 | "*** completed plan: {} for host: {}: {}/{} ***", 201 | &plan.name(), 202 | host, 203 | tasks_completed, 204 | plan.blueprint().len() 205 | ); 206 | } 207 | Err(e) => { 208 | warn!("error applying plan: {}", e); 209 | #[allow(clippy::single_match)] 210 | match e.downcast() { 211 | Ok(PlanApplyErrors::PlanApplyFailed(host, tasks_completed, e)) => { 212 | println!( 213 | "*** failed plan: {} for host: {}: {}/{} ***", 214 | &plan.name(), 215 | host, 216 | tasks_completed, 217 | plan.blueprint().len() 218 | ); 219 | println!("*** error: {:#?}", e); 220 | } 221 | Err(msg) => { 222 | println!("{}", msg); 223 | } 224 | #[allow(unreachable_patterns)] 225 | e => { 226 | println!("*** failed plan: ??? for host: ???: ???/??? ***",); 227 | println!("*** error: {:#?}", e); 228 | println!("THIS SHOULD NEVER HAPPEN"); 229 | } 230 | } 231 | } 232 | } 233 | } 234 | info!("done!"); 235 | } 236 | 237 | Ok(()) 238 | } 239 | 240 | /// Returns how many tasks passed. 241 | #[tracing::instrument(skip(self, plan, matches))] 242 | async fn do_apply( 243 | &self, 244 | plan: plan::Plan, 245 | hostname: String, 246 | host: &Host, 247 | executor_type: ExecutorType, 248 | matches: &ArgMatches, 249 | ) -> Result<(String, u32)> { 250 | let (tx, rx) = mpsc::channel(1024); 251 | let mut log_source = there::executor::simple::SimpleLogSource::new(rx); 252 | let log_hostname = hostname.clone(); 253 | let ssh_hostname = hostname.clone(); 254 | let join_handle = tokio::task::spawn(async move { 255 | 'outer: while let Ok(partial_stream) = log_source.source().await { 256 | match partial_stream { 257 | PartialLogStream::Next(logs) => { 258 | for log in logs { 259 | println!("{}: {}", log_hostname, log); 260 | } 261 | } 262 | PartialLogStream::End => { 263 | break 'outer; 264 | } 265 | } 266 | } 267 | info!("join finished :D"); 268 | }); 269 | 270 | // TODO: Figure out this generics mess lmao 271 | let tasks_completed = match executor_type { 272 | ExecutorType::Local => { 273 | let mut context = simple::SimpleExecutionContext::new("test", &plan); 274 | let context = Mutex::new(&mut context); 275 | let mut executor = simple::SimpleExecutor::new(&tx); 276 | executor.execute(context).await 277 | .map_err(|err| { 278 | eyre!( 279 | "local executor failed to apply plan {} to host {}: {}/{} tasks finished:\n\n{:?}", 280 | plan.name(), 281 | hostname, 282 | executor.tasks_completed(), 283 | plan.blueprint().len(), 284 | err 285 | ) 286 | }) 287 | ?; 288 | Ok(*executor.tasks_completed()) 289 | } 290 | ExecutorType::Ssh => { 291 | let ssh_key_file = matches 292 | .get_one::("ssh-key") 293 | .ok_or(eyre!("--ssh-key not passed!"))?; 294 | let ssh_key = fs::read_to_string(ssh_key_file).await?; 295 | 296 | let ssh_key_passphrase = matches 297 | .get_one::("ssh-key-passphrase") 298 | .map(std::fs::read_to_string); 299 | let ssh_key_passphrase = match ssh_key_passphrase { 300 | Some(Ok(passphrase)) => Some(passphrase), 301 | Some(Err(e)) => { 302 | return Err(eyre!("failed to read ssh-key-passphrase file: {}", e)) 303 | } 304 | None => None, 305 | }; 306 | let mut context = ssh::SshExecutionContext::new("test", &plan); 307 | let context = Mutex::new(&mut context); 308 | #[allow(clippy::or_fun_call)] 309 | let mut executor = 310 | ssh::SshExecutor::new(host, &ssh_hostname, &tx, &ssh_key, ssh_key_passphrase)?; 311 | match executor.execute(context).await.map_err(|err| { 312 | eyre!( 313 | "ssh executor failed to apply plan {} to host {}: {}/{} tasks finished:\n\n{:?}", 314 | plan.name(), 315 | hostname, 316 | executor.tasks_completed(), 317 | plan.blueprint().len(), 318 | err 319 | ) 320 | }) { 321 | Ok(_) => Ok(*executor.tasks_completed()), 322 | Err(e) => Err(PlanApplyErrors::PlanApplyFailed( 323 | hostname.clone(), 324 | *executor.tasks_completed(), 325 | e, 326 | )), 327 | } 328 | } 329 | #[allow(unreachable_patterns)] 330 | _ => { 331 | unreachable!() 332 | } 333 | }; 334 | info!("finished applying plan"); 335 | match join_handle.await { 336 | Ok(_) => match tasks_completed { 337 | Ok(tasks_completed) => Ok((hostname, tasks_completed)), 338 | Err(e) => Err(eyre!("failed to apply plan: {}", e)), 339 | }, 340 | e @ Err(_) => e 341 | .map(|_| (hostname, 0)) 342 | .map_err(color_eyre::eyre::Report::new), 343 | } 344 | } 345 | } 346 | 347 | #[async_trait] 348 | impl<'a> super::Command<'a> for PlanCommand { 349 | fn new() -> Self 350 | where 351 | Self: Sized, 352 | { 353 | Self {} 354 | } 355 | 356 | #[tracing::instrument(skip(self, context))] 357 | async fn run(&self, context: &'a super::CliContext) -> Result<()> { 358 | match context.matches.subcommand() { 359 | Some(("validate", matches)) => { 360 | self.subcommand_validate(context, matches).await?; 361 | } 362 | Some(("apply", matches)) => { 363 | self.subcommand_apply(context, matches).await?; 364 | } 365 | Some((name, _)) => { 366 | return Err(super::CommandErrors::InvalidSubcommand(name.to_string()).into()) 367 | } 368 | None => return Err(super::CommandErrors::NoSubcommandProvided.into()), 369 | } 370 | Ok(()) 371 | } 372 | } 373 | 374 | impl<'a> super::Interactive<'a> for PlanCommand {} 375 | 376 | #[derive(thiserror::Error, Debug)] 377 | enum PlanApplyErrors { 378 | #[error("failed to apply plan to host: {0} ({1} tasks complete): {2}")] 379 | PlanApplyFailed(String, u32, color_eyre::eyre::Error), 380 | } 381 | -------------------------------------------------------------------------------- /there-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use clap::{command, Arg, ArgAction}; 4 | use color_eyre::eyre::Result; 5 | use tracing_subscriber::filter::LevelFilter; 6 | use tracing_subscriber::util::SubscriberInitExt; 7 | 8 | use crate::commands::Command; 9 | 10 | mod commands; 11 | 12 | use there::log::*; 13 | 14 | #[tokio::main] 15 | #[tracing::instrument] 16 | async fn main() -> Result<()> { 17 | install_color_eyre()?; 18 | 19 | // Command configuration 20 | let matches = command!() 21 | .arg( 22 | Arg::new("verbose") 23 | .short('v') 24 | .long("verbose") 25 | .help("Turn debugging information on. Overrides -q. Can specify up to -vv.") 26 | .action(ArgAction::Count), 27 | ) 28 | .arg( 29 | Arg::new("quiet") 30 | .short('q') 31 | .long("quiet") 32 | .help("Silence all output. Overridden by -v.") 33 | .action(ArgAction::SetTrue), 34 | ) 35 | .subcommand( 36 | command!("plan") 37 | .about("Manage plans.") 38 | .subcommand( 39 | command!("validate") 40 | .about("Validate a plan.") 41 | .arg( 42 | Arg::new("file") 43 | .help("Path to the plan file. No default.") 44 | .short('f') 45 | .long("file"), 46 | ).arg( 47 | Arg::new("hosts") 48 | .help("Path to the hosts file. No default.") 49 | .long("hosts"), 50 | ), 51 | ) 52 | .subcommand( 53 | command!("apply") 54 | .about("Apply a plan.") 55 | .arg( 56 | Arg::new("file") 57 | .help("Path to the plan file.") 58 | .short('f') 59 | .long("file"), 60 | ) 61 | .arg( 62 | Arg::new("dry") 63 | .help("Don't actually apply the plan, just show the changes it will make.") 64 | .short('d') 65 | .long("dry") 66 | .action(ArgAction::SetTrue), 67 | ) 68 | .arg( 69 | Arg::new("hosts") 70 | .help("Path to the hosts file. No default.") 71 | .long("hosts"), 72 | ) 73 | .arg( 74 | Arg::new("ssh-key") 75 | .help("Path to the SSH key to use for SSH executor.") 76 | .short('k') 77 | .long("ssh-key"), 78 | ) 79 | .arg(Arg::new("ssh-key-passphrase").help("Path to the SSH key passphrase file.").short('p').long("ssh-key-passphrase")) 80 | .arg( 81 | Arg::new("controller") 82 | .help("Controller to do remote execution with.") 83 | .short('c') 84 | .long("controller"), 85 | ) 86 | , 87 | ) 88 | ) 89 | .subcommand_required(true) 90 | .get_matches(); 91 | 92 | // Set up logging 93 | let logging_config = tracing_subscriber::fmt::SubscriberBuilder::default() 94 | .with_timer(tracing_subscriber::fmt::time::UtcTime::new( 95 | time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), 96 | )) 97 | .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE) 98 | .compact(); 99 | 100 | let quiet = matches.get_flag("quiet"); 101 | let verbose = matches.get_count("verbose") as usize; 102 | let logging_config = if quiet && verbose == 0 { 103 | logging_config.with_max_level(LevelFilter::ERROR) 104 | } else if verbose > 0 { 105 | let level = match verbose { 106 | 1 => LevelFilter::WARN, 107 | 2 => LevelFilter::INFO, 108 | 3 => { 109 | std::env::set_var("RUST_LIB_BACKTRACE", "1"); 110 | LevelFilter::DEBUG 111 | } 112 | _ => { 113 | std::env::set_var("RUST_LIB_BACKTRACE", "full"); 114 | LevelFilter::TRACE 115 | } 116 | }; 117 | logging_config.with_max_level(level) 118 | } else { 119 | logging_config.with_max_level(LevelFilter::ERROR) 120 | }; 121 | if let Some(expected_exe) = std::env::args().next() { 122 | if expected_exe == "target/debug/there-cli" { 123 | std::env::set_var("RUST_LIB_BACKTRACE", "full"); 124 | } 125 | } 126 | 127 | let subscriber = logging_config.finish(); 128 | subscriber.init(); 129 | 130 | // Run the commands 131 | if let Some((subcommand, matches)) = matches.subcommand() { 132 | let ctx = commands::CliContext::new(matches); 133 | debug!( 134 | "matched subcommand {} with matches: {:?}", 135 | &subcommand, 136 | &matches.ids().map(|id| id.as_str()).collect::>() 137 | ); 138 | match subcommand { 139 | "plan" => commands::plan::PlanCommand::new().run(&ctx).await?, 140 | _ => return Err(eyre!("Unrecognized subcommand: {}", subcommand)), 141 | } 142 | } 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /there-controller/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /there-controller/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "there-controller" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | there = { path = "../libthere" } 10 | 11 | axum = "0.6.20" 12 | color-eyre = { version = "0.6.2", features = ["issue-url"] } 13 | futures = "0.3.28" 14 | rand = "0.8.5" 15 | thrussh = { version = "0.34.0", features = ["openssl"] } 16 | thrussh-keys = { version = "0.22.1", features = ["openssl"] } 17 | tokio = { version = "1.35.0", features = ["full"] } 18 | network-interface = "1.0.3" 19 | serde = { version = "1.0.193", features = ["derive"] } 20 | quick_cache = "0.3.0" 21 | nanoid = "0.4.0" 22 | nanoid-dictionary = "0.4.3" 23 | serde_json = "1.0.107" 24 | hyper = "0.14.27" 25 | tracing = "0.1.38" 26 | thiserror = "1.0.50" 27 | tracing-subscriber = { version = "0.3.17", features = ["json", "time"] } 28 | time = { version = "0.3.29", features = ["macros", "formatting"] } 29 | -------------------------------------------------------------------------------- /there-controller/src/executor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use color_eyre::eyre::Result; 4 | use futures::stream::FuturesUnordered; 5 | use futures::StreamExt; 6 | use there::executor::{ssh, Executor, LogSource, PartialLogStream}; 7 | use there::ipc::http::LogEntry; 8 | use there::log::*; 9 | use there::plan::host::{Host, HostConfig}; 10 | use there::plan::Plan; 11 | use tokio::sync::{mpsc, Mutex}; 12 | 13 | use crate::http_server::ServerState; 14 | 15 | #[tracing::instrument] 16 | pub async fn apply_plan<'a>( 17 | job_id: &String, 18 | server_state: &Arc>, 19 | plan: Plan, 20 | hosts: HostConfig, 21 | ) -> Result<()> { 22 | let mut futures = FuturesUnordered::new(); 23 | #[allow(clippy::for_kv_map)] 24 | for (_group_name, group_hosts) in hosts.groups() { 25 | // println!("*** applying plan to group: {} ***", group_name); 26 | for hostname in group_hosts { 27 | let plan = plan.plan_for_host(hostname, &hosts); 28 | if !plan.blueprint().is_empty() { 29 | let host = &hosts 30 | .hosts() 31 | .get(hostname) 32 | .ok_or_else(|| eyre!("hostname not in host map: {}", hostname))?; 33 | futures.push(do_apply( 34 | job_id.clone(), 35 | plan, 36 | hostname.clone(), 37 | host, 38 | server_state.clone(), 39 | )); 40 | // println!("*** prepared plan for host: {}", &hostname); 41 | } else { 42 | // println!("*** skipping host, no tasks: {}", &hostname); 43 | } 44 | } 45 | } 46 | while let Some(result) = futures.next().await { 47 | match result { 48 | Ok((host, tasks_completed)) => { 49 | println!( 50 | "*** completed plan: {} for host: {}: {}/{} ***", 51 | &plan.name(), 52 | host, 53 | tasks_completed, 54 | plan.blueprint().len() 55 | ); 56 | } 57 | Err(e) => { 58 | warn!("error applying plan: {}", e); 59 | #[allow(clippy::single_match)] 60 | match e.downcast() { 61 | Ok(PlanApplyErrors::PlanApplyFailed(host, tasks_completed, e)) => { 62 | println!( 63 | "*** failed plan: {} for host: {}: {}/{} ***", 64 | &plan.name(), 65 | host, 66 | tasks_completed, 67 | plan.blueprint().len() 68 | ); 69 | println!("*** error: {:#?}", e); 70 | } 71 | Err(msg) => { 72 | println!("{}", msg); 73 | } 74 | #[allow(unreachable_patterns)] 75 | e => { 76 | println!("*** failed plan: ??? for host: ???: ???/??? ***",); 77 | println!("*** error: {:#?}", e); 78 | println!("THIS SHOULD NEVER HAPPEN"); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | info!("finished applying plan! :D"); 86 | 87 | Ok(()) 88 | } 89 | 90 | /// Returns how many tasks passed. 91 | #[tracing::instrument(skip(plan, host))] 92 | async fn do_apply( 93 | job_id: String, 94 | plan: Plan, 95 | hostname: String, 96 | host: &Host, 97 | state: Arc>, 98 | ) -> Result<(String, u32)> { 99 | let (tx, rx) = mpsc::channel(1024); 100 | let mut log_source = there::executor::simple::SimpleLogSource::new(rx); 101 | let log_hostname = hostname.clone(); 102 | let ssh_hostname = hostname.clone(); 103 | let join_handle = tokio::task::spawn(async move { 104 | 'outer: while let Ok(partial_stream) = log_source.source().await { 105 | match partial_stream { 106 | PartialLogStream::Next(logs) => { 107 | // Try to hold the server state lock for as little time as 108 | // possible. 109 | let state = state.lock().await; 110 | if let Some(mut job_state) = state.jobs.get(&job_id.clone()) { 111 | let mut logs: Vec = logs 112 | .iter() 113 | .map(|log| LogEntry { 114 | hostname: log_hostname.clone(), 115 | log: log.clone(), 116 | }) 117 | .collect(); 118 | job_state 119 | .logs 120 | .entry(log_hostname.clone()) 121 | .or_insert_with(Vec::new); 122 | job_state 123 | .logs 124 | .get_mut(&log_hostname) 125 | .unwrap() 126 | .append(&mut logs); 127 | state.jobs.insert(job_id.clone(), job_state); 128 | } else { 129 | error!("missing state for job {job_id}!?"); 130 | } 131 | } 132 | PartialLogStream::End => { 133 | break 'outer; 134 | } 135 | } 136 | } 137 | }); 138 | 139 | let tasks_completed = { 140 | let mut context = ssh::SshExecutionContext::new("test", &plan); 141 | let context = Mutex::new(&mut context); 142 | #[allow(clippy::or_fun_call)] 143 | let mut executor = ssh::SshExecutor::new_with_existing_key( 144 | host, 145 | &ssh_hostname, 146 | &tx, 147 | crate::keys::get_or_create_executor_keypair().await?, 148 | )?; 149 | match executor.execute(context).await.map_err(|err| { 150 | eyre!( 151 | "ssh executor failed to apply plan {} to host {}: {}/{} tasks finished:\n\n{:?}", 152 | plan.name(), 153 | hostname, 154 | executor.tasks_completed(), 155 | plan.blueprint().len(), 156 | err 157 | ) 158 | }) { 159 | Ok(_) => Ok(*executor.tasks_completed()), 160 | Err(e) => Err(PlanApplyErrors::PlanApplyFailed( 161 | hostname.clone(), 162 | *executor.tasks_completed(), 163 | e, 164 | )), 165 | } 166 | }; 167 | 168 | match join_handle.await { 169 | Ok(_) => match tasks_completed { 170 | Ok(tasks_completed) => Ok((hostname, tasks_completed)), 171 | Err(e) => Err(eyre!("failed to apply plan: {}", e)), 172 | }, 173 | e @ Err(_) => e 174 | .map(|_| (hostname, 0)) 175 | .map_err(color_eyre::eyre::Report::new), 176 | } 177 | } 178 | 179 | #[derive(thiserror::Error, Debug)] 180 | enum PlanApplyErrors { 181 | #[error("failed to apply plan to host: {0} ({1} tasks complete): {2}")] 182 | PlanApplyFailed(String, u32, color_eyre::eyre::Error), 183 | } 184 | -------------------------------------------------------------------------------- /there-controller/src/http_server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::net::SocketAddr; 3 | use std::sync::Arc; 4 | 5 | use axum::extract::{Json, Path, Query, State}; 6 | use axum::http::StatusCode; 7 | use axum::response::IntoResponse; 8 | use axum::routing::{get, post}; 9 | use axum::Router; 10 | use color_eyre::eyre::Result; 11 | use nanoid::nanoid; 12 | use quick_cache::sync::Cache; 13 | use serde::{Deserialize, Serialize}; 14 | use there::ipc::http::{JobStartRequest, JobState, JobStatus}; 15 | use there::log::*; 16 | use thrussh_keys::PublicKeyBase64; 17 | use tokio::sync::Mutex; 18 | 19 | #[derive(Debug)] 20 | pub struct ServerState { 21 | pub jobs: Cache, 22 | } 23 | 24 | #[tracing::instrument] 25 | pub async fn run_server(port: u16) -> Result<()> { 26 | let state = Arc::new(Mutex::new(ServerState { 27 | jobs: Cache::new(1000), 28 | })); 29 | 30 | let app = Router::new() 31 | .route("/", get(root)) 32 | .route("/api/bootstrap", get(bootstrap)) 33 | .route("/api/plan/run", post(run_plan)) 34 | .route("/api/plan/:job_id/status", get(get_job_status)) 35 | .with_state(state); 36 | 37 | let addr = SocketAddr::from(([0, 0, 0, 0], port)); 38 | axum::Server::bind(&addr) 39 | .serve(app.into_make_service()) 40 | .await 41 | .map_err(|e| e.into()) 42 | } 43 | 44 | #[tracing::instrument] 45 | async fn root() -> &'static str { 46 | "there-controller" 47 | } 48 | 49 | #[tracing::instrument] 50 | async fn run_plan( 51 | State(state): State>>, 52 | Json(body): Json, 53 | ) -> impl IntoResponse { 54 | let job_id = nanoid!(21, nanoid_dictionary::NOLOOKALIKES_SAFE); 55 | let job_accessible_body = body.clone(); 56 | let job_state = JobState { 57 | logs: HashMap::new(), 58 | plan: body.plan, 59 | hosts: body.hosts, 60 | status: JobStatus::Running, 61 | }; 62 | 63 | let job_accessible_state = state.clone(); 64 | let state = state.lock().await; 65 | state.jobs.insert(job_id.clone(), job_state); 66 | 67 | let job_id_clone = job_id.clone(); 68 | tokio::spawn(async move { 69 | match crate::executor::apply_plan( 70 | &job_id_clone, 71 | &job_accessible_state, 72 | job_accessible_body.plan, 73 | job_accessible_body.hosts, 74 | ) 75 | .await 76 | { 77 | Ok(_) => { 78 | let state = job_accessible_state.lock().await; 79 | if let Some(mut job_state) = state.jobs.get(&job_id_clone.clone()) { 80 | job_state.status = JobStatus::Completed; 81 | state.jobs.insert(job_id_clone.clone(), job_state); 82 | } else { 83 | error!("job {job_id_clone} disappeared from state"); 84 | } 85 | } 86 | Err(e) => { 87 | error!("error applying plan {job_id_clone}: {e}"); 88 | let state = job_accessible_state.lock().await; 89 | if let Some(mut job_state) = state.jobs.get(&job_id_clone.clone()) { 90 | job_state.status = JobStatus::Failed; 91 | state.jobs.insert(job_id_clone.clone(), job_state); 92 | } else { 93 | error!("job {job_id_clone} disappeared from state"); 94 | } 95 | } 96 | } 97 | }); 98 | 99 | (StatusCode::OK, job_id) 100 | } 101 | 102 | #[tracing::instrument] 103 | async fn get_job_status( 104 | State(state): State>>, 105 | Path(job_id): Path, 106 | ) -> impl IntoResponse { 107 | let state = state.lock().await; 108 | let job_state = state.jobs.get(&job_id).unwrap(); 109 | 110 | (StatusCode::OK, serde_json::to_string(&job_state).unwrap()) 111 | } 112 | 113 | #[derive(Serialize, Deserialize, Debug)] 114 | struct Bootstrap { 115 | token: String, 116 | } 117 | 118 | #[tracing::instrument] 119 | async fn bootstrap(bootstrap: Query) -> impl IntoResponse { 120 | let token = crate::keys::get_or_create_token().await.unwrap(); 121 | if token == bootstrap.token { 122 | let pubkey = crate::keys::get_or_create_executor_keypair() 123 | .await 124 | .map(|keys| keys.public_key_base64()) 125 | .unwrap(); 126 | 127 | ( 128 | StatusCode::OK, 129 | format!("ssh-ed25519 {} there-controller", pubkey), 130 | ) 131 | } else { 132 | (StatusCode::UNAUTHORIZED, "invalid token".into()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /there-controller/src/keys.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use color_eyre::eyre::Result; 4 | use there::log::*; 5 | use tokio::fs::{self, File}; 6 | use tokio::io::AsyncWriteExt; 7 | 8 | #[tracing::instrument] 9 | pub fn passphrase() -> Result { 10 | std::env::var("THERE_SSH_PASSPHRASE").map_err(|e| e.into()) 11 | } 12 | 13 | #[tracing::instrument] 14 | pub async fn get_or_create_executor_keypair() -> Result { 15 | let path = Path::new("./there-controller-executor-key"); 16 | if path.exists() { 17 | let key = fs::read_to_string(path).await?; 18 | thrussh_keys::decode_secret_key(&key, Some(&passphrase()?)).map_err(|e| e.into()) 19 | } else { 20 | let mut file = File::create(path).await?; 21 | // Safety: thrussh_keys always returns Some(...) right now. 22 | let key = thrussh_keys::key::KeyPair::generate_ed25519().unwrap(); 23 | let mut pem = Vec::new(); 24 | debug!("encrypting key with 10_000 rounds..."); 25 | thrussh_keys::encode_pkcs8_pem_encrypted(&key, passphrase()?.as_bytes(), 10_000, &mut pem)?; 26 | file.write_all(&pem).await?; 27 | Ok(key) 28 | } 29 | } 30 | 31 | #[tracing::instrument] 32 | pub async fn get_or_create_token() -> Result { 33 | let path = Path::new("./there-controller-agent-token"); 34 | if path.exists() { 35 | Ok(fs::read_to_string(path).await?) 36 | } else { 37 | let mut file = File::create(path).await?; 38 | let token = generate_token()?; 39 | file.write_all(token.as_bytes()).await?; 40 | Ok(token) 41 | } 42 | } 43 | 44 | #[tracing::instrument] 45 | fn generate_token() -> Result { 46 | use rand::Rng; 47 | let mut rng = rand::thread_rng(); 48 | let token: String = (0..32) 49 | .map(|_| rng.sample(rand::distributions::Alphanumeric) as char) 50 | .collect(); 51 | Ok(token) 52 | } 53 | -------------------------------------------------------------------------------- /there-controller/src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use color_eyre::eyre::Result; 4 | use network_interface::{NetworkInterface, NetworkInterfaceConfig}; 5 | use there::log::*; 6 | use tracing_subscriber::util::SubscriberInitExt; 7 | 8 | mod executor; 9 | mod http_server; 10 | mod keys; 11 | 12 | const PORT: u16 = 2345; 13 | 14 | #[tokio::main] 15 | #[tracing::instrument] 16 | async fn main() -> Result<()> { 17 | install_color_eyre()?; 18 | tracing_subscriber::fmt::SubscriberBuilder::default() 19 | .with_timer(tracing_subscriber::fmt::time::UtcTime::new( 20 | time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), 21 | )) 22 | .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE) 23 | .json() 24 | .finish() 25 | .init(); 26 | 27 | info!("starting there-controller..."); 28 | 29 | info!("ensuring keypair exists..."); 30 | let _client_key = keys::get_or_create_executor_keypair().await?; 31 | 32 | info!("ensuring agent token exists..."); 33 | let token = keys::get_or_create_token().await?; 34 | println!("* token for agent connections: {token}"); 35 | println!("* agents can bootstrap via:"); 36 | for iface in NetworkInterface::show()? { 37 | if let Some(addr) = iface.addr.get(0) { 38 | if addr.ip().is_loopback() { 39 | continue; 40 | } 41 | match addr.ip() { 42 | std::net::IpAddr::V4(v4) => { 43 | let first_octet = v4.octets()[0]; 44 | let second_octet = v4.octets()[1]; 45 | if first_octet == 127 // 127.x.x.x 46 | || (first_octet == 169 && second_octet == 254) // 169.254.x.x 47 | || (first_octet == 172 && (16..=31).contains(&second_octet)) 48 | // 172.16.x.x - 172.31.x.x 49 | { 50 | continue; 51 | } 52 | if v4.is_loopback() { 53 | continue; 54 | } 55 | } 56 | std::net::IpAddr::V6(v6) => { 57 | if v6.segments()[0] == 0xfe80 { 58 | continue; 59 | } 60 | if v6.is_loopback() { 61 | continue; 62 | } 63 | } 64 | } 65 | 66 | println!( 67 | " - http://{}:{PORT}/api/bootstrap?token={token}", 68 | addr.ip() 69 | ); 70 | } 71 | } 72 | 73 | http_server::run_server(PORT).await 74 | } 75 | --------------------------------------------------------------------------------