├── .dockerignore ├── .github └── workflows │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── README.md ├── helm ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── role.yaml │ └── serviceaccount.yaml └── values.yaml ├── src ├── config.rs ├── consts.rs ├── error.rs ├── finalizers.rs ├── label_filter.rs ├── lb.rs └── main.rs └── tutorial.md /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.env 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | permissions: 8 | contents: read 9 | packages: write 10 | 11 | 12 | jobs: 13 | release_image: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Update version 19 | # Update a version in Cargo.toml before building the app. 20 | run: sed -i '0,/^version = .*/{s/version = .*/version = "${{ github.ref_name }}"/}' Cargo.toml 21 | - name: Set up Docker 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Login to GitHub Container Registry 26 | uses: docker/login-action@v2 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Build and push 32 | uses: docker/build-push-action@v2 33 | with: 34 | context: . 35 | file: ./Dockerfile 36 | platforms: linux/amd64 # ,linux/arm64,linux/arm/v7 (uncomment once the fix for long builds is found). 37 | push: true 38 | tags: ghcr.io/intreecom/robotlb:latest,ghcr.io/intreecom/robotlb:${{ github.ref_name }} 39 | 40 | upload_helm: 41 | runs-on: ubuntu-latest 42 | needs: [release_image] 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: actions-rust-lang/setup-rust-toolchain@v1 46 | with: 47 | toolchain: stable 48 | - uses: azure/setup-helm@v4.2.0 49 | with: 50 | version: latest 51 | - name: Build Helm chart 52 | run: | 53 | helm package --app-version "${{ github.ref_name }}" --dependency-update ./helm 54 | helm show chart *.tgz 55 | helm registry login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io 56 | helm push *.tgz oci://ghcr.io/intreecom/charts 57 | env: 58 | HELM_EXPERIMENTAL_OCI: 1 59 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: "Testing package" 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | pre-commit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-python@v3 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | override: true 15 | components: rustfmt, clippy 16 | - uses: pre-commit/action@v3.0.0 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.env 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - repo: local 7 | hooks: 8 | - id: fmt 9 | types: 10 | - rust 11 | name: cargo fmt 12 | language: system 13 | entry: cargo 14 | pass_filenames: false 15 | args: 16 | - fmt 17 | - --all 18 | 19 | - id: clippy 20 | types: 21 | - rust 22 | name: cargo clippy 23 | language: system 24 | pass_filenames: false 25 | entry: cargo 26 | args: 27 | - clippy 28 | - --all 29 | 30 | - id: check 31 | types: 32 | - rust 33 | name: cargo check 34 | language: system 35 | entry: cargo 36 | pass_filenames: false 37 | args: 38 | - build 39 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_try_shorthand=true 2 | use_field_init_shorthand=true 3 | -------------------------------------------------------------------------------- /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.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom", 28 | "once_cell", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "aho-corasick" 35 | version = "1.1.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 38 | dependencies = [ 39 | "memchr", 40 | ] 41 | 42 | [[package]] 43 | name = "allocator-api2" 44 | version = "0.2.20" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" 47 | 48 | [[package]] 49 | name = "android-tzdata" 50 | version = "0.1.1" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 53 | 54 | [[package]] 55 | name = "android_system_properties" 56 | version = "0.1.5" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 59 | dependencies = [ 60 | "libc", 61 | ] 62 | 63 | [[package]] 64 | name = "anstream" 65 | version = "0.6.18" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 68 | dependencies = [ 69 | "anstyle", 70 | "anstyle-parse", 71 | "anstyle-query", 72 | "anstyle-wincon", 73 | "colorchoice", 74 | "is_terminal_polyfill", 75 | "utf8parse", 76 | ] 77 | 78 | [[package]] 79 | name = "anstyle" 80 | version = "1.0.10" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 83 | 84 | [[package]] 85 | name = "anstyle-parse" 86 | version = "0.2.6" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 89 | dependencies = [ 90 | "utf8parse", 91 | ] 92 | 93 | [[package]] 94 | name = "anstyle-query" 95 | version = "1.1.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 98 | dependencies = [ 99 | "windows-sys 0.59.0", 100 | ] 101 | 102 | [[package]] 103 | name = "anstyle-wincon" 104 | version = "3.0.6" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 107 | dependencies = [ 108 | "anstyle", 109 | "windows-sys 0.59.0", 110 | ] 111 | 112 | [[package]] 113 | name = "async-broadcast" 114 | version = "0.7.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" 117 | dependencies = [ 118 | "event-listener", 119 | "event-listener-strategy", 120 | "futures-core", 121 | "pin-project-lite", 122 | ] 123 | 124 | [[package]] 125 | name = "async-stream" 126 | version = "0.3.6" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 129 | dependencies = [ 130 | "async-stream-impl", 131 | "futures-core", 132 | "pin-project-lite", 133 | ] 134 | 135 | [[package]] 136 | name = "async-stream-impl" 137 | version = "0.3.6" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 140 | dependencies = [ 141 | "proc-macro2", 142 | "quote", 143 | "syn", 144 | ] 145 | 146 | [[package]] 147 | name = "async-trait" 148 | version = "0.1.83" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 151 | dependencies = [ 152 | "proc-macro2", 153 | "quote", 154 | "syn", 155 | ] 156 | 157 | [[package]] 158 | name = "atomic-waker" 159 | version = "1.1.2" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 162 | 163 | [[package]] 164 | name = "autocfg" 165 | version = "1.4.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 168 | 169 | [[package]] 170 | name = "backoff" 171 | version = "0.4.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" 174 | dependencies = [ 175 | "getrandom", 176 | "instant", 177 | "rand", 178 | ] 179 | 180 | [[package]] 181 | name = "backtrace" 182 | version = "0.3.74" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 185 | dependencies = [ 186 | "addr2line", 187 | "cfg-if", 188 | "libc", 189 | "miniz_oxide", 190 | "object", 191 | "rustc-demangle", 192 | "windows-targets", 193 | ] 194 | 195 | [[package]] 196 | name = "base64" 197 | version = "0.21.7" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 200 | 201 | [[package]] 202 | name = "base64" 203 | version = "0.22.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 206 | 207 | [[package]] 208 | name = "bitflags" 209 | version = "1.3.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 212 | 213 | [[package]] 214 | name = "bitflags" 215 | version = "2.6.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 218 | 219 | [[package]] 220 | name = "block-buffer" 221 | version = "0.10.4" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 224 | dependencies = [ 225 | "generic-array", 226 | ] 227 | 228 | [[package]] 229 | name = "bumpalo" 230 | version = "3.16.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 233 | 234 | [[package]] 235 | name = "byteorder" 236 | version = "1.5.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 239 | 240 | [[package]] 241 | name = "bytes" 242 | version = "1.8.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 245 | 246 | [[package]] 247 | name = "cc" 248 | version = "1.2.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" 251 | dependencies = [ 252 | "shlex", 253 | ] 254 | 255 | [[package]] 256 | name = "cfg-if" 257 | version = "1.0.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 260 | 261 | [[package]] 262 | name = "chrono" 263 | version = "0.4.38" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 266 | dependencies = [ 267 | "android-tzdata", 268 | "iana-time-zone", 269 | "num-traits", 270 | "serde", 271 | "windows-targets", 272 | ] 273 | 274 | [[package]] 275 | name = "clap" 276 | version = "4.5.21" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" 279 | dependencies = [ 280 | "clap_builder", 281 | "clap_derive", 282 | ] 283 | 284 | [[package]] 285 | name = "clap_builder" 286 | version = "4.5.21" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" 289 | dependencies = [ 290 | "anstream", 291 | "anstyle", 292 | "clap_lex", 293 | "strsim", 294 | ] 295 | 296 | [[package]] 297 | name = "clap_derive" 298 | version = "4.5.18" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 301 | dependencies = [ 302 | "heck", 303 | "proc-macro2", 304 | "quote", 305 | "syn", 306 | ] 307 | 308 | [[package]] 309 | name = "clap_lex" 310 | version = "0.7.3" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" 313 | 314 | [[package]] 315 | name = "colorchoice" 316 | version = "1.0.3" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 319 | 320 | [[package]] 321 | name = "concurrent-queue" 322 | version = "2.5.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 325 | dependencies = [ 326 | "crossbeam-utils", 327 | ] 328 | 329 | [[package]] 330 | name = "core-foundation" 331 | version = "0.9.4" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 334 | dependencies = [ 335 | "core-foundation-sys", 336 | "libc", 337 | ] 338 | 339 | [[package]] 340 | name = "core-foundation-sys" 341 | version = "0.8.7" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 344 | 345 | [[package]] 346 | name = "cpufeatures" 347 | version = "0.2.15" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" 350 | dependencies = [ 351 | "libc", 352 | ] 353 | 354 | [[package]] 355 | name = "crossbeam-utils" 356 | version = "0.8.20" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 359 | 360 | [[package]] 361 | name = "crypto-common" 362 | version = "0.1.6" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 365 | dependencies = [ 366 | "generic-array", 367 | "typenum", 368 | ] 369 | 370 | [[package]] 371 | name = "darling" 372 | version = "0.20.10" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 375 | dependencies = [ 376 | "darling_core", 377 | "darling_macro", 378 | ] 379 | 380 | [[package]] 381 | name = "darling_core" 382 | version = "0.20.10" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 385 | dependencies = [ 386 | "fnv", 387 | "ident_case", 388 | "proc-macro2", 389 | "quote", 390 | "strsim", 391 | "syn", 392 | ] 393 | 394 | [[package]] 395 | name = "darling_macro" 396 | version = "0.20.10" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 399 | dependencies = [ 400 | "darling_core", 401 | "quote", 402 | "syn", 403 | ] 404 | 405 | [[package]] 406 | name = "deranged" 407 | version = "0.3.11" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 410 | dependencies = [ 411 | "powerfmt", 412 | "serde", 413 | ] 414 | 415 | [[package]] 416 | name = "digest" 417 | version = "0.10.7" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 420 | dependencies = [ 421 | "block-buffer", 422 | "crypto-common", 423 | ] 424 | 425 | [[package]] 426 | name = "displaydoc" 427 | version = "0.2.5" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 430 | dependencies = [ 431 | "proc-macro2", 432 | "quote", 433 | "syn", 434 | ] 435 | 436 | [[package]] 437 | name = "dotenvy" 438 | version = "0.15.7" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 441 | 442 | [[package]] 443 | name = "educe" 444 | version = "0.6.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" 447 | dependencies = [ 448 | "enum-ordinalize", 449 | "proc-macro2", 450 | "quote", 451 | "syn", 452 | ] 453 | 454 | [[package]] 455 | name = "either" 456 | version = "1.13.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 459 | 460 | [[package]] 461 | name = "encoding_rs" 462 | version = "0.8.35" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 465 | dependencies = [ 466 | "cfg-if", 467 | ] 468 | 469 | [[package]] 470 | name = "enum-ordinalize" 471 | version = "4.3.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" 474 | dependencies = [ 475 | "enum-ordinalize-derive", 476 | ] 477 | 478 | [[package]] 479 | name = "enum-ordinalize-derive" 480 | version = "4.3.1" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" 483 | dependencies = [ 484 | "proc-macro2", 485 | "quote", 486 | "syn", 487 | ] 488 | 489 | [[package]] 490 | name = "equivalent" 491 | version = "1.0.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 494 | 495 | [[package]] 496 | name = "errno" 497 | version = "0.3.9" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 500 | dependencies = [ 501 | "libc", 502 | "windows-sys 0.52.0", 503 | ] 504 | 505 | [[package]] 506 | name = "event-listener" 507 | version = "5.3.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" 510 | dependencies = [ 511 | "concurrent-queue", 512 | "parking", 513 | "pin-project-lite", 514 | ] 515 | 516 | [[package]] 517 | name = "event-listener-strategy" 518 | version = "0.5.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" 521 | dependencies = [ 522 | "event-listener", 523 | "pin-project-lite", 524 | ] 525 | 526 | [[package]] 527 | name = "fastrand" 528 | version = "2.2.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" 531 | 532 | [[package]] 533 | name = "fluent-uri" 534 | version = "0.1.4" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" 537 | dependencies = [ 538 | "bitflags 1.3.2", 539 | ] 540 | 541 | [[package]] 542 | name = "fnv" 543 | version = "1.0.7" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 546 | 547 | [[package]] 548 | name = "foreign-types" 549 | version = "0.3.2" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 552 | dependencies = [ 553 | "foreign-types-shared", 554 | ] 555 | 556 | [[package]] 557 | name = "foreign-types-shared" 558 | version = "0.1.1" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 561 | 562 | [[package]] 563 | name = "form_urlencoded" 564 | version = "1.2.1" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 567 | dependencies = [ 568 | "percent-encoding", 569 | ] 570 | 571 | [[package]] 572 | name = "futures" 573 | version = "0.3.31" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 576 | dependencies = [ 577 | "futures-channel", 578 | "futures-core", 579 | "futures-executor", 580 | "futures-io", 581 | "futures-sink", 582 | "futures-task", 583 | "futures-util", 584 | ] 585 | 586 | [[package]] 587 | name = "futures-channel" 588 | version = "0.3.31" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 591 | dependencies = [ 592 | "futures-core", 593 | "futures-sink", 594 | ] 595 | 596 | [[package]] 597 | name = "futures-core" 598 | version = "0.3.31" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 601 | 602 | [[package]] 603 | name = "futures-executor" 604 | version = "0.3.31" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 607 | dependencies = [ 608 | "futures-core", 609 | "futures-task", 610 | "futures-util", 611 | ] 612 | 613 | [[package]] 614 | name = "futures-io" 615 | version = "0.3.31" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 618 | 619 | [[package]] 620 | name = "futures-macro" 621 | version = "0.3.31" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 624 | dependencies = [ 625 | "proc-macro2", 626 | "quote", 627 | "syn", 628 | ] 629 | 630 | [[package]] 631 | name = "futures-sink" 632 | version = "0.3.31" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 635 | 636 | [[package]] 637 | name = "futures-task" 638 | version = "0.3.31" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 641 | 642 | [[package]] 643 | name = "futures-util" 644 | version = "0.3.31" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 647 | dependencies = [ 648 | "futures-channel", 649 | "futures-core", 650 | "futures-io", 651 | "futures-macro", 652 | "futures-sink", 653 | "futures-task", 654 | "memchr", 655 | "pin-project-lite", 656 | "pin-utils", 657 | "slab", 658 | ] 659 | 660 | [[package]] 661 | name = "generic-array" 662 | version = "0.14.7" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 665 | dependencies = [ 666 | "typenum", 667 | "version_check", 668 | ] 669 | 670 | [[package]] 671 | name = "getrandom" 672 | version = "0.2.15" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 675 | dependencies = [ 676 | "cfg-if", 677 | "libc", 678 | "wasi", 679 | ] 680 | 681 | [[package]] 682 | name = "gimli" 683 | version = "0.31.1" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 686 | 687 | [[package]] 688 | name = "h2" 689 | version = "0.4.6" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" 692 | dependencies = [ 693 | "atomic-waker", 694 | "bytes", 695 | "fnv", 696 | "futures-core", 697 | "futures-sink", 698 | "http", 699 | "indexmap 2.6.0", 700 | "slab", 701 | "tokio", 702 | "tokio-util", 703 | "tracing", 704 | ] 705 | 706 | [[package]] 707 | name = "hashbrown" 708 | version = "0.12.3" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 711 | 712 | [[package]] 713 | name = "hashbrown" 714 | version = "0.14.5" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 717 | dependencies = [ 718 | "ahash", 719 | "allocator-api2", 720 | ] 721 | 722 | [[package]] 723 | name = "hashbrown" 724 | version = "0.15.1" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" 727 | 728 | [[package]] 729 | name = "hcloud" 730 | version = "0.21.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "1f7b446f80cb00b49b795e423cdc013ee9d53f60ad9cc8bbcf94f27d7c2a2fdf" 733 | dependencies = [ 734 | "reqwest", 735 | "serde", 736 | "serde_json", 737 | "serde_with", 738 | "url", 739 | "uuid", 740 | ] 741 | 742 | [[package]] 743 | name = "headers" 744 | version = "0.4.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" 747 | dependencies = [ 748 | "base64 0.21.7", 749 | "bytes", 750 | "headers-core", 751 | "http", 752 | "httpdate", 753 | "mime", 754 | "sha1", 755 | ] 756 | 757 | [[package]] 758 | name = "headers-core" 759 | version = "0.3.0" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" 762 | dependencies = [ 763 | "http", 764 | ] 765 | 766 | [[package]] 767 | name = "heck" 768 | version = "0.5.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 771 | 772 | [[package]] 773 | name = "hermit-abi" 774 | version = "0.3.9" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 777 | 778 | [[package]] 779 | name = "hex" 780 | version = "0.4.3" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 783 | 784 | [[package]] 785 | name = "home" 786 | version = "0.5.9" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 789 | dependencies = [ 790 | "windows-sys 0.52.0", 791 | ] 792 | 793 | [[package]] 794 | name = "http" 795 | version = "1.1.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 798 | dependencies = [ 799 | "bytes", 800 | "fnv", 801 | "itoa", 802 | ] 803 | 804 | [[package]] 805 | name = "http-body" 806 | version = "1.0.1" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 809 | dependencies = [ 810 | "bytes", 811 | "http", 812 | ] 813 | 814 | [[package]] 815 | name = "http-body-util" 816 | version = "0.1.2" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 819 | dependencies = [ 820 | "bytes", 821 | "futures-util", 822 | "http", 823 | "http-body", 824 | "pin-project-lite", 825 | ] 826 | 827 | [[package]] 828 | name = "httparse" 829 | version = "1.9.5" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 832 | 833 | [[package]] 834 | name = "httpdate" 835 | version = "1.0.3" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 838 | 839 | [[package]] 840 | name = "hyper" 841 | version = "1.5.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" 844 | dependencies = [ 845 | "bytes", 846 | "futures-channel", 847 | "futures-util", 848 | "h2", 849 | "http", 850 | "http-body", 851 | "httparse", 852 | "itoa", 853 | "pin-project-lite", 854 | "smallvec", 855 | "tokio", 856 | "want", 857 | ] 858 | 859 | [[package]] 860 | name = "hyper-http-proxy" 861 | version = "1.0.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "5d06dbdfbacf34d996c6fb540a71a684a7aae9056c71951163af8a8a4c07b9a4" 864 | dependencies = [ 865 | "bytes", 866 | "futures-util", 867 | "headers", 868 | "http", 869 | "hyper", 870 | "hyper-rustls", 871 | "hyper-util", 872 | "pin-project-lite", 873 | "rustls-native-certs 0.7.3", 874 | "tokio", 875 | "tokio-rustls", 876 | "tower-service", 877 | ] 878 | 879 | [[package]] 880 | name = "hyper-rustls" 881 | version = "0.27.3" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" 884 | dependencies = [ 885 | "futures-util", 886 | "http", 887 | "hyper", 888 | "hyper-util", 889 | "log", 890 | "rustls", 891 | "rustls-native-certs 0.8.0", 892 | "rustls-pki-types", 893 | "tokio", 894 | "tokio-rustls", 895 | "tower-service", 896 | ] 897 | 898 | [[package]] 899 | name = "hyper-timeout" 900 | version = "0.5.2" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" 903 | dependencies = [ 904 | "hyper", 905 | "hyper-util", 906 | "pin-project-lite", 907 | "tokio", 908 | "tower-service", 909 | ] 910 | 911 | [[package]] 912 | name = "hyper-tls" 913 | version = "0.6.0" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 916 | dependencies = [ 917 | "bytes", 918 | "http-body-util", 919 | "hyper", 920 | "hyper-util", 921 | "native-tls", 922 | "tokio", 923 | "tokio-native-tls", 924 | "tower-service", 925 | ] 926 | 927 | [[package]] 928 | name = "hyper-util" 929 | version = "0.1.10" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 932 | dependencies = [ 933 | "bytes", 934 | "futures-channel", 935 | "futures-util", 936 | "http", 937 | "http-body", 938 | "hyper", 939 | "pin-project-lite", 940 | "socket2", 941 | "tokio", 942 | "tower-service", 943 | "tracing", 944 | ] 945 | 946 | [[package]] 947 | name = "iana-time-zone" 948 | version = "0.1.61" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 951 | dependencies = [ 952 | "android_system_properties", 953 | "core-foundation-sys", 954 | "iana-time-zone-haiku", 955 | "js-sys", 956 | "wasm-bindgen", 957 | "windows-core", 958 | ] 959 | 960 | [[package]] 961 | name = "iana-time-zone-haiku" 962 | version = "0.1.2" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 965 | dependencies = [ 966 | "cc", 967 | ] 968 | 969 | [[package]] 970 | name = "icu_collections" 971 | version = "1.5.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 974 | dependencies = [ 975 | "displaydoc", 976 | "yoke", 977 | "zerofrom", 978 | "zerovec", 979 | ] 980 | 981 | [[package]] 982 | name = "icu_locid" 983 | version = "1.5.0" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 986 | dependencies = [ 987 | "displaydoc", 988 | "litemap", 989 | "tinystr", 990 | "writeable", 991 | "zerovec", 992 | ] 993 | 994 | [[package]] 995 | name = "icu_locid_transform" 996 | version = "1.5.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 999 | dependencies = [ 1000 | "displaydoc", 1001 | "icu_locid", 1002 | "icu_locid_transform_data", 1003 | "icu_provider", 1004 | "tinystr", 1005 | "zerovec", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "icu_locid_transform_data" 1010 | version = "1.5.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 1013 | 1014 | [[package]] 1015 | name = "icu_normalizer" 1016 | version = "1.5.0" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 1019 | dependencies = [ 1020 | "displaydoc", 1021 | "icu_collections", 1022 | "icu_normalizer_data", 1023 | "icu_properties", 1024 | "icu_provider", 1025 | "smallvec", 1026 | "utf16_iter", 1027 | "utf8_iter", 1028 | "write16", 1029 | "zerovec", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "icu_normalizer_data" 1034 | version = "1.5.0" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 1037 | 1038 | [[package]] 1039 | name = "icu_properties" 1040 | version = "1.5.1" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 1043 | dependencies = [ 1044 | "displaydoc", 1045 | "icu_collections", 1046 | "icu_locid_transform", 1047 | "icu_properties_data", 1048 | "icu_provider", 1049 | "tinystr", 1050 | "zerovec", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "icu_properties_data" 1055 | version = "1.5.0" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 1058 | 1059 | [[package]] 1060 | name = "icu_provider" 1061 | version = "1.5.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 1064 | dependencies = [ 1065 | "displaydoc", 1066 | "icu_locid", 1067 | "icu_provider_macros", 1068 | "stable_deref_trait", 1069 | "tinystr", 1070 | "writeable", 1071 | "yoke", 1072 | "zerofrom", 1073 | "zerovec", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "icu_provider_macros" 1078 | version = "1.5.0" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 1081 | dependencies = [ 1082 | "proc-macro2", 1083 | "quote", 1084 | "syn", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "ident_case" 1089 | version = "1.0.1" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 1092 | 1093 | [[package]] 1094 | name = "idna" 1095 | version = "1.0.3" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1098 | dependencies = [ 1099 | "idna_adapter", 1100 | "smallvec", 1101 | "utf8_iter", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "idna_adapter" 1106 | version = "1.2.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 1109 | dependencies = [ 1110 | "icu_normalizer", 1111 | "icu_properties", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "indexmap" 1116 | version = "1.9.3" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1119 | dependencies = [ 1120 | "autocfg", 1121 | "hashbrown 0.12.3", 1122 | "serde", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "indexmap" 1127 | version = "2.6.0" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 1130 | dependencies = [ 1131 | "equivalent", 1132 | "hashbrown 0.15.1", 1133 | "serde", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "instant" 1138 | version = "0.1.13" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 1141 | dependencies = [ 1142 | "cfg-if", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "ipnet" 1147 | version = "2.10.1" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 1150 | 1151 | [[package]] 1152 | name = "is_terminal_polyfill" 1153 | version = "1.70.1" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1156 | 1157 | [[package]] 1158 | name = "itoa" 1159 | version = "1.0.11" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 1162 | 1163 | [[package]] 1164 | name = "js-sys" 1165 | version = "0.3.72" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 1168 | dependencies = [ 1169 | "wasm-bindgen", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "json-patch" 1174 | version = "2.0.0" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" 1177 | dependencies = [ 1178 | "jsonptr", 1179 | "serde", 1180 | "serde_json", 1181 | "thiserror 1.0.69", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "jsonpath-rust" 1186 | version = "0.5.1" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "19d8fe85bd70ff715f31ce8c739194b423d79811a19602115d611a3ec85d6200" 1189 | dependencies = [ 1190 | "lazy_static", 1191 | "once_cell", 1192 | "pest", 1193 | "pest_derive", 1194 | "regex", 1195 | "serde_json", 1196 | "thiserror 1.0.69", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "jsonptr" 1201 | version = "0.4.7" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" 1204 | dependencies = [ 1205 | "fluent-uri", 1206 | "serde", 1207 | "serde_json", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "k8s-openapi" 1212 | version = "0.23.0" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "9c8847402328d8301354c94d605481f25a6bdc1ed65471fd96af8eca71141b13" 1215 | dependencies = [ 1216 | "base64 0.22.1", 1217 | "chrono", 1218 | "serde", 1219 | "serde-value", 1220 | "serde_json", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "kube" 1225 | version = "0.96.0" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "efffeb3df0bd4ef3e5d65044573499c0e4889b988070b08c50b25b1329289a1f" 1228 | dependencies = [ 1229 | "k8s-openapi", 1230 | "kube-client", 1231 | "kube-core", 1232 | "kube-runtime", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "kube-client" 1237 | version = "0.96.0" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "8bf471ece8ff8d24735ce78dac4d091e9fcb8d74811aeb6b75de4d1c3f5de0f1" 1240 | dependencies = [ 1241 | "base64 0.22.1", 1242 | "bytes", 1243 | "chrono", 1244 | "either", 1245 | "futures", 1246 | "home", 1247 | "http", 1248 | "http-body", 1249 | "http-body-util", 1250 | "hyper", 1251 | "hyper-http-proxy", 1252 | "hyper-rustls", 1253 | "hyper-timeout", 1254 | "hyper-util", 1255 | "jsonpath-rust", 1256 | "k8s-openapi", 1257 | "kube-core", 1258 | "pem", 1259 | "rustls", 1260 | "rustls-pemfile", 1261 | "secrecy", 1262 | "serde", 1263 | "serde_json", 1264 | "serde_yaml", 1265 | "thiserror 1.0.69", 1266 | "tokio", 1267 | "tokio-util", 1268 | "tower", 1269 | "tower-http", 1270 | "tracing", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "kube-core" 1275 | version = "0.96.0" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "f42346d30bb34d1d7adc5c549b691bce7aa3a1e60254e68fab7e2d7b26fe3d77" 1278 | dependencies = [ 1279 | "chrono", 1280 | "form_urlencoded", 1281 | "http", 1282 | "json-patch", 1283 | "k8s-openapi", 1284 | "serde", 1285 | "serde-value", 1286 | "serde_json", 1287 | "thiserror 1.0.69", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "kube-runtime" 1292 | version = "0.96.0" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "d3fbf1f6ffa98e65f1d2a9a69338bb60605d46be7edf00237784b89e62c9bd44" 1295 | dependencies = [ 1296 | "ahash", 1297 | "async-broadcast", 1298 | "async-stream", 1299 | "async-trait", 1300 | "backoff", 1301 | "educe", 1302 | "futures", 1303 | "hashbrown 0.14.5", 1304 | "json-patch", 1305 | "jsonptr", 1306 | "k8s-openapi", 1307 | "kube-client", 1308 | "parking_lot", 1309 | "pin-project", 1310 | "serde", 1311 | "serde_json", 1312 | "thiserror 1.0.69", 1313 | "tokio", 1314 | "tokio-util", 1315 | "tracing", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "lazy_static" 1320 | version = "1.5.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1323 | 1324 | [[package]] 1325 | name = "libc" 1326 | version = "0.2.162" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" 1329 | 1330 | [[package]] 1331 | name = "linux-raw-sys" 1332 | version = "0.4.14" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 1335 | 1336 | [[package]] 1337 | name = "litemap" 1338 | version = "0.7.3" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" 1341 | 1342 | [[package]] 1343 | name = "lock_api" 1344 | version = "0.4.12" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1347 | dependencies = [ 1348 | "autocfg", 1349 | "scopeguard", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "log" 1354 | version = "0.4.22" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 1357 | 1358 | [[package]] 1359 | name = "memchr" 1360 | version = "2.7.4" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1363 | 1364 | [[package]] 1365 | name = "mime" 1366 | version = "0.3.17" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1369 | 1370 | [[package]] 1371 | name = "mime_guess" 1372 | version = "2.0.5" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1375 | dependencies = [ 1376 | "mime", 1377 | "unicase", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "miniz_oxide" 1382 | version = "0.8.0" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 1385 | dependencies = [ 1386 | "adler2", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "mio" 1391 | version = "1.0.2" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 1394 | dependencies = [ 1395 | "hermit-abi", 1396 | "libc", 1397 | "wasi", 1398 | "windows-sys 0.52.0", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "native-tls" 1403 | version = "0.2.12" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 1406 | dependencies = [ 1407 | "libc", 1408 | "log", 1409 | "openssl", 1410 | "openssl-probe", 1411 | "openssl-sys", 1412 | "schannel", 1413 | "security-framework", 1414 | "security-framework-sys", 1415 | "tempfile", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "nu-ansi-term" 1420 | version = "0.46.0" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1423 | dependencies = [ 1424 | "overload", 1425 | "winapi", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "num-conv" 1430 | version = "0.1.0" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1433 | 1434 | [[package]] 1435 | name = "num-traits" 1436 | version = "0.2.19" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1439 | dependencies = [ 1440 | "autocfg", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "object" 1445 | version = "0.36.5" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 1448 | dependencies = [ 1449 | "memchr", 1450 | ] 1451 | 1452 | [[package]] 1453 | name = "once_cell" 1454 | version = "1.20.2" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 1457 | 1458 | [[package]] 1459 | name = "openssl" 1460 | version = "0.10.70" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" 1463 | dependencies = [ 1464 | "bitflags 2.6.0", 1465 | "cfg-if", 1466 | "foreign-types", 1467 | "libc", 1468 | "once_cell", 1469 | "openssl-macros", 1470 | "openssl-sys", 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "openssl-macros" 1475 | version = "0.1.1" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1478 | dependencies = [ 1479 | "proc-macro2", 1480 | "quote", 1481 | "syn", 1482 | ] 1483 | 1484 | [[package]] 1485 | name = "openssl-probe" 1486 | version = "0.1.5" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1489 | 1490 | [[package]] 1491 | name = "openssl-sys" 1492 | version = "0.9.105" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" 1495 | dependencies = [ 1496 | "cc", 1497 | "libc", 1498 | "pkg-config", 1499 | "vcpkg", 1500 | ] 1501 | 1502 | [[package]] 1503 | name = "ordered-float" 1504 | version = "2.10.1" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" 1507 | dependencies = [ 1508 | "num-traits", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "overload" 1513 | version = "0.1.1" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1516 | 1517 | [[package]] 1518 | name = "parking" 1519 | version = "2.2.1" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1522 | 1523 | [[package]] 1524 | name = "parking_lot" 1525 | version = "0.12.3" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1528 | dependencies = [ 1529 | "lock_api", 1530 | "parking_lot_core", 1531 | ] 1532 | 1533 | [[package]] 1534 | name = "parking_lot_core" 1535 | version = "0.9.10" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1538 | dependencies = [ 1539 | "cfg-if", 1540 | "libc", 1541 | "redox_syscall", 1542 | "smallvec", 1543 | "windows-targets", 1544 | ] 1545 | 1546 | [[package]] 1547 | name = "pem" 1548 | version = "3.0.4" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" 1551 | dependencies = [ 1552 | "base64 0.22.1", 1553 | "serde", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "percent-encoding" 1558 | version = "2.3.1" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1561 | 1562 | [[package]] 1563 | name = "pest" 1564 | version = "2.7.14" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" 1567 | dependencies = [ 1568 | "memchr", 1569 | "thiserror 1.0.69", 1570 | "ucd-trie", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "pest_derive" 1575 | version = "2.7.14" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" 1578 | dependencies = [ 1579 | "pest", 1580 | "pest_generator", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "pest_generator" 1585 | version = "2.7.14" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" 1588 | dependencies = [ 1589 | "pest", 1590 | "pest_meta", 1591 | "proc-macro2", 1592 | "quote", 1593 | "syn", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "pest_meta" 1598 | version = "2.7.14" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" 1601 | dependencies = [ 1602 | "once_cell", 1603 | "pest", 1604 | "sha2", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "pin-project" 1609 | version = "1.1.7" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" 1612 | dependencies = [ 1613 | "pin-project-internal", 1614 | ] 1615 | 1616 | [[package]] 1617 | name = "pin-project-internal" 1618 | version = "1.1.7" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" 1621 | dependencies = [ 1622 | "proc-macro2", 1623 | "quote", 1624 | "syn", 1625 | ] 1626 | 1627 | [[package]] 1628 | name = "pin-project-lite" 1629 | version = "0.2.15" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 1632 | 1633 | [[package]] 1634 | name = "pin-utils" 1635 | version = "0.1.0" 1636 | source = "registry+https://github.com/rust-lang/crates.io-index" 1637 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1638 | 1639 | [[package]] 1640 | name = "pkg-config" 1641 | version = "0.3.31" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1644 | 1645 | [[package]] 1646 | name = "powerfmt" 1647 | version = "0.2.0" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1650 | 1651 | [[package]] 1652 | name = "ppv-lite86" 1653 | version = "0.2.20" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1656 | dependencies = [ 1657 | "zerocopy", 1658 | ] 1659 | 1660 | [[package]] 1661 | name = "proc-macro2" 1662 | version = "1.0.89" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 1665 | dependencies = [ 1666 | "unicode-ident", 1667 | ] 1668 | 1669 | [[package]] 1670 | name = "quote" 1671 | version = "1.0.37" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 1674 | dependencies = [ 1675 | "proc-macro2", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "rand" 1680 | version = "0.8.5" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1683 | dependencies = [ 1684 | "libc", 1685 | "rand_chacha", 1686 | "rand_core", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "rand_chacha" 1691 | version = "0.3.1" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1694 | dependencies = [ 1695 | "ppv-lite86", 1696 | "rand_core", 1697 | ] 1698 | 1699 | [[package]] 1700 | name = "rand_core" 1701 | version = "0.6.4" 1702 | source = "registry+https://github.com/rust-lang/crates.io-index" 1703 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1704 | dependencies = [ 1705 | "getrandom", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "redox_syscall" 1710 | version = "0.5.7" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 1713 | dependencies = [ 1714 | "bitflags 2.6.0", 1715 | ] 1716 | 1717 | [[package]] 1718 | name = "regex" 1719 | version = "1.11.1" 1720 | source = "registry+https://github.com/rust-lang/crates.io-index" 1721 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1722 | dependencies = [ 1723 | "aho-corasick", 1724 | "memchr", 1725 | "regex-automata", 1726 | "regex-syntax", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "regex-automata" 1731 | version = "0.4.9" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1734 | dependencies = [ 1735 | "aho-corasick", 1736 | "memchr", 1737 | "regex-syntax", 1738 | ] 1739 | 1740 | [[package]] 1741 | name = "regex-syntax" 1742 | version = "0.8.5" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1745 | 1746 | [[package]] 1747 | name = "reqwest" 1748 | version = "0.12.9" 1749 | source = "registry+https://github.com/rust-lang/crates.io-index" 1750 | checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" 1751 | dependencies = [ 1752 | "base64 0.22.1", 1753 | "bytes", 1754 | "encoding_rs", 1755 | "futures-core", 1756 | "futures-util", 1757 | "h2", 1758 | "http", 1759 | "http-body", 1760 | "http-body-util", 1761 | "hyper", 1762 | "hyper-rustls", 1763 | "hyper-tls", 1764 | "hyper-util", 1765 | "ipnet", 1766 | "js-sys", 1767 | "log", 1768 | "mime", 1769 | "mime_guess", 1770 | "native-tls", 1771 | "once_cell", 1772 | "percent-encoding", 1773 | "pin-project-lite", 1774 | "rustls-pemfile", 1775 | "serde", 1776 | "serde_json", 1777 | "serde_urlencoded", 1778 | "sync_wrapper 1.0.1", 1779 | "system-configuration", 1780 | "tokio", 1781 | "tokio-native-tls", 1782 | "tower-service", 1783 | "url", 1784 | "wasm-bindgen", 1785 | "wasm-bindgen-futures", 1786 | "web-sys", 1787 | "windows-registry", 1788 | ] 1789 | 1790 | [[package]] 1791 | name = "ring" 1792 | version = "0.17.8" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 1795 | dependencies = [ 1796 | "cc", 1797 | "cfg-if", 1798 | "getrandom", 1799 | "libc", 1800 | "spin", 1801 | "untrusted", 1802 | "windows-sys 0.52.0", 1803 | ] 1804 | 1805 | [[package]] 1806 | name = "robotlb" 1807 | version = "0.0.0" 1808 | dependencies = [ 1809 | "clap", 1810 | "dotenvy", 1811 | "futures", 1812 | "hcloud", 1813 | "k8s-openapi", 1814 | "kube", 1815 | "thiserror 2.0.3", 1816 | "tikv-jemallocator", 1817 | "tokio", 1818 | "tracing", 1819 | "tracing-subscriber", 1820 | ] 1821 | 1822 | [[package]] 1823 | name = "rustc-demangle" 1824 | version = "0.1.24" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1827 | 1828 | [[package]] 1829 | name = "rustix" 1830 | version = "0.38.40" 1831 | source = "registry+https://github.com/rust-lang/crates.io-index" 1832 | checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" 1833 | dependencies = [ 1834 | "bitflags 2.6.0", 1835 | "errno", 1836 | "libc", 1837 | "linux-raw-sys", 1838 | "windows-sys 0.52.0", 1839 | ] 1840 | 1841 | [[package]] 1842 | name = "rustls" 1843 | version = "0.23.18" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" 1846 | dependencies = [ 1847 | "log", 1848 | "once_cell", 1849 | "ring", 1850 | "rustls-pki-types", 1851 | "rustls-webpki", 1852 | "subtle", 1853 | "zeroize", 1854 | ] 1855 | 1856 | [[package]] 1857 | name = "rustls-native-certs" 1858 | version = "0.7.3" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" 1861 | dependencies = [ 1862 | "openssl-probe", 1863 | "rustls-pemfile", 1864 | "rustls-pki-types", 1865 | "schannel", 1866 | "security-framework", 1867 | ] 1868 | 1869 | [[package]] 1870 | name = "rustls-native-certs" 1871 | version = "0.8.0" 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" 1873 | checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" 1874 | dependencies = [ 1875 | "openssl-probe", 1876 | "rustls-pemfile", 1877 | "rustls-pki-types", 1878 | "schannel", 1879 | "security-framework", 1880 | ] 1881 | 1882 | [[package]] 1883 | name = "rustls-pemfile" 1884 | version = "2.2.0" 1885 | source = "registry+https://github.com/rust-lang/crates.io-index" 1886 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1887 | dependencies = [ 1888 | "rustls-pki-types", 1889 | ] 1890 | 1891 | [[package]] 1892 | name = "rustls-pki-types" 1893 | version = "1.10.0" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" 1896 | 1897 | [[package]] 1898 | name = "rustls-webpki" 1899 | version = "0.102.8" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1902 | dependencies = [ 1903 | "ring", 1904 | "rustls-pki-types", 1905 | "untrusted", 1906 | ] 1907 | 1908 | [[package]] 1909 | name = "ryu" 1910 | version = "1.0.18" 1911 | source = "registry+https://github.com/rust-lang/crates.io-index" 1912 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1913 | 1914 | [[package]] 1915 | name = "schannel" 1916 | version = "0.1.26" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" 1919 | dependencies = [ 1920 | "windows-sys 0.59.0", 1921 | ] 1922 | 1923 | [[package]] 1924 | name = "scopeguard" 1925 | version = "1.2.0" 1926 | source = "registry+https://github.com/rust-lang/crates.io-index" 1927 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1928 | 1929 | [[package]] 1930 | name = "secrecy" 1931 | version = "0.10.3" 1932 | source = "registry+https://github.com/rust-lang/crates.io-index" 1933 | checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" 1934 | dependencies = [ 1935 | "zeroize", 1936 | ] 1937 | 1938 | [[package]] 1939 | name = "security-framework" 1940 | version = "2.11.1" 1941 | source = "registry+https://github.com/rust-lang/crates.io-index" 1942 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1943 | dependencies = [ 1944 | "bitflags 2.6.0", 1945 | "core-foundation", 1946 | "core-foundation-sys", 1947 | "libc", 1948 | "security-framework-sys", 1949 | ] 1950 | 1951 | [[package]] 1952 | name = "security-framework-sys" 1953 | version = "2.12.1" 1954 | source = "registry+https://github.com/rust-lang/crates.io-index" 1955 | checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" 1956 | dependencies = [ 1957 | "core-foundation-sys", 1958 | "libc", 1959 | ] 1960 | 1961 | [[package]] 1962 | name = "serde" 1963 | version = "1.0.215" 1964 | source = "registry+https://github.com/rust-lang/crates.io-index" 1965 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 1966 | dependencies = [ 1967 | "serde_derive", 1968 | ] 1969 | 1970 | [[package]] 1971 | name = "serde-value" 1972 | version = "0.7.0" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" 1975 | dependencies = [ 1976 | "ordered-float", 1977 | "serde", 1978 | ] 1979 | 1980 | [[package]] 1981 | name = "serde_derive" 1982 | version = "1.0.215" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 1985 | dependencies = [ 1986 | "proc-macro2", 1987 | "quote", 1988 | "syn", 1989 | ] 1990 | 1991 | [[package]] 1992 | name = "serde_json" 1993 | version = "1.0.132" 1994 | source = "registry+https://github.com/rust-lang/crates.io-index" 1995 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 1996 | dependencies = [ 1997 | "itoa", 1998 | "memchr", 1999 | "ryu", 2000 | "serde", 2001 | ] 2002 | 2003 | [[package]] 2004 | name = "serde_urlencoded" 2005 | version = "0.7.1" 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" 2007 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2008 | dependencies = [ 2009 | "form_urlencoded", 2010 | "itoa", 2011 | "ryu", 2012 | "serde", 2013 | ] 2014 | 2015 | [[package]] 2016 | name = "serde_with" 2017 | version = "3.11.0" 2018 | source = "registry+https://github.com/rust-lang/crates.io-index" 2019 | checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" 2020 | dependencies = [ 2021 | "base64 0.22.1", 2022 | "chrono", 2023 | "hex", 2024 | "indexmap 1.9.3", 2025 | "indexmap 2.6.0", 2026 | "serde", 2027 | "serde_derive", 2028 | "serde_json", 2029 | "serde_with_macros", 2030 | "time", 2031 | ] 2032 | 2033 | [[package]] 2034 | name = "serde_with_macros" 2035 | version = "3.11.0" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" 2038 | dependencies = [ 2039 | "darling", 2040 | "proc-macro2", 2041 | "quote", 2042 | "syn", 2043 | ] 2044 | 2045 | [[package]] 2046 | name = "serde_yaml" 2047 | version = "0.9.34+deprecated" 2048 | source = "registry+https://github.com/rust-lang/crates.io-index" 2049 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 2050 | dependencies = [ 2051 | "indexmap 2.6.0", 2052 | "itoa", 2053 | "ryu", 2054 | "serde", 2055 | "unsafe-libyaml", 2056 | ] 2057 | 2058 | [[package]] 2059 | name = "sha1" 2060 | version = "0.10.6" 2061 | source = "registry+https://github.com/rust-lang/crates.io-index" 2062 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2063 | dependencies = [ 2064 | "cfg-if", 2065 | "cpufeatures", 2066 | "digest", 2067 | ] 2068 | 2069 | [[package]] 2070 | name = "sha2" 2071 | version = "0.10.8" 2072 | source = "registry+https://github.com/rust-lang/crates.io-index" 2073 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 2074 | dependencies = [ 2075 | "cfg-if", 2076 | "cpufeatures", 2077 | "digest", 2078 | ] 2079 | 2080 | [[package]] 2081 | name = "sharded-slab" 2082 | version = "0.1.7" 2083 | source = "registry+https://github.com/rust-lang/crates.io-index" 2084 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 2085 | dependencies = [ 2086 | "lazy_static", 2087 | ] 2088 | 2089 | [[package]] 2090 | name = "shlex" 2091 | version = "1.3.0" 2092 | source = "registry+https://github.com/rust-lang/crates.io-index" 2093 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2094 | 2095 | [[package]] 2096 | name = "signal-hook-registry" 2097 | version = "1.4.2" 2098 | source = "registry+https://github.com/rust-lang/crates.io-index" 2099 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 2100 | dependencies = [ 2101 | "libc", 2102 | ] 2103 | 2104 | [[package]] 2105 | name = "slab" 2106 | version = "0.4.9" 2107 | source = "registry+https://github.com/rust-lang/crates.io-index" 2108 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 2109 | dependencies = [ 2110 | "autocfg", 2111 | ] 2112 | 2113 | [[package]] 2114 | name = "smallvec" 2115 | version = "1.13.2" 2116 | source = "registry+https://github.com/rust-lang/crates.io-index" 2117 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 2118 | 2119 | [[package]] 2120 | name = "socket2" 2121 | version = "0.5.7" 2122 | source = "registry+https://github.com/rust-lang/crates.io-index" 2123 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 2124 | dependencies = [ 2125 | "libc", 2126 | "windows-sys 0.52.0", 2127 | ] 2128 | 2129 | [[package]] 2130 | name = "spin" 2131 | version = "0.9.8" 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" 2133 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2134 | 2135 | [[package]] 2136 | name = "stable_deref_trait" 2137 | version = "1.2.0" 2138 | source = "registry+https://github.com/rust-lang/crates.io-index" 2139 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2140 | 2141 | [[package]] 2142 | name = "strsim" 2143 | version = "0.11.1" 2144 | source = "registry+https://github.com/rust-lang/crates.io-index" 2145 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2146 | 2147 | [[package]] 2148 | name = "subtle" 2149 | version = "2.6.1" 2150 | source = "registry+https://github.com/rust-lang/crates.io-index" 2151 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2152 | 2153 | [[package]] 2154 | name = "syn" 2155 | version = "2.0.87" 2156 | source = "registry+https://github.com/rust-lang/crates.io-index" 2157 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 2158 | dependencies = [ 2159 | "proc-macro2", 2160 | "quote", 2161 | "unicode-ident", 2162 | ] 2163 | 2164 | [[package]] 2165 | name = "sync_wrapper" 2166 | version = "0.1.2" 2167 | source = "registry+https://github.com/rust-lang/crates.io-index" 2168 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 2169 | 2170 | [[package]] 2171 | name = "sync_wrapper" 2172 | version = "1.0.1" 2173 | source = "registry+https://github.com/rust-lang/crates.io-index" 2174 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 2175 | dependencies = [ 2176 | "futures-core", 2177 | ] 2178 | 2179 | [[package]] 2180 | name = "synstructure" 2181 | version = "0.13.1" 2182 | source = "registry+https://github.com/rust-lang/crates.io-index" 2183 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 2184 | dependencies = [ 2185 | "proc-macro2", 2186 | "quote", 2187 | "syn", 2188 | ] 2189 | 2190 | [[package]] 2191 | name = "system-configuration" 2192 | version = "0.6.1" 2193 | source = "registry+https://github.com/rust-lang/crates.io-index" 2194 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2195 | dependencies = [ 2196 | "bitflags 2.6.0", 2197 | "core-foundation", 2198 | "system-configuration-sys", 2199 | ] 2200 | 2201 | [[package]] 2202 | name = "system-configuration-sys" 2203 | version = "0.6.0" 2204 | source = "registry+https://github.com/rust-lang/crates.io-index" 2205 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2206 | dependencies = [ 2207 | "core-foundation-sys", 2208 | "libc", 2209 | ] 2210 | 2211 | [[package]] 2212 | name = "tempfile" 2213 | version = "3.14.0" 2214 | source = "registry+https://github.com/rust-lang/crates.io-index" 2215 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 2216 | dependencies = [ 2217 | "cfg-if", 2218 | "fastrand", 2219 | "once_cell", 2220 | "rustix", 2221 | "windows-sys 0.59.0", 2222 | ] 2223 | 2224 | [[package]] 2225 | name = "thiserror" 2226 | version = "1.0.69" 2227 | source = "registry+https://github.com/rust-lang/crates.io-index" 2228 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2229 | dependencies = [ 2230 | "thiserror-impl 1.0.69", 2231 | ] 2232 | 2233 | [[package]] 2234 | name = "thiserror" 2235 | version = "2.0.3" 2236 | source = "registry+https://github.com/rust-lang/crates.io-index" 2237 | checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" 2238 | dependencies = [ 2239 | "thiserror-impl 2.0.3", 2240 | ] 2241 | 2242 | [[package]] 2243 | name = "thiserror-impl" 2244 | version = "1.0.69" 2245 | source = "registry+https://github.com/rust-lang/crates.io-index" 2246 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2247 | dependencies = [ 2248 | "proc-macro2", 2249 | "quote", 2250 | "syn", 2251 | ] 2252 | 2253 | [[package]] 2254 | name = "thiserror-impl" 2255 | version = "2.0.3" 2256 | source = "registry+https://github.com/rust-lang/crates.io-index" 2257 | checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" 2258 | dependencies = [ 2259 | "proc-macro2", 2260 | "quote", 2261 | "syn", 2262 | ] 2263 | 2264 | [[package]] 2265 | name = "thread_local" 2266 | version = "1.1.8" 2267 | source = "registry+https://github.com/rust-lang/crates.io-index" 2268 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 2269 | dependencies = [ 2270 | "cfg-if", 2271 | "once_cell", 2272 | ] 2273 | 2274 | [[package]] 2275 | name = "tikv-jemalloc-sys" 2276 | version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" 2277 | source = "registry+https://github.com/rust-lang/crates.io-index" 2278 | checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" 2279 | dependencies = [ 2280 | "cc", 2281 | "libc", 2282 | ] 2283 | 2284 | [[package]] 2285 | name = "tikv-jemallocator" 2286 | version = "0.6.0" 2287 | source = "registry+https://github.com/rust-lang/crates.io-index" 2288 | checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" 2289 | dependencies = [ 2290 | "libc", 2291 | "tikv-jemalloc-sys", 2292 | ] 2293 | 2294 | [[package]] 2295 | name = "time" 2296 | version = "0.3.36" 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" 2298 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 2299 | dependencies = [ 2300 | "deranged", 2301 | "itoa", 2302 | "num-conv", 2303 | "powerfmt", 2304 | "serde", 2305 | "time-core", 2306 | "time-macros", 2307 | ] 2308 | 2309 | [[package]] 2310 | name = "time-core" 2311 | version = "0.1.2" 2312 | source = "registry+https://github.com/rust-lang/crates.io-index" 2313 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 2314 | 2315 | [[package]] 2316 | name = "time-macros" 2317 | version = "0.2.18" 2318 | source = "registry+https://github.com/rust-lang/crates.io-index" 2319 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 2320 | dependencies = [ 2321 | "num-conv", 2322 | "time-core", 2323 | ] 2324 | 2325 | [[package]] 2326 | name = "tinystr" 2327 | version = "0.7.6" 2328 | source = "registry+https://github.com/rust-lang/crates.io-index" 2329 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 2330 | dependencies = [ 2331 | "displaydoc", 2332 | "zerovec", 2333 | ] 2334 | 2335 | [[package]] 2336 | name = "tokio" 2337 | version = "1.41.1" 2338 | source = "registry+https://github.com/rust-lang/crates.io-index" 2339 | checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" 2340 | dependencies = [ 2341 | "backtrace", 2342 | "bytes", 2343 | "libc", 2344 | "mio", 2345 | "pin-project-lite", 2346 | "signal-hook-registry", 2347 | "socket2", 2348 | "tokio-macros", 2349 | "windows-sys 0.52.0", 2350 | ] 2351 | 2352 | [[package]] 2353 | name = "tokio-macros" 2354 | version = "2.4.0" 2355 | source = "registry+https://github.com/rust-lang/crates.io-index" 2356 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 2357 | dependencies = [ 2358 | "proc-macro2", 2359 | "quote", 2360 | "syn", 2361 | ] 2362 | 2363 | [[package]] 2364 | name = "tokio-native-tls" 2365 | version = "0.3.1" 2366 | source = "registry+https://github.com/rust-lang/crates.io-index" 2367 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2368 | dependencies = [ 2369 | "native-tls", 2370 | "tokio", 2371 | ] 2372 | 2373 | [[package]] 2374 | name = "tokio-rustls" 2375 | version = "0.26.0" 2376 | source = "registry+https://github.com/rust-lang/crates.io-index" 2377 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 2378 | dependencies = [ 2379 | "rustls", 2380 | "rustls-pki-types", 2381 | "tokio", 2382 | ] 2383 | 2384 | [[package]] 2385 | name = "tokio-util" 2386 | version = "0.7.12" 2387 | source = "registry+https://github.com/rust-lang/crates.io-index" 2388 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 2389 | dependencies = [ 2390 | "bytes", 2391 | "futures-core", 2392 | "futures-sink", 2393 | "pin-project-lite", 2394 | "slab", 2395 | "tokio", 2396 | ] 2397 | 2398 | [[package]] 2399 | name = "tower" 2400 | version = "0.5.1" 2401 | source = "registry+https://github.com/rust-lang/crates.io-index" 2402 | checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" 2403 | dependencies = [ 2404 | "futures-core", 2405 | "futures-util", 2406 | "pin-project-lite", 2407 | "sync_wrapper 0.1.2", 2408 | "tokio", 2409 | "tokio-util", 2410 | "tower-layer", 2411 | "tower-service", 2412 | "tracing", 2413 | ] 2414 | 2415 | [[package]] 2416 | name = "tower-http" 2417 | version = "0.6.1" 2418 | source = "registry+https://github.com/rust-lang/crates.io-index" 2419 | checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" 2420 | dependencies = [ 2421 | "base64 0.22.1", 2422 | "bitflags 2.6.0", 2423 | "bytes", 2424 | "http", 2425 | "http-body", 2426 | "mime", 2427 | "pin-project-lite", 2428 | "tower-layer", 2429 | "tower-service", 2430 | "tracing", 2431 | ] 2432 | 2433 | [[package]] 2434 | name = "tower-layer" 2435 | version = "0.3.3" 2436 | source = "registry+https://github.com/rust-lang/crates.io-index" 2437 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2438 | 2439 | [[package]] 2440 | name = "tower-service" 2441 | version = "0.3.3" 2442 | source = "registry+https://github.com/rust-lang/crates.io-index" 2443 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2444 | 2445 | [[package]] 2446 | name = "tracing" 2447 | version = "0.1.40" 2448 | source = "registry+https://github.com/rust-lang/crates.io-index" 2449 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 2450 | dependencies = [ 2451 | "log", 2452 | "pin-project-lite", 2453 | "tracing-attributes", 2454 | "tracing-core", 2455 | ] 2456 | 2457 | [[package]] 2458 | name = "tracing-attributes" 2459 | version = "0.1.27" 2460 | source = "registry+https://github.com/rust-lang/crates.io-index" 2461 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 2462 | dependencies = [ 2463 | "proc-macro2", 2464 | "quote", 2465 | "syn", 2466 | ] 2467 | 2468 | [[package]] 2469 | name = "tracing-core" 2470 | version = "0.1.32" 2471 | source = "registry+https://github.com/rust-lang/crates.io-index" 2472 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 2473 | dependencies = [ 2474 | "once_cell", 2475 | "valuable", 2476 | ] 2477 | 2478 | [[package]] 2479 | name = "tracing-log" 2480 | version = "0.2.0" 2481 | source = "registry+https://github.com/rust-lang/crates.io-index" 2482 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2483 | dependencies = [ 2484 | "log", 2485 | "once_cell", 2486 | "tracing-core", 2487 | ] 2488 | 2489 | [[package]] 2490 | name = "tracing-subscriber" 2491 | version = "0.3.18" 2492 | source = "registry+https://github.com/rust-lang/crates.io-index" 2493 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 2494 | dependencies = [ 2495 | "nu-ansi-term", 2496 | "sharded-slab", 2497 | "smallvec", 2498 | "thread_local", 2499 | "tracing-core", 2500 | "tracing-log", 2501 | ] 2502 | 2503 | [[package]] 2504 | name = "try-lock" 2505 | version = "0.2.5" 2506 | source = "registry+https://github.com/rust-lang/crates.io-index" 2507 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2508 | 2509 | [[package]] 2510 | name = "typenum" 2511 | version = "1.17.0" 2512 | source = "registry+https://github.com/rust-lang/crates.io-index" 2513 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2514 | 2515 | [[package]] 2516 | name = "ucd-trie" 2517 | version = "0.1.7" 2518 | source = "registry+https://github.com/rust-lang/crates.io-index" 2519 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 2520 | 2521 | [[package]] 2522 | name = "unicase" 2523 | version = "2.8.0" 2524 | source = "registry+https://github.com/rust-lang/crates.io-index" 2525 | checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" 2526 | 2527 | [[package]] 2528 | name = "unicode-ident" 2529 | version = "1.0.13" 2530 | source = "registry+https://github.com/rust-lang/crates.io-index" 2531 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 2532 | 2533 | [[package]] 2534 | name = "unsafe-libyaml" 2535 | version = "0.2.11" 2536 | source = "registry+https://github.com/rust-lang/crates.io-index" 2537 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 2538 | 2539 | [[package]] 2540 | name = "untrusted" 2541 | version = "0.9.0" 2542 | source = "registry+https://github.com/rust-lang/crates.io-index" 2543 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2544 | 2545 | [[package]] 2546 | name = "url" 2547 | version = "2.5.3" 2548 | source = "registry+https://github.com/rust-lang/crates.io-index" 2549 | checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" 2550 | dependencies = [ 2551 | "form_urlencoded", 2552 | "idna", 2553 | "percent-encoding", 2554 | ] 2555 | 2556 | [[package]] 2557 | name = "utf16_iter" 2558 | version = "1.0.5" 2559 | source = "registry+https://github.com/rust-lang/crates.io-index" 2560 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 2561 | 2562 | [[package]] 2563 | name = "utf8_iter" 2564 | version = "1.0.4" 2565 | source = "registry+https://github.com/rust-lang/crates.io-index" 2566 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2567 | 2568 | [[package]] 2569 | name = "utf8parse" 2570 | version = "0.2.2" 2571 | source = "registry+https://github.com/rust-lang/crates.io-index" 2572 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2573 | 2574 | [[package]] 2575 | name = "uuid" 2576 | version = "1.11.0" 2577 | source = "registry+https://github.com/rust-lang/crates.io-index" 2578 | checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" 2579 | dependencies = [ 2580 | "getrandom", 2581 | "serde", 2582 | ] 2583 | 2584 | [[package]] 2585 | name = "valuable" 2586 | version = "0.1.0" 2587 | source = "registry+https://github.com/rust-lang/crates.io-index" 2588 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2589 | 2590 | [[package]] 2591 | name = "vcpkg" 2592 | version = "0.2.15" 2593 | source = "registry+https://github.com/rust-lang/crates.io-index" 2594 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2595 | 2596 | [[package]] 2597 | name = "version_check" 2598 | version = "0.9.5" 2599 | source = "registry+https://github.com/rust-lang/crates.io-index" 2600 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2601 | 2602 | [[package]] 2603 | name = "want" 2604 | version = "0.3.1" 2605 | source = "registry+https://github.com/rust-lang/crates.io-index" 2606 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2607 | dependencies = [ 2608 | "try-lock", 2609 | ] 2610 | 2611 | [[package]] 2612 | name = "wasi" 2613 | version = "0.11.0+wasi-snapshot-preview1" 2614 | source = "registry+https://github.com/rust-lang/crates.io-index" 2615 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2616 | 2617 | [[package]] 2618 | name = "wasm-bindgen" 2619 | version = "0.2.95" 2620 | source = "registry+https://github.com/rust-lang/crates.io-index" 2621 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 2622 | dependencies = [ 2623 | "cfg-if", 2624 | "once_cell", 2625 | "wasm-bindgen-macro", 2626 | ] 2627 | 2628 | [[package]] 2629 | name = "wasm-bindgen-backend" 2630 | version = "0.2.95" 2631 | source = "registry+https://github.com/rust-lang/crates.io-index" 2632 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 2633 | dependencies = [ 2634 | "bumpalo", 2635 | "log", 2636 | "once_cell", 2637 | "proc-macro2", 2638 | "quote", 2639 | "syn", 2640 | "wasm-bindgen-shared", 2641 | ] 2642 | 2643 | [[package]] 2644 | name = "wasm-bindgen-futures" 2645 | version = "0.4.45" 2646 | source = "registry+https://github.com/rust-lang/crates.io-index" 2647 | checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" 2648 | dependencies = [ 2649 | "cfg-if", 2650 | "js-sys", 2651 | "wasm-bindgen", 2652 | "web-sys", 2653 | ] 2654 | 2655 | [[package]] 2656 | name = "wasm-bindgen-macro" 2657 | version = "0.2.95" 2658 | source = "registry+https://github.com/rust-lang/crates.io-index" 2659 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 2660 | dependencies = [ 2661 | "quote", 2662 | "wasm-bindgen-macro-support", 2663 | ] 2664 | 2665 | [[package]] 2666 | name = "wasm-bindgen-macro-support" 2667 | version = "0.2.95" 2668 | source = "registry+https://github.com/rust-lang/crates.io-index" 2669 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 2670 | dependencies = [ 2671 | "proc-macro2", 2672 | "quote", 2673 | "syn", 2674 | "wasm-bindgen-backend", 2675 | "wasm-bindgen-shared", 2676 | ] 2677 | 2678 | [[package]] 2679 | name = "wasm-bindgen-shared" 2680 | version = "0.2.95" 2681 | source = "registry+https://github.com/rust-lang/crates.io-index" 2682 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 2683 | 2684 | [[package]] 2685 | name = "web-sys" 2686 | version = "0.3.72" 2687 | source = "registry+https://github.com/rust-lang/crates.io-index" 2688 | checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" 2689 | dependencies = [ 2690 | "js-sys", 2691 | "wasm-bindgen", 2692 | ] 2693 | 2694 | [[package]] 2695 | name = "winapi" 2696 | version = "0.3.9" 2697 | source = "registry+https://github.com/rust-lang/crates.io-index" 2698 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2699 | dependencies = [ 2700 | "winapi-i686-pc-windows-gnu", 2701 | "winapi-x86_64-pc-windows-gnu", 2702 | ] 2703 | 2704 | [[package]] 2705 | name = "winapi-i686-pc-windows-gnu" 2706 | version = "0.4.0" 2707 | source = "registry+https://github.com/rust-lang/crates.io-index" 2708 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2709 | 2710 | [[package]] 2711 | name = "winapi-x86_64-pc-windows-gnu" 2712 | version = "0.4.0" 2713 | source = "registry+https://github.com/rust-lang/crates.io-index" 2714 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2715 | 2716 | [[package]] 2717 | name = "windows-core" 2718 | version = "0.52.0" 2719 | source = "registry+https://github.com/rust-lang/crates.io-index" 2720 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2721 | dependencies = [ 2722 | "windows-targets", 2723 | ] 2724 | 2725 | [[package]] 2726 | name = "windows-registry" 2727 | version = "0.2.0" 2728 | source = "registry+https://github.com/rust-lang/crates.io-index" 2729 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 2730 | dependencies = [ 2731 | "windows-result", 2732 | "windows-strings", 2733 | "windows-targets", 2734 | ] 2735 | 2736 | [[package]] 2737 | name = "windows-result" 2738 | version = "0.2.0" 2739 | source = "registry+https://github.com/rust-lang/crates.io-index" 2740 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2741 | dependencies = [ 2742 | "windows-targets", 2743 | ] 2744 | 2745 | [[package]] 2746 | name = "windows-strings" 2747 | version = "0.1.0" 2748 | source = "registry+https://github.com/rust-lang/crates.io-index" 2749 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2750 | dependencies = [ 2751 | "windows-result", 2752 | "windows-targets", 2753 | ] 2754 | 2755 | [[package]] 2756 | name = "windows-sys" 2757 | version = "0.52.0" 2758 | source = "registry+https://github.com/rust-lang/crates.io-index" 2759 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2760 | dependencies = [ 2761 | "windows-targets", 2762 | ] 2763 | 2764 | [[package]] 2765 | name = "windows-sys" 2766 | version = "0.59.0" 2767 | source = "registry+https://github.com/rust-lang/crates.io-index" 2768 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2769 | dependencies = [ 2770 | "windows-targets", 2771 | ] 2772 | 2773 | [[package]] 2774 | name = "windows-targets" 2775 | version = "0.52.6" 2776 | source = "registry+https://github.com/rust-lang/crates.io-index" 2777 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2778 | dependencies = [ 2779 | "windows_aarch64_gnullvm", 2780 | "windows_aarch64_msvc", 2781 | "windows_i686_gnu", 2782 | "windows_i686_gnullvm", 2783 | "windows_i686_msvc", 2784 | "windows_x86_64_gnu", 2785 | "windows_x86_64_gnullvm", 2786 | "windows_x86_64_msvc", 2787 | ] 2788 | 2789 | [[package]] 2790 | name = "windows_aarch64_gnullvm" 2791 | version = "0.52.6" 2792 | source = "registry+https://github.com/rust-lang/crates.io-index" 2793 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2794 | 2795 | [[package]] 2796 | name = "windows_aarch64_msvc" 2797 | version = "0.52.6" 2798 | source = "registry+https://github.com/rust-lang/crates.io-index" 2799 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2800 | 2801 | [[package]] 2802 | name = "windows_i686_gnu" 2803 | version = "0.52.6" 2804 | source = "registry+https://github.com/rust-lang/crates.io-index" 2805 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2806 | 2807 | [[package]] 2808 | name = "windows_i686_gnullvm" 2809 | version = "0.52.6" 2810 | source = "registry+https://github.com/rust-lang/crates.io-index" 2811 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2812 | 2813 | [[package]] 2814 | name = "windows_i686_msvc" 2815 | version = "0.52.6" 2816 | source = "registry+https://github.com/rust-lang/crates.io-index" 2817 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2818 | 2819 | [[package]] 2820 | name = "windows_x86_64_gnu" 2821 | version = "0.52.6" 2822 | source = "registry+https://github.com/rust-lang/crates.io-index" 2823 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2824 | 2825 | [[package]] 2826 | name = "windows_x86_64_gnullvm" 2827 | version = "0.52.6" 2828 | source = "registry+https://github.com/rust-lang/crates.io-index" 2829 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2830 | 2831 | [[package]] 2832 | name = "windows_x86_64_msvc" 2833 | version = "0.52.6" 2834 | source = "registry+https://github.com/rust-lang/crates.io-index" 2835 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2836 | 2837 | [[package]] 2838 | name = "write16" 2839 | version = "1.0.0" 2840 | source = "registry+https://github.com/rust-lang/crates.io-index" 2841 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2842 | 2843 | [[package]] 2844 | name = "writeable" 2845 | version = "0.5.5" 2846 | source = "registry+https://github.com/rust-lang/crates.io-index" 2847 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2848 | 2849 | [[package]] 2850 | name = "yoke" 2851 | version = "0.7.4" 2852 | source = "registry+https://github.com/rust-lang/crates.io-index" 2853 | checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" 2854 | dependencies = [ 2855 | "serde", 2856 | "stable_deref_trait", 2857 | "yoke-derive", 2858 | "zerofrom", 2859 | ] 2860 | 2861 | [[package]] 2862 | name = "yoke-derive" 2863 | version = "0.7.4" 2864 | source = "registry+https://github.com/rust-lang/crates.io-index" 2865 | checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" 2866 | dependencies = [ 2867 | "proc-macro2", 2868 | "quote", 2869 | "syn", 2870 | "synstructure", 2871 | ] 2872 | 2873 | [[package]] 2874 | name = "zerocopy" 2875 | version = "0.7.35" 2876 | source = "registry+https://github.com/rust-lang/crates.io-index" 2877 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2878 | dependencies = [ 2879 | "byteorder", 2880 | "zerocopy-derive", 2881 | ] 2882 | 2883 | [[package]] 2884 | name = "zerocopy-derive" 2885 | version = "0.7.35" 2886 | source = "registry+https://github.com/rust-lang/crates.io-index" 2887 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2888 | dependencies = [ 2889 | "proc-macro2", 2890 | "quote", 2891 | "syn", 2892 | ] 2893 | 2894 | [[package]] 2895 | name = "zerofrom" 2896 | version = "0.1.4" 2897 | source = "registry+https://github.com/rust-lang/crates.io-index" 2898 | checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" 2899 | dependencies = [ 2900 | "zerofrom-derive", 2901 | ] 2902 | 2903 | [[package]] 2904 | name = "zerofrom-derive" 2905 | version = "0.1.4" 2906 | source = "registry+https://github.com/rust-lang/crates.io-index" 2907 | checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" 2908 | dependencies = [ 2909 | "proc-macro2", 2910 | "quote", 2911 | "syn", 2912 | "synstructure", 2913 | ] 2914 | 2915 | [[package]] 2916 | name = "zeroize" 2917 | version = "1.8.1" 2918 | source = "registry+https://github.com/rust-lang/crates.io-index" 2919 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2920 | 2921 | [[package]] 2922 | name = "zerovec" 2923 | version = "0.10.4" 2924 | source = "registry+https://github.com/rust-lang/crates.io-index" 2925 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2926 | dependencies = [ 2927 | "yoke", 2928 | "zerofrom", 2929 | "zerovec-derive", 2930 | ] 2931 | 2932 | [[package]] 2933 | name = "zerovec-derive" 2934 | version = "0.10.3" 2935 | source = "registry+https://github.com/rust-lang/crates.io-index" 2936 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2937 | dependencies = [ 2938 | "proc-macro2", 2939 | "quote", 2940 | "syn", 2941 | ] 2942 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "robotlb" 3 | version = "0.0.0" 4 | edition = "2021" 5 | readme = "README.md" 6 | 7 | [dependencies] 8 | clap = { version = "4.5.21", features = ["derive", "env"] } 9 | dotenvy = "0.15.7" 10 | futures = "0.3.31" 11 | hcloud = "0.21.0" 12 | k8s-openapi = { version = "0.23.0", features = ["v1_31"] } 13 | kube = { version = "0.96.0", features = ["runtime"] } 14 | thiserror = "2.0.3" 15 | tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } 16 | tracing = "0.1.40" 17 | tracing-subscriber = "0.3.18" 18 | 19 | [target.'cfg(not(target_env = "msvc"))'.dependencies] 20 | tikv-jemallocator = "0.6" 21 | 22 | [profile.release] 23 | codegen-units = 1 24 | lto = true 25 | opt-level = 3 26 | panic = "abort" 27 | debug = false 28 | strip = true 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.82-bookworm AS builder 2 | 3 | RUN apt update && apt-get install -y pkg-config libjemalloc-dev libssl-dev && apt-get clean 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | ENV RUST_BACKTRACE=1 10 | ENV JEMALLOC_SYS_WITH_MALLOC_CONF="background_thread:true,tcache:false,dirty_decay_ms:100,muzzy_decay_ms:100,abort_conf:true" 11 | RUN cargo build --release 12 | 13 | FROM debian:bookworm AS base 14 | 15 | RUN apt-get update && apt install -y libssl-dev ca-certificates libjemalloc-dev && apt-get clean 16 | 17 | COPY --from=builder /app/target/release/robotlb /usr/local/bin/ 18 | ENV PATH=/usr/local/bin:$PATH 19 | ENTRYPOINT ["/usr/local/bin/robotlb"] 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hetzner LoadBalancer for bare-metal robot clusters 2 | 3 | This project is useful when you've deployed a bare-metal Kubernetes cluster on Hetzner Robot and want to use Hetzner's cloud load balancer. 4 | 5 | This small operator integrates them together, allowing you to use the `LoadBalancer` service type. 6 | 7 | You can follow the [TUTORIAL.md](./tutorial.md) to see how to set up a cluster using RobotLB from scratch. 8 | 9 | ## Prerequisites 10 | 11 | Before using this operator, make sure: 12 | 13 | 1. You have a cluster deployed on [Hetzner robot](https://robot.hetzner.com/) (at least agent nodes); 14 | 2. You've created a [vSwitch](https://docs.hetzner.com/robot/dedicated-server/network/vswitch/) for these servers; 15 | 3. You've assigned IPs to your dedicated servers within the vSwitch network. 16 | 4. You have a cloud network with subnet that points to the vSwitch ([Tutorial](https://docs.hetzner.com/cloud/networks/connect-dedi-vswitch/)); 17 | 5. You’ve specified node IPs using the `--node-ip` argument with the private IP. 18 | 19 | If you meet all the requirements, you can deploy `robotlb`. 20 | 21 | ## Deploying 22 | 23 | The recommended way to deploy this operator is using the Helm chart. 24 | 25 | ```bash 26 | helm show values oci://ghcr.io/intreecom/charts/robotlb > values.yaml 27 | # Edit values.yaml to suit your needs 28 | # Set `envs.ROBOTLB_HCLOUD_TOKEN`. 29 | helm install robotlb oci://ghcr.io/intreecom/charts/robotlb -f values.yaml 30 | ``` 31 | 32 | After the chart is installed, you should be able to create `LoadBalancer` services. 33 | 34 | ## How it works 35 | 36 | The operator listens to the Kubernetes API for services of type `LoadBalancer` and creates Hetzner load balancers that point to nodes based on `node-ip`. 37 | 38 | Nodes are selected based on where the service's target pods are deployed, which is determined by searching for pods with the service's selector. This behavior can be configured. 39 | 40 | 41 | ## Configuration 42 | 43 | This project has two places for configuration: environment variables and service annotations. 44 | 45 | ### Envs 46 | 47 | Environment variables are mainly used to override default arguments and provide sensitive information. 48 | 49 | Here’s a complete list of parameters for the operator's binary: 50 | 51 | ``` 52 | Usage: robotlb [OPTIONS] --hcloud-token 53 | 54 | Options: 55 | -t, --hcloud-token 56 | `HCloud` API token [env: ROBOTLB_HCLOUD_TOKEN=] 57 | --default-network 58 | Default network to use for load balancers. If not set, then only network from the service annotation will be used [env: ROBOTLB_DEFAULT_NETWORK=] 59 | --dynamic-node-selector 60 | If enabled, the operator will try to find target nodes based on where the target pods are actually deployed. If disabled, the operator will try to find target nodes based on the node selector [env: ROBOTLB_DYNAMIC_NODE_SELECTOR=] 61 | --default-lb-retries 62 | Default load balancer healthcheck retries cound [env: ROBOTLB_DEFAULT_LB_RETRIES=] [default: 3] 63 | --default-lb-timeout 64 | Default load balancer healthcheck timeout [env: ROBOTLB_DEFAULT_LB_TIMEOUT=] [default: 10] 65 | --default-lb-interval 66 | Default load balancer healhcheck interval [env: ROBOTLB_DEFAULT_LB_INTERVAL=] [default: 15] 67 | --default-lb-location 68 | Default location of a load balancer. https://docs.hetzner.com/cloud/general/locations/ [env: ROBOTLB_DEFAULT_LB_LOCATION=] [default: hel1] 69 | --default-balancer-type 70 | Type of a load balancer. It differs in price, number of connections, target servers, etc. The default value is the smallest balancer. https://docs.hetzner.com/cloud/load-balancers/overview#pricing [env: ROBOTLB_DEFAULT_LB_TYPE=] [default: lb11] 71 | --default-lb-algorithm 72 | Default load balancer algorithm. Possible values: * `least-connections` * `round-robin` https://docs.hetzner.com/cloud/load-balancers/overview#load-balancers [env: ROBOTLB_DEFAULT_LB_ALGORITHM=] [default: least-connections] 73 | --default-lb-proxy-mode-enabled 74 | Default load balancer proxy mode. If enabled, the load balancer will act as a proxy for the target servers. The default value is `false`. https://docs.hetzner.com/cloud/load-balancers/faq/#what-does-proxy-protocol-mean-and-should-i-enable-it [env: ROBOTLB_DEFAULT_LB_PROXY_MODE_ENABLED=] 75 | --ipv6-ingress 76 | Whether to enable IPv6 ingress for the load balancer. If enabled, the load balancer's IPv6 will be attached to the service as an external IP along with IPv4 [env: ROBOTLB_IPV6_INGRESS=] 77 | --log-level 78 | [env: ROBOTLB_LOG_LEVEL=] [default: INFO] 79 | -h, --help 80 | Print help 81 | ``` 82 | 83 | 84 | ### Service annotations 85 | 86 | 87 | ```yaml 88 | apiVersion: v1 89 | kind: Service 90 | metadata: 91 | name: target 92 | annotations: 93 | # Custom name of the balancer to create on Hetzner. Defaults to service name. 94 | robotlb/balancer: "custom name" 95 | # Hetzner cloud network. If this annotation is missing, the operator will try to 96 | # assign external IPs to the load balancer if available. Otherwise, the update won't happen. 97 | robotlb/lb-network: "my-net" 98 | # Requests specific IP address for the load balancer in the private network. If not specified, 99 | # a random one is given. This parameter does nothing in case if network is not specified. 100 | robotlb/lb-private-ip: "10.10.10.10" 101 | # Node selector for the loadbalancer. This is only required if ROBOTLB_DYNAMIC_NODE_SELECTOR 102 | # is set to false. If not specified then, all nodes will be selected as LB targets by default. 103 | # This property helps you filter out nodes. 104 | # Filters are separated by commas and should have one of the following formats: 105 | # * key=value -- checks that the node has a label `key` with value `value`; 106 | # * key!=value -- verifies that key either doesn't exist or isn't equal to `value`; 107 | # * !key -- verifies that the node doesn't have a label `key`; 108 | # * key -- verifies that the node has a label `key`. 109 | robotlb/node-selector: "node-role.kubernetes.io/control-plane!=true,beta.kubernetes.io/arch=amd64" 110 | ### Load balancer healthcheck options. ### 111 | # How often to run health probes. 112 | robotlb/lb-check-interval: "5" 113 | # Timeout for a single probe. 114 | robotlb/lb-timeout: "3" 115 | # How many failed probes before marking the node as unhealthy. 116 | robotlb/lb-retries: "3" 117 | 118 | ### Load balancer options ### 119 | # Whether to use proxy mode for this target. 120 | # https://docs.hetzner.com/cloud/load-balancers/faq/#what-does-proxy-protocol-mean-and-should-i-enable-it 121 | robotlb/lb-proxy-mode: "false" 122 | # Location of the load balancer. This expects the code of one of Hetzner's available locations. 123 | robotlb/lb-location: "hel1" 124 | # Balancing algorithm. Can be either 125 | # * least-connection 126 | # * round-robin 127 | robotlb/lb-algorithm: "least-connection" 128 | # Type of balancer. 129 | robotlb/balancer-type: "lb11" 130 | spec: 131 | type: LoadBalancer 132 | # If dynamic node selector is enabled, nodes will be found 133 | # using this property. 134 | selector: 135 | app: target 136 | ports: 137 | # Currently only TCP protocol is supported. UDP will be ignored. 138 | - protocol: TCP 139 | port: 80 # This will become the listening port on the LB 140 | targetPort: 80 141 | ``` 142 | 143 | ## Star History 144 | 145 | [![Star History Chart](https://api.star-history.com/svg?repos=Intreecom/robotlb&type=Date)](https://star-history.com/#Intreecom/robotlb&Date) 146 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: robotlb 3 | description: A Helm chart for robotlb (loadbalancer on hetzner cloud). 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.3 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.0.0" 25 | -------------------------------------------------------------------------------- /helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | The RobotLB Operator was successfully installed. 2 | Please follow the readme to create loadbalanced services. 3 | 4 | README: https://github.com/intreecom/robotlb 5 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "robotlb.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "robotlb.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "robotlb.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "robotlb.labels" -}} 37 | helm.sh/chart: {{ include "robotlb.chart" . }} 38 | {{ include "robotlb.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "robotlb.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "robotlb.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "robotlb.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "robotlb.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "robotlb.fullname" . }} 5 | labels: 6 | {{- include "robotlb.labels" . | nindent 4 }} 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | {{- include "robotlb.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | {{- with .Values.podAnnotations }} 15 | annotations: 16 | {{- toYaml . | nindent 8 }} 17 | {{- end }} 18 | labels: 19 | {{- include "robotlb.labels" . | nindent 8 }} 20 | {{- with .Values.podLabels }} 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | spec: 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | serviceAccountName: {{ include "robotlb.serviceAccountName" . }} 29 | securityContext: 30 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 31 | containers: 32 | - name: {{ .Chart.Name }} 33 | securityContext: 34 | {{- toYaml .Values.securityContext | nindent 12 }} 35 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 36 | imagePullPolicy: {{ .Values.image.pullPolicy }} 37 | command: 38 | - /usr/local/bin/robotlb 39 | resources: 40 | {{- toYaml .Values.resources | nindent 12 }} 41 | {{- with .Values.envs }} 42 | env: 43 | {{- range $key, $val := . }} 44 | - name: {{ $key | quote }} 45 | value: {{ $val | quote }} 46 | {{ end -}} 47 | {{- end }} 48 | {{- with .Values.existingSecrets }} 49 | envFrom: 50 | {{- range $val := . }} 51 | - secretRef: 52 | name: {{ $val | quote }} 53 | {{ end -}} 54 | {{- end }} 55 | {{- with .Values.nodeSelector }} 56 | nodeSelector: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.affinity }} 60 | affinity: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.tolerations }} 64 | tolerations: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /helm/templates/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "robotlb.fullname" . }}-cr 6 | rules: 7 | {{- toYaml .Values.serviceAccount.permissions | nindent 2 }} 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: ClusterRoleBinding 11 | metadata: 12 | name: {{ include "robotlb.fullname" . }}-crb 13 | roleRef: 14 | apiGroup: rbac.authorization.k8s.io 15 | kind: ClusterRole 16 | name: {{ include "robotlb.fullname" . }}-cr 17 | subjects: 18 | - kind: ServiceAccount 19 | name: {{ include "robotlb.serviceAccountName" . }} 20 | namespace: {{ .Release.Namespace }} 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "robotlb.serviceAccountName" . }} 6 | labels: 7 | {{- include "robotlb.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for robotlb. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | image: 6 | repository: ghcr.io/intreecom/robotlb 7 | pullPolicy: IfNotPresent 8 | # Overrides the image tag whose default is the chart appVersion. 9 | tag: "" 10 | 11 | imagePullSecrets: [] 12 | nameOverride: "" 13 | fullnameOverride: "" 14 | 15 | envs: 16 | ROBOTLB_LOG_LEVEL: "INFO" 17 | 18 | existingSecrets: [] 19 | 20 | serviceAccount: 21 | # Specifies whether a service account should be created 22 | create: true 23 | # Automatically mount a ServiceAccount's API credentials? 24 | automount: true 25 | # Annotations to add to the service account 26 | annotations: {} 27 | # The name of the service account to use. 28 | # If not set and create is true, a name is generated using the fullname template 29 | name: "" 30 | # This is a list of cluster permissions to apply to the service account. 31 | # By default it grants all permissions. 32 | permissions: 33 | - apiGroups: [""] 34 | resources: [services, services/status] 35 | verbs: [get, list, patch, update, watch] 36 | - apiGroups: [""] 37 | resources: [nodes, pods] 38 | verbs: [get, list, watch] 39 | 40 | podAnnotations: {} 41 | podLabels: {} 42 | 43 | podSecurityContext: 44 | {} 45 | # fsGroup: 2000 46 | 47 | securityContext: 48 | {} 49 | # capabilities: 50 | # drop: 51 | # - ALL 52 | # readOnlyRootFilesystem: true 53 | # runAsNonRoot: true 54 | # runAsUser: 1000 55 | 56 | resources: 57 | {} 58 | # We usually recommend not to specify default resources and to leave this as a conscious 59 | # choice for the user. This also increases chances charts run on environments with little 60 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 61 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 62 | # limits: 63 | # cpu: 100m 64 | # memory: 128Mi 65 | # requests: 66 | # cpu: 100m 67 | # memory: 128Mi 68 | 69 | nodeSelector: {} 70 | 71 | tolerations: [] 72 | 73 | affinity: {} 74 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use tracing::level_filters::LevelFilter; 3 | 4 | #[derive(Debug, Clone, Parser)] 5 | pub struct OperatorConfig { 6 | /// `HCloud` API token. 7 | #[arg(short = 't', long, env = "ROBOTLB_HCLOUD_TOKEN")] 8 | pub hcloud_token: String, 9 | 10 | /// Default network to use for load balancers. 11 | /// If not set, then only network from the service annotation will be used. 12 | #[arg(long, env = "ROBOTLB_DEFAULT_NETWORK", default_value = None)] 13 | pub default_network: Option, 14 | 15 | /// If enabled, the operator will try to find target nodes based on where the target pods are actually deployed. 16 | /// If disabled, the operator will try to find target nodes based on the node selector. 17 | #[arg(long, env = "ROBOTLB_DYNAMIC_NODE_SELECTOR", default_value = "true")] 18 | pub dynamic_node_selector: bool, 19 | 20 | /// Default load balancer healthcheck retries cound. 21 | #[arg(long, env = "ROBOTLB_DEFAULT_LB_RETRIES", default_value = "3")] 22 | pub default_lb_retries: i32, 23 | 24 | /// Default load balancer healthcheck timeout. 25 | #[arg(long, env = "ROBOTLB_DEFAULT_LB_TIMEOUT", default_value = "10")] 26 | pub default_lb_timeout: i32, 27 | 28 | /// Default load balancer healhcheck interval. 29 | #[arg(long, env = "ROBOTLB_DEFAULT_LB_INTERVAL", default_value = "15")] 30 | pub default_lb_interval: i32, 31 | 32 | /// Default location of a load balancer. 33 | /// https://docs.hetzner.com/cloud/general/locations/ 34 | #[arg(long, env = "ROBOTLB_DEFAULT_LB_LOCATION", default_value = "hel1")] 35 | pub default_lb_location: String, 36 | 37 | /// Type of a load balancer. It differs in price, number of connections, 38 | /// target servers, etc. The default value is the smallest balancer. 39 | /// https://docs.hetzner.com/cloud/load-balancers/overview#pricing 40 | #[arg(long, env = "ROBOTLB_DEFAULT_LB_TYPE", default_value = "lb11")] 41 | pub default_balancer_type: String, 42 | 43 | /// Default load balancer algorithm. 44 | /// Possible values: 45 | /// * `least-connections` 46 | /// * `round-robin` 47 | /// https://docs.hetzner.com/cloud/load-balancers/overview#load-balancers 48 | #[arg( 49 | long, 50 | env = "ROBOTLB_DEFAULT_LB_ALGORITHM", 51 | default_value = "least-connections" 52 | )] 53 | pub default_lb_algorithm: String, 54 | 55 | /// Default load balancer proxy mode. If enabled, the load balancer will 56 | /// act as a proxy for the target servers. The default value is `false`. 57 | /// https://docs.hetzner.com/cloud/load-balancers/faq/#what-does-proxy-protocol-mean-and-should-i-enable-it 58 | #[arg( 59 | long, 60 | env = "ROBOTLB_DEFAULT_LB_PROXY_MODE_ENABLED", 61 | default_value = "false" 62 | )] 63 | pub default_lb_proxy_mode_enabled: bool, 64 | 65 | /// Whether to enable IPv6 ingress for the load balancer. 66 | /// If enabled, the load balancer's IPv6 will be attached to the service as an external IP along with IPv4. 67 | #[arg(long, env = "ROBOTLB_IPV6_INGRESS", default_value = "false")] 68 | pub ipv6_ingress: bool, 69 | 70 | // Log level of the operator. 71 | #[arg(long, env = "ROBOTLB_LOG_LEVEL", default_value = "INFO")] 72 | pub log_level: LevelFilter, 73 | } 74 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const LB_NAME_LABEL_NAME: &str = "robotlb/balancer"; 2 | pub const LB_NODE_SELECTOR: &str = "robotlb/node-selector"; 3 | pub const LB_NODE_IP_LABEL_NAME: &str = "robotlb/node-ip"; 4 | 5 | // LB config 6 | pub const LB_CHECK_INTERVAL_ANN_NAME: &str = "robotlb/lb-check-interval"; 7 | pub const LB_TIMEOUT_ANN_NAME: &str = "robotlb/lb-timeout"; 8 | pub const LB_RETRIES_ANN_NAME: &str = "robotlb/lb-retries"; 9 | pub const LB_PROXY_MODE_LABEL_NAME: &str = "robotlb/lb-proxy-mode"; 10 | pub const LB_NETWORK_LABEL_NAME: &str = "robotlb/lb-network"; 11 | pub const LB_PRIVATE_IP_LABEL_NAME: &str = "robotlb/lb-private-ip"; 12 | 13 | pub const LB_LOCATION_LABEL_NAME: &str = "robotlb/lb-location"; 14 | pub const LB_ALGORITHM_LABEL_NAME: &str = "robotlb/lb-algorithm"; 15 | pub const LB_BALANCER_TYPE_LABEL_NAME: &str = "robotlb/balancer-type"; 16 | 17 | pub const DEFAULT_LB_RETRIES: i32 = 3; 18 | pub const DEFAULT_LB_TIMEOUT: i32 = 10; 19 | pub const DEFAULT_LB_INTERVAL: i32 = 15; 20 | 21 | pub const DEFAULT_LB_LOCATION: &str = "hel1"; 22 | pub const DEFAULT_LB_ALGORITHM: &str = "least-connections"; 23 | pub const DEFAULT_LB_BALANCER_TYPE: &str = "lb11"; 24 | 25 | pub const FINALIZER_NAME: &str = "robotlb/finalizer"; 26 | pub const ROBOTLB_LB_CLASS: &str = "robotlb"; 27 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type RobotLBResult = Result; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum RobotLBError { 7 | #[error("Cannot parse node filter: {0}")] 8 | InvalidNodeFilter(String), 9 | #[error("Unsupported service type")] 10 | UnsupportedServiceType, 11 | #[error("Service was skipped")] 12 | SkipService, 13 | #[error("Cannot parse integer value: {0}")] 14 | PaseIntError(#[from] std::num::ParseIntError), 15 | #[error("Cannot parse boolean value: {0}")] 16 | PaseBoolError(#[from] std::str::ParseBoolError), 17 | #[error("HCloud error: {0}")] 18 | HCloudError(String), 19 | #[error("Kube error: {0}")] 20 | KubeError(#[from] kube::Error), 21 | #[error("Unknown LoadBalancing alorithm")] 22 | UnknownLBAlgorithm, 23 | #[error("Cannot get target nodes, because the service has no selector")] 24 | ServiceWithoutSelector, 25 | 26 | // HCloud API errors 27 | #[error("Cannot attach load balancer to a network. Reason: {0}")] 28 | HCloudLBAttachToNetworkError( 29 | #[from] 30 | hcloud::apis::Error, 31 | ), 32 | #[error("Cannot detach load balancer from network. Reason: {0}")] 33 | HcloudLBDetachFromNetworkError( 34 | #[from] 35 | hcloud::apis::Error, 36 | ), 37 | #[error("Cannot add load balancer target. Reason: {0}")] 38 | HcloudLBAddTargetError( 39 | #[from] hcloud::apis::Error, 40 | ), 41 | #[error("Cannot remove load balancer target. Reason: {0}")] 42 | HcloudLBRemoveTargetError( 43 | #[from] hcloud::apis::Error, 44 | ), 45 | #[error("Cannot add service to load balancer. Reason: {0}")] 46 | HcloudLBAddServiceError( 47 | #[from] hcloud::apis::Error, 48 | ), 49 | #[error("Cannot remove service from load balancer. Reason: {0}")] 50 | HcloudLBRemoveServiceError( 51 | #[from] hcloud::apis::Error, 52 | ), 53 | #[error("Cannot create load balancer. Reason: {0}")] 54 | HcloudLBCreateError( 55 | #[from] hcloud::apis::Error, 56 | ), 57 | #[error("Cannot delete load balancer. Reason: {0}")] 58 | HcloudLBDeleteError( 59 | #[from] hcloud::apis::Error, 60 | ), 61 | #[error("Cannot get load balancer. Reason: {0}")] 62 | HcloudLBGetError( 63 | #[from] hcloud::apis::Error, 64 | ), 65 | #[error("Cannot update service. Reason: {0}")] 66 | HcloudLBUpdateServiceError( 67 | #[from] hcloud::apis::Error, 68 | ), 69 | #[error("Cannot change type of load balancer. Reason: {0}")] 70 | HcloudLBChangeType( 71 | #[from] 72 | hcloud::apis::Error, 73 | ), 74 | #[error("Cannot change algorithm of load balancer. Reason: {0}")] 75 | HcloudLBChangeAlgorithm( 76 | #[from] hcloud::apis::Error, 77 | ), 78 | #[error("Cannot list networks. Reason: {0}")] 79 | HcloudListNetworksError( 80 | #[from] hcloud::apis::Error, 81 | ), 82 | #[error("Cannot list load balancers. Reason: {0}")] 83 | HcloudListLoadBalancersError( 84 | #[from] hcloud::apis::Error, 85 | ), 86 | } 87 | -------------------------------------------------------------------------------- /src/finalizers.rs: -------------------------------------------------------------------------------- 1 | use k8s_openapi::{api::core::v1::Service, serde_json::json}; 2 | use kube::{ 3 | api::{Patch, PatchParams}, 4 | Api, Client, ResourceExt, 5 | }; 6 | 7 | use crate::{ 8 | consts, 9 | error::{RobotLBError, RobotLBResult}, 10 | }; 11 | 12 | /// Add finalizer to the service. 13 | /// This will prevent the service from being deleted. 14 | pub async fn add(client: Client, svc: &Service) -> RobotLBResult<()> { 15 | let api = Api::::namespaced( 16 | client, 17 | svc.namespace().ok_or(RobotLBError::SkipService)?.as_str(), 18 | ); 19 | let patch = json!({ 20 | "metadata": { 21 | "finalizers": [consts::FINALIZER_NAME] 22 | } 23 | }); 24 | api.patch( 25 | svc.name_any().as_str(), 26 | &PatchParams::default(), 27 | &Patch::Merge(patch), 28 | ) 29 | .await?; 30 | Ok(()) 31 | } 32 | 33 | /// Check if service has the finalizer. 34 | #[must_use] 35 | pub fn check(service: &Service) -> bool { 36 | service 37 | .metadata 38 | .finalizers 39 | .as_ref() 40 | .map_or(false, |finalizers| { 41 | finalizers.contains(&consts::FINALIZER_NAME.to_string()) 42 | }) 43 | } 44 | 45 | /// Remove finalizer from the service. 46 | /// This will allow the service to be deleted. 47 | /// 48 | /// if service does not have the finalizer, this function will do nothing. 49 | pub async fn remove(client: Client, svc: &Service) -> RobotLBResult<()> { 50 | let api = Api::::namespaced( 51 | client, 52 | svc.namespace().ok_or(RobotLBError::SkipService)?.as_str(), 53 | ); 54 | let finalizers = svc 55 | .finalizers() 56 | .iter() 57 | .filter(|item| item.as_str() != consts::FINALIZER_NAME) 58 | .collect::>(); 59 | let patch = json!({ 60 | "metadata": { 61 | "finalizers": finalizers 62 | } 63 | }); 64 | api.patch( 65 | svc.name_any().as_str(), 66 | &PatchParams::default(), 67 | &Patch::Merge(patch), 68 | ) 69 | .await?; 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /src/label_filter.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, str::FromStr}; 2 | 3 | use crate::error::RobotLBError; 4 | 5 | /// Enum of all possible rules for label filtering. 6 | #[derive(Debug, Clone)] 7 | enum Rule { 8 | /// Equal rule checks if the key is equal to the value. 9 | Equal(String, String), 10 | /// `NotEqual` rule checks if the key is not equal to the value. 11 | NotEqual(String, String), 12 | /// Exists rule checks if the key exists. 13 | Exists(String), 14 | /// `DoesNotExist` rule checks if the key does not exist. 15 | DoesNotExist(String), 16 | } 17 | 18 | /// `LabelFilter` is a filter for Kubernetes labels. 19 | /// It is used to filter nodes by their labels. 20 | #[derive(Debug, Clone, Default)] 21 | pub struct LabelFilter { 22 | rules: Vec, 23 | } 24 | 25 | impl LabelFilter { 26 | #[must_use] 27 | pub fn check(&self, labels: &BTreeMap) -> bool { 28 | for rule in &self.rules { 29 | match rule { 30 | Rule::Equal(key, value) => { 31 | if labels.get(key) != Some(value) { 32 | return false; 33 | } 34 | } 35 | Rule::NotEqual(key, value) => { 36 | if labels.get(key) == Some(value) { 37 | return false; 38 | } 39 | } 40 | Rule::Exists(key) => { 41 | if labels.get(key).is_none() { 42 | return false; 43 | } 44 | } 45 | Rule::DoesNotExist(key) => { 46 | if labels.get(key).is_some() { 47 | return false; 48 | } 49 | } 50 | } 51 | } 52 | true 53 | } 54 | } 55 | 56 | /// Parse label filter from string. 57 | /// The string should be in the following format: 58 | /// `key=value,key!=value,key,!key` 59 | impl FromStr for LabelFilter { 60 | type Err = RobotLBError; 61 | 62 | fn from_str(s: &str) -> Result { 63 | let mut rules = Vec::new(); 64 | for rule in s.split(',') { 65 | let parts = rule.split('=').collect::>(); 66 | match *parts.as_slice() { 67 | [key] => { 68 | if key.starts_with('!') { 69 | rules.push(Rule::DoesNotExist( 70 | key.strip_prefix('!').unwrap().to_string(), 71 | )); 72 | continue; 73 | } 74 | rules.push(Rule::Exists(key.to_string())); 75 | } 76 | [key, value] => { 77 | if key.ends_with('!') { 78 | rules.push(Rule::NotEqual( 79 | key.strip_suffix('!').unwrap().to_string(), 80 | value.to_string(), 81 | )); 82 | continue; 83 | } 84 | rules.push(Rule::Equal(key.to_string(), value.to_string())); 85 | } 86 | _ => return Err(RobotLBError::InvalidNodeFilter(rule.to_string())), 87 | } 88 | } 89 | Ok(Self { rules }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lb.rs: -------------------------------------------------------------------------------- 1 | use hcloud::{ 2 | apis::{ 3 | configuration::Configuration as HcloudConfig, 4 | load_balancers_api::{ 5 | AddServiceParams, AddTargetParams, AttachLoadBalancerToNetworkParams, 6 | ChangeAlgorithmParams, ChangeTypeOfLoadBalancerParams, DeleteLoadBalancerParams, 7 | DeleteServiceParams, DetachLoadBalancerFromNetworkParams, ListLoadBalancersParams, 8 | RemoveTargetParams, UpdateServiceParams, 9 | }, 10 | networks_api::ListNetworksParams, 11 | }, 12 | models::{ 13 | AttachLoadBalancerToNetworkRequest, ChangeTypeOfLoadBalancerRequest, DeleteServiceRequest, 14 | DetachLoadBalancerFromNetworkRequest, LoadBalancerAddTarget, LoadBalancerAlgorithm, 15 | LoadBalancerService, LoadBalancerServiceHealthCheck, RemoveTargetRequest, 16 | UpdateLoadBalancerService, 17 | }, 18 | }; 19 | use k8s_openapi::api::core::v1::Service; 20 | use kube::ResourceExt; 21 | use std::{collections::HashMap, str::FromStr}; 22 | 23 | use crate::{ 24 | consts, 25 | error::{RobotLBError, RobotLBResult}, 26 | CurrentContext, 27 | }; 28 | 29 | #[derive(Debug)] 30 | pub struct LBService { 31 | pub listen_port: i32, 32 | pub target_port: i32, 33 | } 34 | 35 | enum LBAlgorithm { 36 | RoundRobin, 37 | LeastConnections, 38 | } 39 | 40 | /// Struct representing a load balancer 41 | /// It holds all the necessary information to manage the load balancer 42 | /// in Hetzner Cloud. 43 | #[derive(Debug)] 44 | pub struct LoadBalancer { 45 | pub name: String, 46 | pub services: HashMap, 47 | pub targets: Vec, 48 | pub private_ip: Option, 49 | 50 | pub check_interval: i32, 51 | pub timeout: i32, 52 | pub retries: i32, 53 | pub proxy_mode: bool, 54 | 55 | pub location: String, 56 | pub balancer_type: String, 57 | pub algorithm: LoadBalancerAlgorithm, 58 | pub network_name: Option, 59 | 60 | pub hcloud_config: HcloudConfig, 61 | } 62 | 63 | impl LoadBalancer { 64 | /// Create a new `LoadBalancer` instance from a Kubernetes service 65 | /// and the current context. 66 | /// This method will try to extract all the necessary information 67 | /// from the service annotations and the context. 68 | /// If some of the required information is missing, the method will 69 | /// try to use the default values from the context. 70 | pub fn try_from_svc(svc: &Service, context: &CurrentContext) -> RobotLBResult { 71 | let retries = svc 72 | .annotations() 73 | .get(consts::LB_RETRIES_ANN_NAME) 74 | .map(String::as_str) 75 | .map(i32::from_str) 76 | .transpose()? 77 | .unwrap_or(context.config.default_lb_retries); 78 | 79 | let timeout = svc 80 | .annotations() 81 | .get(consts::LB_TIMEOUT_ANN_NAME) 82 | .map(String::as_str) 83 | .map(i32::from_str) 84 | .transpose()? 85 | .unwrap_or(context.config.default_lb_timeout); 86 | 87 | let check_interval = svc 88 | .annotations() 89 | .get(consts::LB_CHECK_INTERVAL_ANN_NAME) 90 | .map(String::as_str) 91 | .map(i32::from_str) 92 | .transpose()? 93 | .unwrap_or(context.config.default_lb_interval); 94 | 95 | let proxy_mode = svc 96 | .annotations() 97 | .get(consts::LB_PROXY_MODE_LABEL_NAME) 98 | .map(String::as_str) 99 | .map(bool::from_str) 100 | .transpose()? 101 | .unwrap_or(context.config.default_lb_proxy_mode_enabled); 102 | 103 | let location = svc 104 | .annotations() 105 | .get(consts::LB_LOCATION_LABEL_NAME) 106 | .cloned() 107 | .unwrap_or_else(|| context.config.default_lb_location.clone()); 108 | 109 | let balancer_type = svc 110 | .annotations() 111 | .get(consts::LB_BALANCER_TYPE_LABEL_NAME) 112 | .cloned() 113 | .unwrap_or_else(|| context.config.default_balancer_type.clone()); 114 | 115 | let algorithm = svc 116 | .annotations() 117 | .get(consts::LB_ALGORITHM_LABEL_NAME) 118 | .map(String::as_str) 119 | .or(Some(&context.config.default_lb_algorithm)) 120 | .map(LBAlgorithm::from_str) 121 | .transpose()? 122 | .unwrap_or(LBAlgorithm::LeastConnections); 123 | 124 | let network_name = svc 125 | .annotations() 126 | .get(consts::LB_NETWORK_LABEL_NAME) 127 | .or(context.config.default_network.as_ref()) 128 | .cloned(); 129 | 130 | let name = svc 131 | .annotations() 132 | .get(consts::LB_NAME_LABEL_NAME) 133 | .cloned() 134 | .unwrap_or(svc.name_any()); 135 | 136 | let private_ip = svc 137 | .annotations() 138 | .get(consts::LB_PRIVATE_IP_LABEL_NAME) 139 | .cloned(); 140 | 141 | Ok(Self { 142 | name, 143 | private_ip, 144 | balancer_type, 145 | check_interval, 146 | timeout, 147 | retries, 148 | location, 149 | proxy_mode, 150 | network_name, 151 | algorithm: algorithm.into(), 152 | services: HashMap::default(), 153 | targets: Vec::default(), 154 | hcloud_config: context.hcloud_config.clone(), 155 | }) 156 | } 157 | 158 | /// Add a service to the load balancer. 159 | /// The service will listen on the `listen_port` and forward the 160 | /// traffic to the `target_port` to all targets. 161 | pub fn add_service(&mut self, listen_port: i32, target_port: i32) { 162 | self.services.insert(listen_port, target_port); 163 | } 164 | 165 | /// Add a target to the load balancer. 166 | /// The target will receive the traffic from the services. 167 | /// The target is identified by its IP address. 168 | pub fn add_target(&mut self, ip: &str) { 169 | tracing::debug!("Adding target {}", ip); 170 | self.targets.push(ip.to_string()); 171 | } 172 | 173 | /// Reconcile the load balancer to match the desired configuration. 174 | #[tracing::instrument(skip(self), fields(lb_name=self.name))] 175 | pub async fn reconcile(&self) -> RobotLBResult { 176 | let hcloud_balancer = self.get_or_create_hcloud_lb().await?; 177 | self.reconcile_algorithm(&hcloud_balancer).await?; 178 | self.reconcile_lb_type(&hcloud_balancer).await?; 179 | self.reconcile_network(&hcloud_balancer).await?; 180 | self.reconcile_services(&hcloud_balancer).await?; 181 | self.reconcile_targets(&hcloud_balancer).await?; 182 | Ok(hcloud_balancer) 183 | } 184 | 185 | /// Reconcile the services of the load balancer. 186 | /// This method will compare the desired configuration of the services 187 | /// with the current configuration of the services in the load balancer. 188 | /// If the configuration does not match, the method will update the service. 189 | async fn reconcile_services( 190 | &self, 191 | hcloud_balancer: &hcloud::models::LoadBalancer, 192 | ) -> RobotLBResult<()> { 193 | for service in &hcloud_balancer.services { 194 | // Here we check that all the services are configured correctly. 195 | // If the service is not configured correctly, we update it. 196 | if let Some(destination_port) = self.services.get(&service.listen_port) { 197 | if service.destination_port == *destination_port 198 | && service.health_check.port == *destination_port 199 | && service.health_check.interval == self.check_interval 200 | && service.health_check.retries == self.retries 201 | && service.health_check.timeout == self.timeout 202 | && service.proxyprotocol == self.proxy_mode 203 | && service.http.is_none() 204 | && service.health_check.protocol 205 | == hcloud::models::load_balancer_service_health_check::Protocol::Tcp 206 | { 207 | // The desired configuration matches the current configuration. 208 | continue; 209 | } 210 | tracing::info!( 211 | "Desired service configuration for port {} does not match current configuration. Updating ...", 212 | service.listen_port, 213 | ); 214 | hcloud::apis::load_balancers_api::update_service( 215 | &self.hcloud_config, 216 | UpdateServiceParams { 217 | id: hcloud_balancer.id, 218 | body: Some(UpdateLoadBalancerService { 219 | http: None, 220 | protocol: Some(hcloud::models::update_load_balancer_service::Protocol::Tcp), 221 | listen_port: service.listen_port, 222 | destination_port: Some(*destination_port), 223 | proxyprotocol: Some(self.proxy_mode), 224 | health_check: Some(Box::new( 225 | hcloud::models::UpdateLoadBalancerServiceHealthCheck { 226 | protocol: Some(hcloud::models::update_load_balancer_service_health_check::Protocol::Tcp), 227 | http: None, 228 | interval: Some(self.check_interval), 229 | port: Some(*destination_port), 230 | retries: Some(self.retries), 231 | timeout: Some(self.timeout), 232 | }, 233 | )), 234 | }), 235 | }, 236 | ) 237 | .await?; 238 | } else { 239 | tracing::info!( 240 | "Deleting service that listens for port {} from load-balancer {}", 241 | service.listen_port, 242 | hcloud_balancer.name, 243 | ); 244 | hcloud::apis::load_balancers_api::delete_service( 245 | &self.hcloud_config, 246 | DeleteServiceParams { 247 | id: hcloud_balancer.id, 248 | delete_service_request: Some(DeleteServiceRequest { 249 | listen_port: service.listen_port, 250 | }), 251 | }, 252 | ) 253 | .await?; 254 | } 255 | } 256 | 257 | for (listen_port, destination_port) in &self.services { 258 | if !hcloud_balancer 259 | .services 260 | .iter() 261 | .any(|s| s.listen_port == *listen_port) 262 | { 263 | tracing::info!( 264 | "Found missing service. Adding service that listens for port {}", 265 | listen_port 266 | ); 267 | hcloud::apis::load_balancers_api::add_service( 268 | &self.hcloud_config, 269 | AddServiceParams { 270 | id: hcloud_balancer.id, 271 | body: Some(LoadBalancerService { 272 | http: None, 273 | listen_port: *listen_port, 274 | destination_port: *destination_port, 275 | protocol: hcloud::models::load_balancer_service::Protocol::Tcp, 276 | proxyprotocol: self.proxy_mode, 277 | health_check: Box::new(LoadBalancerServiceHealthCheck { 278 | http: None, 279 | interval: self.check_interval, 280 | port: *destination_port, 281 | protocol: 282 | hcloud::models::load_balancer_service_health_check::Protocol::Tcp, 283 | retries: self.retries, 284 | timeout: self.timeout, 285 | }), 286 | }), 287 | }, 288 | ) 289 | .await?; 290 | } 291 | } 292 | Ok(()) 293 | } 294 | 295 | /// Reconcile the targets of the load balancer. 296 | /// This method will compare the desired configuration of the targets 297 | /// with the current configuration of the targets in the load balancer. 298 | /// If the configuration does not match, the method will update the target. 299 | async fn reconcile_targets( 300 | &self, 301 | hcloud_balancer: &hcloud::models::LoadBalancer, 302 | ) -> RobotLBResult<()> { 303 | for target in &hcloud_balancer.targets { 304 | let Some(target_ip) = target.ip.clone() else { 305 | continue; 306 | }; 307 | if !self.targets.contains(&target_ip.ip) { 308 | tracing::info!("Removing target {}", target_ip.ip); 309 | hcloud::apis::load_balancers_api::remove_target( 310 | &self.hcloud_config, 311 | RemoveTargetParams { 312 | id: hcloud_balancer.id, 313 | remove_target_request: Some(RemoveTargetRequest { 314 | ip: Some(target_ip), 315 | ..Default::default() 316 | }), 317 | }, 318 | ) 319 | .await?; 320 | } 321 | } 322 | 323 | for ip in &self.targets { 324 | if !hcloud_balancer 325 | .targets 326 | .iter() 327 | .any(|t| t.ip.as_ref().map(|i| i.ip.as_str()) == Some(ip)) 328 | { 329 | tracing::info!("Adding target {}", ip); 330 | hcloud::apis::load_balancers_api::add_target( 331 | &self.hcloud_config, 332 | AddTargetParams { 333 | id: hcloud_balancer.id, 334 | body: Some(LoadBalancerAddTarget { 335 | ip: Some(Box::new(hcloud::models::LoadBalancerTargetIp { 336 | ip: ip.clone(), 337 | })), 338 | ..Default::default() 339 | }), 340 | }, 341 | ) 342 | .await?; 343 | } 344 | } 345 | Ok(()) 346 | } 347 | 348 | /// Reconcile the load balancer algorithm. 349 | /// This method will compare the desired algorithm configuration 350 | /// and update it if it does not match the current configuration. 351 | async fn reconcile_algorithm( 352 | &self, 353 | hcloud_balancer: &hcloud::models::LoadBalancer, 354 | ) -> RobotLBResult<()> { 355 | if *hcloud_balancer.algorithm == self.algorithm.clone().into() { 356 | return Ok(()); 357 | } 358 | tracing::info!( 359 | "Changing load balancer algorithm from {:?} to {:?}", 360 | hcloud_balancer.algorithm, 361 | self.algorithm 362 | ); 363 | hcloud::apis::load_balancers_api::change_algorithm( 364 | &self.hcloud_config, 365 | ChangeAlgorithmParams { 366 | id: hcloud_balancer.id, 367 | body: Some(self.algorithm.clone().into()), 368 | }, 369 | ) 370 | .await?; 371 | Ok(()) 372 | } 373 | 374 | /// Reconcile the load balancer type. 375 | async fn reconcile_lb_type( 376 | &self, 377 | hcloud_balancer: &hcloud::models::LoadBalancer, 378 | ) -> RobotLBResult<()> { 379 | if hcloud_balancer.load_balancer_type.name == self.balancer_type { 380 | return Ok(()); 381 | } 382 | tracing::info!( 383 | "Changing load balancer type from {} to {}", 384 | hcloud_balancer.load_balancer_type.name, 385 | self.balancer_type 386 | ); 387 | hcloud::apis::load_balancers_api::change_type_of_load_balancer( 388 | &self.hcloud_config, 389 | ChangeTypeOfLoadBalancerParams { 390 | id: hcloud_balancer.id, 391 | change_type_of_load_balancer_request: Some(ChangeTypeOfLoadBalancerRequest { 392 | load_balancer_type: self.balancer_type.clone(), 393 | }), 394 | }, 395 | ) 396 | .await?; 397 | Ok(()) 398 | } 399 | 400 | /// Reconcile the network of the load balancer. 401 | /// This method will compare the desired network configuration 402 | /// with the current network configuration of the load balancer. 403 | /// If the configuration does not match, the method will update the 404 | /// network configuration. 405 | async fn reconcile_network( 406 | &self, 407 | hcloud_balancer: &hcloud::models::LoadBalancer, 408 | ) -> RobotLBResult<()> { 409 | // If the network name is not provided, and laod balancer is not attached to any network, 410 | // we can skip this step. 411 | if self.network_name.is_none() && hcloud_balancer.private_net.is_empty() { 412 | return Ok(()); 413 | } 414 | 415 | let desired_network = self.get_network().await?.map(|network| network.id); 416 | // If the network name is not provided, but the load balancer is attached to a network, 417 | // we need to detach it from the network. 418 | let mut contain_desired_network = false; 419 | if !hcloud_balancer.private_net.is_empty() { 420 | for private_net in &hcloud_balancer.private_net { 421 | let Some(private_net_id) = private_net.network else { 422 | continue; 423 | }; 424 | // The load balancer is attached to a target network. 425 | if desired_network == Some(private_net_id) { 426 | // Specific IP was provided, we need to check if the IP is the same. 427 | if self.private_ip.is_some() { 428 | // if IPs match, we can leave everything as it is. 429 | if private_net.ip == self.private_ip { 430 | contain_desired_network = true; 431 | continue; 432 | } 433 | } else { 434 | // No specific IP was provided, we can leave everything as it is. 435 | contain_desired_network = true; 436 | continue; 437 | } 438 | } 439 | tracing::info!("Detaching balancer from network {}", private_net_id); 440 | hcloud::apis::load_balancers_api::detach_load_balancer_from_network( 441 | &self.hcloud_config, 442 | DetachLoadBalancerFromNetworkParams { 443 | id: hcloud_balancer.id, 444 | detach_load_balancer_from_network_request: Some( 445 | DetachLoadBalancerFromNetworkRequest { 446 | network: private_net_id, 447 | }, 448 | ), 449 | }, 450 | ) 451 | .await?; 452 | } 453 | } 454 | if !contain_desired_network { 455 | let Some(network_id) = desired_network else { 456 | return Ok(()); 457 | }; 458 | tracing::info!("Attaching balancer to network {}", network_id); 459 | hcloud::apis::load_balancers_api::attach_load_balancer_to_network( 460 | &self.hcloud_config, 461 | AttachLoadBalancerToNetworkParams { 462 | id: hcloud_balancer.id, 463 | attach_load_balancer_to_network_request: Some( 464 | AttachLoadBalancerToNetworkRequest { 465 | ip: self.private_ip.clone(), 466 | network: network_id, 467 | }, 468 | ), 469 | }, 470 | ) 471 | .await?; 472 | } 473 | Ok(()) 474 | } 475 | 476 | /// Cleanup the load balancer. 477 | /// This method will remove all the services and targets from the 478 | /// load balancer. 479 | pub async fn cleanup(&self) -> RobotLBResult<()> { 480 | let Some(hcloud_balancer) = self.get_hcloud_lb().await? else { 481 | return Ok(()); 482 | }; 483 | for service in &hcloud_balancer.services { 484 | tracing::info!( 485 | "Deleting service that listens for port {} from load-balancer {}", 486 | service.listen_port, 487 | hcloud_balancer.name, 488 | ); 489 | hcloud::apis::load_balancers_api::delete_service( 490 | &self.hcloud_config, 491 | DeleteServiceParams { 492 | id: hcloud_balancer.id, 493 | delete_service_request: Some(DeleteServiceRequest { 494 | listen_port: service.listen_port, 495 | }), 496 | }, 497 | ) 498 | .await?; 499 | } 500 | for target in &hcloud_balancer.targets { 501 | if let Some(target_ip) = target.ip.clone() { 502 | tracing::info!("Removing target {}", target_ip.ip); 503 | hcloud::apis::load_balancers_api::remove_target( 504 | &self.hcloud_config, 505 | RemoveTargetParams { 506 | id: hcloud_balancer.id, 507 | remove_target_request: Some(RemoveTargetRequest { 508 | ip: Some(target_ip), 509 | ..Default::default() 510 | }), 511 | }, 512 | ) 513 | .await?; 514 | } 515 | } 516 | hcloud::apis::load_balancers_api::delete_load_balancer( 517 | &self.hcloud_config, 518 | DeleteLoadBalancerParams { 519 | id: hcloud_balancer.id, 520 | }, 521 | ) 522 | .await?; 523 | Ok(()) 524 | } 525 | 526 | /// Get the load balancer from Hetzner Cloud. 527 | /// This method will try to find the load balancer with the name 528 | /// specified in the `LoadBalancer` struct. 529 | /// 530 | /// The method might return an error if the load balancer is not found 531 | /// or if there are multiple load balancers with the same name. 532 | async fn get_hcloud_lb(&self) -> RobotLBResult> { 533 | let hcloud_balancers = hcloud::apis::load_balancers_api::list_load_balancers( 534 | &self.hcloud_config, 535 | ListLoadBalancersParams { 536 | name: Some(self.name.to_string()), 537 | ..Default::default() 538 | }, 539 | ) 540 | .await?; 541 | if hcloud_balancers.load_balancers.len() > 1 { 542 | tracing::warn!( 543 | "Found more than one balancer with name {}, skipping", 544 | self.name 545 | ); 546 | return Err(RobotLBError::SkipService); 547 | } 548 | // Here we just return the first load balancer, 549 | // if it exists, otherwise we return None 550 | Ok(hcloud_balancers.load_balancers.into_iter().next()) 551 | } 552 | 553 | /// Get or create the load balancer in Hetzner Cloud. 554 | /// 555 | /// this method will try to find the load balancer with the name 556 | /// specified in the `LoadBalancer` struct. If the load balancer 557 | /// is not found, the method will create a new load balancer 558 | /// with the specified configuration in service's annotations. 559 | async fn get_or_create_hcloud_lb(&self) -> RobotLBResult { 560 | let hcloud_lb = self.get_hcloud_lb().await?; 561 | if let Some(balancer) = hcloud_lb { 562 | return Ok(balancer); 563 | } 564 | 565 | let response = hcloud::apis::load_balancers_api::create_load_balancer( 566 | &self.hcloud_config, 567 | hcloud::apis::load_balancers_api::CreateLoadBalancerParams { 568 | create_load_balancer_request: Some(hcloud::models::CreateLoadBalancerRequest { 569 | algorithm: Some(Box::new(self.algorithm.clone())), 570 | labels: None, 571 | load_balancer_type: self.balancer_type.clone(), 572 | location: Some(self.location.clone()), 573 | name: self.name.clone(), 574 | network: None, 575 | network_zone: None, 576 | public_interface: Some(true), 577 | services: Some(vec![]), 578 | targets: Some(vec![]), 579 | }), 580 | }, 581 | ) 582 | .await; 583 | if let Err(e) = response { 584 | tracing::error!("Failed to create load balancer: {:?}", e); 585 | return Err(RobotLBError::HCloudError(format!( 586 | "Failed to create load balancer: {:?}", 587 | e 588 | ))); 589 | } 590 | 591 | Ok(*response.unwrap().load_balancer) 592 | } 593 | 594 | /// Get the network from Hetzner Cloud. 595 | /// This method will try to find the network with the name 596 | /// specified in the `LoadBalancer` struct. It returns `None` only 597 | /// in case the network name is not provided. If the network was not found, 598 | /// the error is returned. 599 | async fn get_network(&self) -> RobotLBResult> { 600 | let Some(network_name) = self.network_name.clone() else { 601 | return Ok(None); 602 | }; 603 | let response = hcloud::apis::networks_api::list_networks( 604 | &self.hcloud_config, 605 | ListNetworksParams { 606 | name: Some(network_name.clone()), 607 | ..Default::default() 608 | }, 609 | ) 610 | .await?; 611 | 612 | if response.networks.len() > 1 { 613 | tracing::warn!( 614 | "Found more than one network with name {}, skipping", 615 | network_name 616 | ); 617 | return Err(RobotLBError::HCloudError(format!( 618 | "Found more than one network with name {}", 619 | network_name, 620 | ))); 621 | } 622 | if response.networks.is_empty() { 623 | tracing::warn!("Network with name {} not found", network_name); 624 | return Err(RobotLBError::HCloudError(format!( 625 | "Network with name {} not found", 626 | network_name, 627 | ))); 628 | } 629 | 630 | Ok(response.networks.into_iter().next()) 631 | } 632 | } 633 | 634 | impl FromStr for LBAlgorithm { 635 | type Err = RobotLBError; 636 | fn from_str(s: &str) -> Result { 637 | match s { 638 | "round-robin" => Ok(Self::RoundRobin), 639 | "least-connections" => Ok(Self::LeastConnections), 640 | _ => Err(RobotLBError::UnknownLBAlgorithm), 641 | } 642 | } 643 | } 644 | 645 | impl From for LoadBalancerAlgorithm { 646 | fn from(value: LBAlgorithm) -> Self { 647 | let r#type = match value { 648 | LBAlgorithm::RoundRobin => hcloud::models::load_balancer_algorithm::Type::RoundRobin, 649 | LBAlgorithm::LeastConnections => { 650 | hcloud::models::load_balancer_algorithm::Type::LeastConnections 651 | } 652 | }; 653 | Self { r#type } 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | // Base lints. 3 | clippy::all, 4 | // Some pedantic lints. 5 | clippy::pedantic, 6 | // New lints which are cool. 7 | clippy::nursery, 8 | )] 9 | #![ 10 | allow( 11 | // I don't care about this. 12 | clippy::module_name_repetitions, 13 | // Yo, the hell you should put 14 | // it in docs, if signature is clear as sky. 15 | clippy::missing_errors_doc 16 | ) 17 | ] 18 | 19 | use clap::Parser; 20 | use config::OperatorConfig; 21 | use error::{RobotLBError, RobotLBResult}; 22 | use futures::StreamExt; 23 | use hcloud::apis::configuration::Configuration as HCloudConfig; 24 | use k8s_openapi::{ 25 | api::core::v1::{Node, Pod, Service}, 26 | serde_json::json, 27 | }; 28 | use kube::{ 29 | api::{ListParams, PatchParams}, 30 | runtime::{controller::Action, watcher, Controller}, 31 | Resource, ResourceExt, 32 | }; 33 | use label_filter::LabelFilter; 34 | use lb::LoadBalancer; 35 | use std::{collections::HashSet, str::FromStr, sync::Arc, time::Duration}; 36 | 37 | pub mod config; 38 | pub mod consts; 39 | pub mod error; 40 | pub mod finalizers; 41 | pub mod label_filter; 42 | pub mod lb; 43 | 44 | #[cfg(not(target_env = "msvc"))] 45 | #[global_allocator] 46 | static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 47 | 48 | #[tokio::main] 49 | async fn main() -> RobotLBResult<()> { 50 | dotenvy::dotenv().ok(); 51 | let operator_config = config::OperatorConfig::parse(); 52 | tracing_subscriber::fmt() 53 | .with_max_level(operator_config.log_level) 54 | .init(); 55 | 56 | let mut hcloud_conf = HCloudConfig::new(); 57 | hcloud_conf.bearer_access_token = Some(operator_config.hcloud_token.clone()); 58 | 59 | tracing::info!("Starting robotlb operator v{}", env!("CARGO_PKG_VERSION")); 60 | let kube_client = kube::Client::try_default().await?; 61 | tracing::info!("Kube client is connected"); 62 | watcher::Config::default(); 63 | let context = Arc::new(CurrentContext::new( 64 | kube_client.clone(), 65 | operator_config.clone(), 66 | hcloud_conf, 67 | )); 68 | tracing::info!("Starting the controller"); 69 | Controller::new( 70 | kube::Api::::all(kube_client), 71 | watcher::Config::default(), 72 | ) 73 | .run(reconcile_service, on_error, context) 74 | .for_each(|reconcilation_result| async move { 75 | match reconcilation_result { 76 | Ok((service, _action)) => { 77 | tracing::info!("Reconcilation of a service {} was successful", service.name); 78 | } 79 | Err(err) => match err { 80 | // During reconcilation process, 81 | // the controller has decided to skip the service. 82 | kube::runtime::controller::Error::ReconcilerFailed( 83 | RobotLBError::SkipService, 84 | _, 85 | ) => {} 86 | _ => { 87 | tracing::error!("Error reconciling service: {:#?}", err); 88 | } 89 | }, 90 | } 91 | }) 92 | .await; 93 | Ok(()) 94 | } 95 | 96 | #[derive(Clone)] 97 | pub struct CurrentContext { 98 | pub client: kube::Client, 99 | pub config: OperatorConfig, 100 | pub hcloud_config: HCloudConfig, 101 | } 102 | impl CurrentContext { 103 | #[must_use] 104 | pub const fn new( 105 | client: kube::Client, 106 | config: OperatorConfig, 107 | hcloud_config: HCloudConfig, 108 | ) -> Self { 109 | Self { 110 | client, 111 | config, 112 | hcloud_config, 113 | } 114 | } 115 | } 116 | 117 | /// Reconcile the service. 118 | /// This function is called by the controller for each service. 119 | /// It will create or update the load balancer based on the service. 120 | /// If the service is being deleted, it will clean up the resources. 121 | #[tracing::instrument(skip(svc,context), fields(service=svc.name_any()))] 122 | pub async fn reconcile_service( 123 | svc: Arc, 124 | context: Arc, 125 | ) -> RobotLBResult { 126 | let svc_type = svc 127 | .spec 128 | .as_ref() 129 | .and_then(|s| s.type_.as_ref()) 130 | .map(String::as_str) 131 | .unwrap_or("ClusterIP"); 132 | if svc_type != "LoadBalancer" { 133 | tracing::debug!("Service type is not LoadBalancer. Skipping..."); 134 | return Err(RobotLBError::SkipService); 135 | } 136 | 137 | let lb_type = svc 138 | .spec 139 | .as_ref() 140 | .and_then(|s| s.load_balancer_class.as_ref()) 141 | .map(String::as_str) 142 | .unwrap_or(consts::ROBOTLB_LB_CLASS); 143 | if lb_type != consts::ROBOTLB_LB_CLASS { 144 | tracing::debug!("Load balancer class is not robotlb. Skipping..."); 145 | return Err(RobotLBError::SkipService); 146 | } 147 | 148 | tracing::info!("Starting service reconcilation"); 149 | 150 | let lb = LoadBalancer::try_from_svc(&svc, &context)?; 151 | 152 | // If the service is being deleted, we need to clean up the resources. 153 | if svc.meta().deletion_timestamp.is_some() { 154 | tracing::info!("Service deletion detected. Cleaning up resources."); 155 | lb.cleanup().await?; 156 | finalizers::remove(context.client.clone(), &svc).await?; 157 | return Ok(Action::await_change()); 158 | } 159 | 160 | // Add finalizer if it's not there yet. 161 | if !finalizers::check(&svc) { 162 | finalizers::add(context.client.clone(), &svc).await?; 163 | } 164 | 165 | // Based on the service type, we will reconcile the load balancer. 166 | reconcile_load_balancer(lb, svc.clone(), context).await 167 | } 168 | 169 | /// Method to get nodes dynamically based on the pods. 170 | /// This method will find the nodes where the target pods are deployed. 171 | /// It will use the pod selector to find the pods and then get the nodes. 172 | async fn get_nodes_dynamically( 173 | svc: &Arc, 174 | context: &Arc, 175 | ) -> RobotLBResult> { 176 | let pod_api = kube::Api::::namespaced( 177 | context.client.clone(), 178 | svc.namespace() 179 | .as_ref() 180 | .map(String::as_str) 181 | .unwrap_or_else(|| context.client.default_namespace()), 182 | ); 183 | 184 | let Some(pod_selector) = svc.spec.as_ref().and_then(|spec| spec.selector.clone()) else { 185 | return Err(RobotLBError::ServiceWithoutSelector); 186 | }; 187 | 188 | let label_selector = pod_selector 189 | .iter() 190 | .map(|(key, val)| format!("{key}={val}")) 191 | .collect::>() 192 | .join(","); 193 | 194 | let pods = pod_api 195 | .list(&ListParams { 196 | label_selector: Some(label_selector), 197 | ..Default::default() 198 | }) 199 | .await?; 200 | 201 | let target_nodes = pods 202 | .iter() 203 | .map(|pod| pod.spec.clone().unwrap_or_default().node_name) 204 | .flatten() 205 | .collect::>(); 206 | 207 | let nodes_api = kube::Api::::all(context.client.clone()); 208 | let nodes = nodes_api 209 | .list(&ListParams::default()) 210 | .await? 211 | .into_iter() 212 | .filter(|node| target_nodes.contains(&node.name_any())) 213 | .collect::>(); 214 | 215 | Ok(nodes) 216 | } 217 | 218 | /// Get nodes based on the node selector. 219 | /// This method will find the nodes based on the node selector 220 | /// from the service annotations. 221 | async fn get_nodes_by_selector( 222 | svc: &Arc, 223 | context: &Arc, 224 | ) -> RobotLBResult> { 225 | let node_selector = svc 226 | .annotations() 227 | .get(consts::LB_NODE_SELECTOR) 228 | .map(String::as_str) 229 | .ok_or(RobotLBError::ServiceWithoutSelector)?; 230 | let label_filter = LabelFilter::from_str(node_selector)?; 231 | let nodes_api = kube::Api::::all(context.client.clone()); 232 | let nodes = nodes_api 233 | .list(&ListParams::default()) 234 | .await? 235 | .into_iter() 236 | .filter(|node| label_filter.check(node.labels())) 237 | .collect::>(); 238 | Ok(nodes) 239 | } 240 | 241 | /// Reconcile the `LoadBalancer` type of service. 242 | /// This function will find the nodes based on the node selector 243 | /// and create or update the load balancer. 244 | pub async fn reconcile_load_balancer( 245 | mut lb: LoadBalancer, 246 | svc: Arc, 247 | context: Arc, 248 | ) -> RobotLBResult { 249 | let mut node_ip_type = "InternalIP"; 250 | if lb.network_name.is_none() { 251 | node_ip_type = "ExternalIP"; 252 | } 253 | 254 | let nodes = if context.config.dynamic_node_selector { 255 | get_nodes_dynamically(&svc, &context).await? 256 | } else { 257 | get_nodes_by_selector(&svc, &context).await? 258 | }; 259 | 260 | for node in nodes { 261 | let Some(status) = node.status else { 262 | continue; 263 | }; 264 | let Some(addresses) = status.addresses else { 265 | continue; 266 | }; 267 | for addr in addresses { 268 | if addr.type_ == node_ip_type { 269 | lb.add_target(&addr.address); 270 | } 271 | } 272 | } 273 | 274 | for port in svc 275 | .spec 276 | .clone() 277 | .unwrap_or_default() 278 | .ports 279 | .unwrap_or_default() 280 | { 281 | let protocol = port.protocol.unwrap_or_else(|| "TCP".to_string()); 282 | if protocol != "TCP" { 283 | tracing::warn!("Protocol {} is not supported. Skipping...", protocol); 284 | continue; 285 | } 286 | let Some(node_port) = port.node_port else { 287 | tracing::warn!( 288 | "Node port is not set for target_port {}. Skipping...", 289 | port.port 290 | ); 291 | continue; 292 | }; 293 | lb.add_service(port.port, node_port); 294 | } 295 | 296 | let svc_api = kube::Api::::namespaced( 297 | context.client.clone(), 298 | svc.namespace() 299 | .unwrap_or_else(|| context.client.default_namespace().to_string()) 300 | .as_str(), 301 | ); 302 | 303 | let hcloud_lb = lb.reconcile().await?; 304 | 305 | let mut ingress = vec![]; 306 | 307 | let dns_ipv4 = hcloud_lb.public_net.ipv4.dns_ptr.flatten(); 308 | let ipv4 = hcloud_lb.public_net.ipv4.ip.flatten(); 309 | let dns_ipv6 = hcloud_lb.public_net.ipv6.dns_ptr.flatten(); 310 | let ipv6 = hcloud_lb.public_net.ipv6.ip.flatten(); 311 | if let Some(ipv4) = &ipv4 { 312 | ingress.push(json!({ 313 | "ip": ipv4, 314 | "dns": dns_ipv4, 315 | "ip_mode": "VIP" 316 | })) 317 | } 318 | if context.config.ipv6_ingress { 319 | if let Some(ipv6) = &ipv6 { 320 | ingress.push(json!({ 321 | "ip": ipv6, 322 | "dns": dns_ipv6, 323 | "ip_mode": "VIP" 324 | })) 325 | } 326 | } 327 | 328 | if !ingress.is_empty() { 329 | svc_api 330 | .patch_status( 331 | svc.name_any().as_str(), 332 | &PatchParams::default(), 333 | &kube::api::Patch::Merge(json!({ 334 | "status" :{ 335 | "loadBalancer": { 336 | "ingress": ingress 337 | } 338 | } 339 | })), 340 | ) 341 | .await?; 342 | } 343 | 344 | Ok(Action::requeue(Duration::from_secs(30))) 345 | } 346 | 347 | /// Handle the error during reconcilation. 348 | #[allow(clippy::needless_pass_by_value)] 349 | fn on_error(_: Arc, error: &RobotLBError, _context: Arc) -> Action { 350 | match error { 351 | RobotLBError::SkipService => Action::await_change(), 352 | _ => Action::requeue(Duration::from_secs(30)), 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | # Deploying Kubernetes tutorial 2 | 3 | For this tutorial we will be setting up HA-kubernetes cluster on hetzner using k3s. 4 | On each server, I will be using the latest Ubuntu LTS release which is `24.04`. 5 | 6 | We start with deploying control-plane nodes. Those nodes won't be serving any workloads, 7 | but only responsible for managing internal Kubernetes jobs. 8 | 9 | 10 | ### Cloud servers 11 | 12 | If you want to go full metal, order servers on the robot. But since these servers won't handle any application workloads, it's fine to deploy not-so-powerful nodes as control planes. 13 | 14 | Let's start by ordering 5 servers. 3 servers will be used as control planes, 1 as a load balancer for Kubernetes API, and 1 as a VPN to access all these services. 15 | You can buy 5 CAX11s which should be sufficient for small and medium-sized clusters and will cost you about 16 EUR. 16 | 17 | ### Setup users 18 | 19 | 20 | As an additional layer of security, you can create a user called k3s on all servers which will be parts of the cluster. 21 | 22 | ```bash 23 | useradd --comment "K3S admin user" --create-home --user-group --shell /bin/bash k3s 24 | 25 | # Add ssh auth keys to k3s user. 26 | mkdir /home/k3s/.ssh 27 | cp .ssh/authorized_keys /home/k3s/.ssh/authorized_keys 28 | chown k3s:k3s /home/k3s/.ssh/authorized_keys 29 | 30 | # Then we add this line to the end of /etc/sudoers file 31 | # This will allow the user to run sudo commands without password 32 | # Required only during the k3s installation 33 | echo "k3s ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers 34 | ``` 35 | 36 | After the installation is complete, remove the k3s from sudoers. 37 | 38 | ```bash 39 | sed -i '/^k3s ALL.*/d' /etc/sudoers 40 | ``` 41 | 42 | 43 | ### Network. 44 | 45 | Once servers have been purchased, let's create a private network. To do so, go to a network tab and create a new network in the same zone as your servers with the subnet `10.10.0.0/16`. This subnet can be anything you want, but if you want to go with any other subnet, please make sure that it doesn't overlap with `--cluster-cidr` (used to give IPs to pods) or `--service-cidr` (used to provide IPs for services). For k3s these values can be found here: https://docs.k3s.io/cli/server#networking. 46 | 47 | Once you have created a network, delete all the generated subnets and start making them from scratch. Let's create the first subnet for our Kubernetes servers in the cloud. It should be any IP subnet within the main one. I will go with `10.10.1.0/24` It gives me 254 possible IPs and should be generally more than enough. 48 | 49 | ### VPN 50 | 51 | Once the network is ready, we can set up a VPN to access servers from a local machine using their private IPs. 52 | To set up a VPN in this tutorial, I will be using WireGuard, but you can use any other VPN you are comfortable with. 53 | 54 | Let's log in to our VPN server with its public IP and install WireGuard. 55 | 56 | ```bash 57 | ssh root@wg-vpn 58 | apt update 59 | apt install -y wireguard 60 | ``` 61 | 62 | Then let's list all network interfaces and find the target interface that our private network is connected to. 63 | 64 | ```bash 65 | $ ip addr 66 | 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 67 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 68 | inet 127.0.0.1/8 scope host lo 69 | valid_lft forever preferred_lft forever 70 | inet6 ::1/128 scope host noprefixroute 71 | valid_lft forever preferred_lft forever 72 | 2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000 73 | link/ether 96:00:03:df:49:47 brd ff:ff:ff:ff:ff:ff 74 | inet 125.111.108.208/32 metric 100 scope global dynamic eth0 75 | valid_lft 82754sec preferred_lft 82754sec 76 | inet6 2a01:4f9:c012:dd8::1/64 scope global 77 | valid_lft forever preferred_lft forever 78 | inet6 fe80::9400:3ff:fedf:4947/64 scope link 79 | valid_lft forever preferred_lft forever 80 | 3: enp7s0: mtu 1450 qdisc fq_codel state UP group default qlen 1000 81 | link/ether 86:00:00:e9:6b:9b brd ff:ff:ff:ff:ff:ff 82 | inet 10.10.1.2/32 brd 10.0.0.2 scope global dynamic enp7s0 83 | valid_lft 82759sec preferred_lft 71959sec 84 | inet6 fe80::8400:ff:fee9:6b9b/64 scope link 85 | valid_lft forever preferred_lft forever 86 | ``` 87 | 88 | As you can see, the address `10.10.1.2/32` is assigned to the interface `enp7s0`. 89 | Now let's create a config that will suit our needs. First, we need public and private keys for the server. Run it on the server. 90 | 91 | 92 | ```bash 93 | wg genkey | tee privatekey | wg pubkey > publickey 94 | ``` 95 | 96 | It will generate public and private keys for the server. 97 | Then you will need to do the same thing for any client that will be connecting to your WireGuard server. 98 | Create a file `/etc/wireguard/wg0.conf` with the following contents (replacing keys and interface names): 99 | 100 | ```ini 101 | [Interface] 102 | # This is an address within Wireguard's own network. 103 | Address = 10.4.0.1/24 104 | # This is our actual address in our Hetzner cloud network. 105 | Address = 10.10.1.2/32 106 | # The port that the server will be listening to. 107 | ListenPort = 5100 108 | # PrivateKey for the server that we have generated. 109 | PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 110 | # This enables IP forwarding before creating a new interface. 111 | PreUp = sysctl net.ipv4.ip_forward=1 112 | # This command allows forwarding packets to the private network on the interface enp7s0 that we have 113 | # looked up previously. 114 | PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp7s0 -j MASQUERADE 115 | # This command will remove forwarding and masquerading after the wireguard is down. 116 | PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp7s0 -j MASQUERADE 117 | # Removing ip_forward setting. 118 | PostDown = sysctl net.ipv4.ip_forward=0 119 | 120 | # This is our client. 121 | [Peer] 122 | # Client's public key. 123 | PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBB= 124 | # Allowed IPs for the client to claim within WireGuard's network. 125 | AllowedIPs = 10.4.0.2/32 126 | ``` 127 | 128 | After the configuration is saved, let's enable this configuration in systemd. 129 | 130 | ```bash 131 | systemctl enable --now wg-quick@wg0 132 | ``` 133 | 134 | Now to configure your client, create a file `/etc/wireguard/wg0.conf` on your machine with the following contents: 135 | 136 | ```ini 137 | [Interface] 138 | # This is an IP of your client in Wireguard's network, it should match AllowedIP on the server side. 139 | Address = 10.4.0.2/32 140 | # Client's private key 141 | PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCC= 142 | 143 | # This is a server configuration. 144 | [Peer] 145 | # Server's public key. 146 | PublicKey = DDDDDDDDDDDDDDDDDDDDDDDDDDDDD= 147 | # Public IP to connect to a server. 148 | Endpoint = 125.111.108.208:5100 149 | # Here you set up which addresses should be routed through this peer. 150 | # Place here your private network's subnet along with the Wireguard's network. 151 | AllowedIPs = 10.10.0.0/16, 10.4.0.0/24 152 | ``` 153 | 154 | Once it's done, verify that this setup works, by running `wg-quick up wg0` on your machine and then try pinging any of the servers in a private subnet. 155 | 156 | ```bash 157 | ❯ ping 10.10.1.3 158 | PING 10.10.1.3 (10.10.1.3) 56(84) bytes of data. 159 | 64 bytes from 10.10.1.3: icmp_seq=1 ttl=62 time=151 ms 160 | 64 bytes from 10.10.1.3: icmp_seq=2 ttl=62 time=89.8 ms 161 | 64 bytes from 10.10.1.3: icmp_seq=3 ttl=62 time=78.2 ms 162 | 64 bytes from 10.10.1.3: icmp_seq=4 ttl=62 time=88.0 ms 163 | 164 | ``` 165 | 166 | If it works, you are ready to proceed. 167 | 168 | ### Firewall 169 | 170 | We don't want our nodes to be accessible from outside. To do so, we set a label on all our cloud servers like `k8s`. 171 | 172 | Then let's create a firewall with no rules and apply it to all servers with the label we created. In my case, it will be `k8s`. 173 | 174 | ### Kube API Load balancing 175 | 176 | To access Kubernetes API you don't want to send requests to a particular server, because if it goes down, you will have to choose the next server to connect to yourself. To fix this issue we will deploy a small load balancer that will be watching over our control-panel nodes and we will be using it as an entry point for our Kubernetes cluster. 177 | 178 | My LoadBalancer server has IP `10.10.1.4`. 179 | 180 | ```bash 181 | ssh root@10.10.1.4 182 | apt update 183 | apt install haproxy 184 | systemctl enable --now haproxy 185 | ``` 186 | 187 | Now, once we have HA-proxy up and running we need to update its config at `/etc/haproxy/haproxy.cfg`. 188 | Here's an example config that you can put in here: 189 | 190 | ```cfg 191 | global 192 | maxconn 1000 193 | log 127.0.0.1 local0 194 | 195 | defaults 196 | log global 197 | mode tcp 198 | retries 2 199 | timeout connect 5s 200 | timeout server 5m 201 | timeout check 5m 202 | 203 | listen stats 204 | mode http 205 | bind *:80 206 | stats enable 207 | stats uri / 208 | 209 | frontend kube-apiserver 210 | bind *:6443 211 | mode tcp 212 | 213 | option tcplog 214 | default_backend kube-apiserver 215 | 216 | backend kube-apiserver 217 | mode tcp 218 | option tcplog 219 | option tcp-check 220 | balance roundrobin 221 | default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100 222 | server kube-apiserver-1 10.10.1.1:6443 check 223 | server kube-apiserver-2 10.10.1.2:6443 check 224 | server kube-apiserver-3 10.10.1.3:6443 check 225 | ``` 226 | 227 | Then reload HA-proxy with `systemctl restart haproxy`. Now you should see your servers on the status dashboard. 228 | Go to `http://10.10.1.4` to verfiy it. 229 | 230 | ### Deploying control panels 231 | 232 | To do that I will be using a simple bash script. Most of the work will be done by an awesome helper [k3sup](https://github.com/alexellis/k3sup). 233 | 234 | Important things to note here: 235 | 236 | * We deploy with `--no-extras` argument which will disable `serviceLB` (aka `klipper`) and `traefik` ingress controller. It's important to disable klipper, because otherwise, it might conflict with `RobotLB`. 237 | * TLS san should include load-balancer IP, otherwise your cert will be rejected. 238 | * Make sure to use the correct flannel interface for inter-node communication. 239 | 240 | 241 | ```bash 242 | #!/bin/bash 243 | # SSH user to use during the installation. You can go with root if you skipped creating a user. 244 | export K3S_USER="k3s" # or "root" 245 | # Path to the SSH key to use during the installation. 246 | export SSH_KEY="" 247 | # Load balancer IP, which will be used as the server IP for agents. 248 | # but for now we need to put this IP in the list of allowed IPs to 249 | # access control plane servers. 250 | export LB_IP="10.10.1.4" 251 | # K3s version to install. 252 | export K3S_VERSION="v1.31.1+k3s1" 253 | # Common arguments to control plane nodes. 254 | # * flannel-iface: The interface for flannel to use, in this case enp7s0, which connected to the private network. 255 | # * kube-proxy-arg=proxy-mode=ipvs: Use IPVS as the kube-proxy mode. Generally, it's more performant. 256 | # * kube-proxy-arg=ipvs-scheduler=lc: Use the least connection scheduler for IPVS. 257 | # * node-taint: Taint the control plane nodes as master nodes to avoid scheduling any workload on them. 258 | export COMMON_ARGS="--flannel-iface=enp7s0 --kube-proxy-arg=ipvs-scheduler=lc --kube-proxy-arg=proxy-mode=ipvs --node-taint node-role.kubernetes.io/master=true:NoSchedule" 259 | 260 | # Join a node to the cluster 261 | function join_server(){ 262 | local SERVER_IP="$1" 263 | local IP="$2" 264 | k3sup join \ 265 | --server \ 266 | --no-extras \ 267 | --ip "$IP" \ 268 | --user "$K3S_USER" \ 269 | --k3s-extra-args "$COMMON_ARGS --node-ip=$IP" \ 270 | --tls-san "$LB_IP,10.10.1.1,10.10.1.2,10.10.1.3" \ 271 | --server-ip "$SERVER_IP" \ 272 | --server-user "$K3S_USER" \ 273 | --ssh-key "$SSH_KEY" \ 274 | --k3s-version "$K3S_VERSION" 275 | } 276 | 277 | # This function is used to set up the first node, 278 | # so it has a `--cluster` flag to initialize the cluster. 279 | function create_cluster(){ 280 | local SERVER_IP="$1" 281 | k3sup install \ 282 | --cluster \ 283 | --no-extras \ 284 | --ip "$SERVER_IP" \ 285 | --k3s-extra-args "$COMMON_ARGS --node-ip=$SERVER_IP" \ # note that node-ip. It will be used by Robotlb to create a cloud load-balancer. 286 | --tls-san "$LB_IP,10.10.1.1,10.10.1.2,10.10.1.3" \ 287 | --user "$K3S_USER" \ 288 | --ssh-key "$SSH_KEY" \ 289 | --k3s-version "$K3S_VERSION" 290 | } 291 | 292 | # We have 3 control plane servers 293 | # 10.10.1.1 294 | # 10.10.1.2 295 | # 10.10.1.3 296 | # Here we deploy the first control plane server 297 | create_cluster "10.10.1.1" 298 | # Connect the second server to the first 299 | join_server "10.10.1.1" "10.10.1.2" 300 | # Connect the third server to the second 301 | join_server "10.10.1.2" "10.10.1.3" 302 | ``` 303 | 304 | After running this script you should see that control planes became healthy on our load-balancer at `http://10.10.1.4`. Also, in your generated kubeconfig file change the address of the server to the load-balancer's address. So it will become: 305 | 306 | 307 | ```yaml 308 | ... 309 | server: https://10.10.1.4:6443 310 | ... 311 | ``` 312 | 313 | Now try running kubectl with this kubeconfig. 314 | 315 | ```bash 316 | $ export KUBECONFIG=kubeconfig 317 | $ kubectl cluster-info 318 | 319 | Kubernetes control plane is running at https://10.10.1.4:6443 320 | CoreDNS is running at https://10.10.1.4:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy 321 | Metrics-server is running at https://10.10.1.4:6443/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy 322 | 323 | To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. 324 | ``` 325 | 326 | You should see that all IPs are pointing to a load-balancer. 327 | 328 | # Robot side 329 | 330 | Now let's create agent nodes on the Hetzner robot. I will buy 2 servers in the same region as our cloud servers. 331 | 332 | After they are ready, create a `vSwitch` and connect robot servers to the switch. But before setting up all the virtual interfaces, 333 | create a `vSwitch` type subnet in our cloud network. 334 | 335 | 336 | ### vSwitch backbone 337 | 338 | 339 | To do so, go to a cloud `network console` > `subnets` > `add subnet` and select a subnet range that you think will be sufficient for all your robot servers. Keep in mind that you can add only one `vSwitch` to a cloud network. I would go with `10.10.192.0/18`, because I want to also host dedicated servers for other purposes, like databases and other things. 340 | 341 | After this is ready, you need to expose routes to the `vSwitch` by clicking on three dots after the `vSwitch` subnet and choosing the appropriate option in the dropdown. 342 | 343 | ### Network interfaces 344 | 345 | After you have chosen the subnet, Hetzner will give you the gateway address that will be used to communicate with cloud servers. Generally, it's the first IP in a chosen subnet. In my scenario, it will be `10.10.192.1`. 346 | 347 | Let's log in to our robot servers by their public IPs and set up private networking interfaces. I won't be using the `ip` command as suggested in a hint by Hetzner, instead, I will be using `netplan` to make my interface setup persistent. 348 | 349 | ```bash 350 | ssh intree-prod-kube-agent-1 351 | vim /etc/netplan/01-netcfg.yaml 352 | ``` 353 | 354 | Here create a new VLAN object inside of the network object. 355 | 356 | ```yaml 357 | network: 358 | ... # here goes some default network configuration. Don't touch it. 359 | 360 | vlans: 361 | enp6s0.4000: # You can name it anyway, but please make the same name for all your servers in this VLAN. 362 | id: 4000 # This is a VLAN id that was chosen when creating vSwitch. 363 | link: enp6s0 # Name of the physical interface. Typically you will have a similar name. 364 | # Check all available interfaces by running `ip addr` 365 | mtu: 1400 # Don't forget to set it, otherwise some packets might mess up. 366 | addresses: 367 | - 10.10.255.1/18 # Here goes the address of this node. Please note that you should use /18, because there's only one subnet 368 | # for all dedicated servers. Therefore for all the IPs in the addresses section, the subnet mask should be /18. 369 | routes: # That section means that to reach any addresses in our cloud network we should go to the gateway. 370 | - to: "10.10.0.0/16" 371 | via: "10.10.192.1" 372 | ``` 373 | 374 | After this is ready, apply the plan and try pinging the load balancer. 375 | 376 | ```bash 377 | netplan generate 378 | netplan apply 379 | ping 10.10.1.4 380 | ``` 381 | 382 | Then do the same thing for the second agent server replacing its address with the one you want. I will go with `10.10.255.2/18`. 383 | 384 | By the way, if you want to get more info about how the connection between the cloud network and vSwitch works, you can follow this tutorial: 385 | https://docs.hetzner.com/cloud/networks/connect-dedi-vswitch/ 386 | 387 | ### Firewall 388 | 389 | Once robot servers are accessible from the private network, we can hide them with the firewall. I would recommend 390 | you to only hide ports that we don't want to expose, because of possible DNS issues. 391 | 392 | ### Deploying agents 393 | 394 | Finally, we got to a part where we started deploying agent nodes. To do so, we will use another bash script. 395 | 396 | I will create a user `k3s` as I have done for control planes, but you can use `root` if you don't want it. 397 | 398 | ```bash 399 | #!/bin/bash 400 | 401 | # Ssh user to use during the installation. 402 | export K3S_USER="k3s" 403 | # SSH key to use during the installation. 404 | export SSH_KEY="" 405 | # Here's a main server, we will use it to get TOKEN for joining 406 | # to the cluster. 407 | export MAIN_SERVER_IP="10.10.1.1" 408 | # Load balancer IP, which will be used as the server IP 409 | # because it load balances the requests to the control plane servers. 410 | # If any of the control plane nodes will go down, agents will reconnect to another automatically. 411 | export LB_IP="10.10.1.4" 412 | # Some extra arguments for the agents: 413 | # * iflannel-iface: The interface for flannel to use, in this case enp6s0.4000, which is connected to the vSwitch. 414 | # * kube-proxy-arg=proxy-mode=ipvs: Use IPVS as the kube-proxy mode. 415 | # * kube-proxy-arg=ipvs-scheduler=lc: Use the least connection scheduler for IPVS. 416 | export K3S_EXTRA_ARGS="--flannel-iface=enp6s0.4000 --kube-proxy-arg=ipvs-scheduler=lc --kube-proxy-arg=proxy-mode=ipvs" 417 | # K3S version to install. 418 | export K3S_VERSION="v1.31.1+k3s1" 419 | 420 | # Join a node to the cluster 421 | function join_agent(){ 422 | local SERVER_IP="$1" 423 | local TOKEN="$2" 424 | local IP="$3" 425 | # again, node-ip argument is important for robotlb to work. 426 | k3sup join \ 427 | --ip "$IP" \ 428 | --user "$K3S_USER" \ 429 | --k3s-extra-args "$K3S_EXTRA_ARGS --node-ip=$IP" \ 430 | --server-ip "$SERVER_IP" \ 431 | --server-user "$K3S_USER" \ 432 | --ssh-key "$SSH_KEY" \ 433 | --node-token "$TOKEN" \ 434 | --k3s-version "$K3S_VERSION" 435 | } 436 | 437 | # Here we get our token from the main node. It will be used to join the agents. 438 | TOKEN="$(k3sup node-token --ip $MAIN_SERVER_IP --ssh-key "$SSH_KEY" --user "$K3S_USER")" 439 | 440 | join_agent "$LB_IP" "$TOKEN" "10.10.255.1" 441 | join_agent "$LB_IP" "$TOKEN" "10.10.255.2" 442 | ``` 443 | 444 | ### Deploying RobotLB 445 | 446 | Once the cluster is ready, we can deploy our RobotLB. Before that, create an hcloud token as shown here: https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/. 447 | 448 | ```bash 449 | helm install robotlb \ 450 | oci://ghcr.io/intreecom/charts/robotlb \ 451 | --set envs.ROBOTLB_HCLOUD_TOKEN="" \ 452 | --namespace robotlb \ 453 | --create-namespace \ 454 | --wait 455 | ``` 456 | 457 | After service LB is deployed, we can deploy the NGINX ingress controller to verify installation. I will set it up as a `DaemonSet` so it will be deployed on all agent nodes. 458 | 459 | Here are the values for helm, that I'm going to use for nginx. 460 | 461 | ```yaml 462 | # nginx-values.yaml 463 | controller: 464 | kind: DaemonSet 465 | admissionWebhooks: 466 | enabled: false 467 | ingressClassResource: 468 | default: true 469 | service: 470 | type: LoadBalancer 471 | annotations: 472 | robotlb/lb-network: "" 473 | robotlb/balancer-type: "lb11" 474 | robotlb/lb-algorithm: "least-connections" 475 | externalTrafficPolicy: "Local" 476 | ``` 477 | 478 | For the full list of parameters, check out `README.md`. 479 | 480 | ```bash 481 | helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx 482 | helm install ingress-nginx \ 483 | ingress-nginx/ingress-nginx \ 484 | --namespace ingress-nginx \ 485 | --create-namespace \ 486 | --wait \ 487 | --values nginx-values.yaml 488 | ``` 489 | 490 | Once the nginx is deployed, verify that the load-balancer is created on Hetzner cloud console. And check that external-ip for the service of type LoadBalancer is an actual public IP of a cloud loadbalancer. 491 | --------------------------------------------------------------------------------