diff --git a/.github/workflows/actions/install-vdev/action.yml b/.github/workflows/actions/install-vdev/action.yml new file mode 100644 index 0000000000000..2d57c70bf5858 --- /dev/null +++ b/.github/workflows/actions/install-vdev/action.yml @@ -0,0 +1,51 @@ +name: Install vdev + +inputs: + token: + required: true + +runs: + using: composite + + steps: + # default 2.x fails + - if: runner.os == 'Windows' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + + - name: Wait for build + uses: lewagon/wait-on-check-action@v1.2.0 + with: + repo-token: ${{ inputs.token }} + check-name: Build executable for ${{ runner.os }} + ref: ${{ github.sha }} + wait-interval: 20 + + - name: Try downloading current artifact + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{ inputs.token }} + workflow: build-vdev.yml + branch: ${{ github.ref_name }} + name: vdev-${{ runner.os }} + path: vdev/bin + + - if: github.event_name == 'pull_request' && failure() + name: Download latest artifact + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{ inputs.token }} + workflow: build-vdev.yml + branch: master + name: vdev-${{ runner.os }} + path: vdev/bin + + - name: Add binary to PATH + run: mv vdev/bin/* "$HOME/.cargo/bin" + shell: bash + + - if: runner.os != 'Windows' + name: Make binary executable + run: chmod +x "$HOME/.cargo/bin/vdev" + shell: bash diff --git a/.github/workflows/build-vdev.yml b/.github/workflows/build-vdev.yml new file mode 100644 index 0000000000000..9cce2ec10ea75 --- /dev/null +++ b/.github/workflows/build-vdev.yml @@ -0,0 +1,43 @@ +name: Build vdev + +on: + push: + branches: + - master + pull_request: + branches: + - master + paths: + - "vdev/**/*" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +jobs: + exe: + # Name must match runner.os https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context + name: Build executable for ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Build release binary + run: | + cd vdev + cargo build --release + + - if: runner.os != 'Windows' + name: Strip binary + run: strip vdev/target/release/vdev + + - uses: actions/upload-artifact@v3 + with: + name: vdev-${{ runner.os }} + path: vdev/target/release/${{ runner.os == 'Windows' && 'vdev.exe' || 'vdev' }} + if-no-files-found: error diff --git a/vdev/Cargo.lock b/vdev/Cargo.lock new file mode 100644 index 0000000000000..759da0aa7bb0f --- /dev/null +++ b/vdev/Cargo.lock @@ -0,0 +1,807 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[package]] +name = "async-trait" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_once" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cached" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b4147cd94d5fbdc2ab71b11d50a2f45493625576b3bb70257f59eedea69f3d" +dependencies = [ + "async-trait", + "async_once", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown", + "instant", + "lazy_static", + "once_cell", + "thiserror", + "tokio", +] + +[[package]] +name = "cached_proc_macro" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751f7f4e7a091545e7f6c65bacc404eaee7e87bfb1f9ece234a1caa173dc16f2" +dependencies = [ + "cached_proc_macro_types", + "darling", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap-verbosity-flag" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e2b6c3dcdb73299f48ae05b294da14e2f560b3ed2c09e742269eb1b22af231" +dependencies = [ + "clap", + "log", +] + +[[package]] +name = "clap_complete" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0fba905b035a30d25c1b585bf1171690712fbb0ad3ac47214963aa4acc36c" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "confy" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" +dependencies = [ + "directories", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dunce" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", + "serde", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "os_info" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4750134fb6a5d49afc80777394ad5d95b04bc12068c6abb92fae8f43817270f" +dependencies = [ + "log", + "winapi", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +dependencies = [ + "supports-color", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15eb2c6e362923af47e13c23ca5afb859e83d54452c55b0b9ac763b8f7c1ac16" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "supports-color" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f" +dependencies = [ + "atty", + "is_ci", +] + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +dependencies = [ + "autocfg", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" + +[[package]] +name = "vdev" +version = "0.1.0" +dependencies = [ + "anyhow", + "atty", + "cached", + "clap", + "clap-verbosity-flag", + "clap_complete", + "confy", + "directories", + "dunce", + "hashlink", + "indicatif", + "itertools", + "log", + "once_cell", + "os_info", + "owo-colors", + "serde", + "serde_json", + "serde_yaml", + "toml", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/vdev/Cargo.toml b/vdev/Cargo.toml new file mode 100644 index 0000000000000..faa795f075c2d --- /dev/null +++ b/vdev/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "vdev" +version = "0.1.0" +edition = "2021" +authors = ["Vector Contributors <vector@datadoghq.com>"] +license = "MPL-2.0" +readme = "README.md" +publish = false + +[dependencies] +anyhow = "1.0.66" +atty = "0.2.14" +cached = "0.40.0" +clap = { version = "4.0.18", features = ["derive"] } +clap-verbosity-flag = "2.0.0" +clap_complete = "4.0.5" +confy = "0.5.1" +directories = "4.0.1" +# remove this when stabilized https://doc.rust-lang.org/stable/std/path/fn.absolute.html +dunce = "1.0.3" +hashlink = { version = "0.8.1", features = ["serde_impl"] } +indicatif = { version = "0.17.1", features = ["improved_unicode"] } +itertools = "0.10.5" +log = "0.4.17" +once_cell = "1.16.0" +os_info = { version = "3.5.1", default-features = false } +# watch https://github.com/epage/anstyle for official interop with Clap +owo-colors = { version = "3.5.0", features = ["supports-colors"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.87" +serde_yaml = "0.9.14" +toml = "0.5.9" + +# https://github.com/rust-lang/cargo/issues/6745#issuecomment-472667516 +[workspace] diff --git a/vdev/README.md b/vdev/README.md new file mode 100644 index 0000000000000..14e599e100cb7 --- /dev/null +++ b/vdev/README.md @@ -0,0 +1,67 @@ +# vdev + +----- + +This is the command line tooling for Vector development. + +Table of Contents: + +- [Installation](#installation) +- [Configuration](#configuration) + - [Repository](#repository) + - [Starship](#starship) +- [CLI](#cli) + +## Installation + +Run the following command from the root of the Vector repository: + +```text +cargo install -f --path vdev +``` + +## Configuration + +### Repository + +Setting the path to the repository explicitly allows the application to be used at any time no matter the current working directory. + +```text +vdev config set repo . +``` + +To test, enter your home directory and then run: + +```text +vdev exec ls +``` + +### Starship + +A custom command for the [Starship](https://starship.rs) prompt is available. + +```toml +format = """ +... +${custom.vdev}\ +... +$line_break\ +... +$character""" + +# <clipped> + +[custom.vdev] +command = "vdev meta starship" +when = true +# Windows +# shell = ["cmd", "/C"] +# Other +# shell = ["sh", "--norc"] +``` + +## CLI + +The CLI uses [Clap](https://github.com/clap-rs/clap) with the `derive` construction mechanism and is stored in the [commands](src/commands) directory. + +Every command group/namespace has its own directory with a `cli` module, including the root `vdev` command group. All commands have an `exec` method that provides the actual implementation, which in the case of command groups will be calling sub-commands. diff --git a/vdev/src/app.rs b/vdev/src/app.rs new file mode 100644 index 0000000000000..d380f2dc88ce2 --- /dev/null +++ b/vdev/src/app.rs @@ -0,0 +1,107 @@ +use std::{borrow::Cow, process::Command, time::Duration}; + +use anyhow::{bail, Result}; +use indicatif::{ProgressBar, ProgressStyle}; +use log::LevelFilter; +use once_cell::sync::OnceCell; + +use crate::config::Config; + +static VERBOSITY: OnceCell<LevelFilter> = OnceCell::new(); +static CONFIG: OnceCell<Config> = OnceCell::new(); +static PATH: OnceCell<String> = OnceCell::new(); + +pub fn verbosity() -> &'static LevelFilter { + VERBOSITY.get().expect("verbosity is not initialized") +} + +pub fn config() -> &'static Config { + CONFIG.get().expect("config is not initialized") +} + +pub fn path() -> &'static String { + PATH.get().expect("path is not initialized") +} + +/// Overlay some extra helper functions onto `std::process::Command` +pub trait CommandExt { + fn with_path(program: &str) -> Self; + fn capture_output(&mut self) -> Result<String>; + fn run(&mut self) -> Result<()>; + fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()>; +} + +impl CommandExt for Command { + fn with_path(program: &str) -> Self { + let mut command = Command::new(program); + command.current_dir(path()); + command + } + + fn capture_output(&mut self) -> Result<String> { + Ok(String::from_utf8(self.output()?.stdout)?) + } + + fn run(&mut self) -> Result<()> { + let status = self.status()?; + if status.success() { + Ok(()) + } else { + bail!( + "command: {} {}\nfailed with exit code: {}", + self.get_program().to_str().expect("Invalid program name"), + self.get_args() + .map(|arg| arg.to_str().expect("Invalid command argument")) + .collect::<Vec<_>>() + .join(" "), + status.code().unwrap() + ) + } + } + + fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()> { + let progress_bar = get_progress_bar()?; + progress_bar.set_message(message); + + let result = self.output(); + progress_bar.finish_and_clear(); + let output = match result { + Ok(output) => output, + Err(_) => bail!("could not run command"), + }; + + if output.status.success() { + Ok(()) + } else { + bail!( + "{}\nfailed with exit code: {}", + String::from_utf8(output.stdout)?, + output.status.code().unwrap() + ) + } + } +} + +fn get_progress_bar() -> Result<ProgressBar> { + let progress_bar = ProgressBar::new_spinner(); + progress_bar.enable_steady_tick(Duration::from_millis(125)); + progress_bar.set_style( + ProgressStyle::with_template("{spinner} {msg:.magenta.bold}")? + // https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json + .tick_strings(&["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]), + ); + + Ok(progress_bar) +} + +pub fn set_global_verbosity(verbosity: LevelFilter) { + VERBOSITY.set(verbosity).expect("could not set verbosity"); +} + +pub fn set_global_config(config: Config) { + CONFIG.set(config).expect("could not set config"); +} + +pub fn set_global_path(path: String) { + PATH.set(path).expect("could not set path"); +} diff --git a/vdev/src/commands/build.rs b/vdev/src/commands/build.rs new file mode 100644 index 0000000000000..c30f04d5f7ebe --- /dev/null +++ b/vdev/src/commands/build.rs @@ -0,0 +1,54 @@ +use std::process::Command; + +use anyhow::Result; +use clap::Args; + +use crate::app::CommandExt as _; +use crate::platform; + +/// Build Vector +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + /// The build target e.g. x86_64-unknown-linux-musl + target: Option<String>, + + /// Build with optimizations + #[arg(short, long)] + release: bool, + + /// The feature to activate (multiple allowed) + #[arg(short = 'F', long)] + feature: Vec<String>, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let mut command = Command::with_path("cargo"); + command.args(["build", "--no-default-features"]); + + if self.release { + command.arg("--release"); + } + + command.arg("--features"); + if !self.feature.is_empty() { + command.args([self.feature.join(",")]); + } else if cfg!(windows) { + command.arg("default-msvc"); + } else { + command.arg("default"); + }; + + if let Some(target) = self.target.as_deref() { + command.args(["--target", target]); + } else { + command.args(["--target", &platform::default_target()]); + }; + + waiting!("Building Vector"); + command.run()?; + + Ok(()) + } +} diff --git a/vdev/src/commands/complete.rs b/vdev/src/commands/complete.rs new file mode 100644 index 0000000000000..e02834c4e6e8a --- /dev/null +++ b/vdev/src/commands/complete.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use clap::{Args, CommandFactory}; +use clap_complete::{generate, Shell}; +use std::io; + +use super::Cli as RootCli; + +/// Display the completion file for a given shell +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + #[arg(value_enum)] + shell: Shell, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let mut cmd = RootCli::command(); + let bin_name = cmd.get_name().to_string(); + generate(self.shell, &mut cmd, bin_name, &mut io::stdout()); + + Ok(()) + } +} diff --git a/vdev/src/commands/config/find.rs b/vdev/src/commands/config/find.rs new file mode 100644 index 0000000000000..aac37ec4f9c56 --- /dev/null +++ b/vdev/src/commands/config/find.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use clap::Args; + +use crate::config; + +/// Locate the config file +#[derive(Args, Debug)] +#[command()] +pub struct Cli {} + +impl Cli { + pub fn exec(self) -> Result<()> { + display!("{}", config::path()?.display()); + + Ok(()) + } +} diff --git a/vdev/src/commands/config/mod.rs b/vdev/src/commands/config/mod.rs new file mode 100644 index 0000000000000..285d3a9cb0632 --- /dev/null +++ b/vdev/src/commands/config/mod.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use clap::{Args, Subcommand}; + +mod find; +mod set; + +/// Manage the vdev config file +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Find(find::Cli), + Set(set::Cli), +} + +impl Cli { + pub fn exec(self) -> Result<()> { + match self.command { + Commands::Find(cli) => cli.exec(), + Commands::Set(cli) => cli.exec(), + } + } +} diff --git a/vdev/src/commands/config/set/mod.rs b/vdev/src/commands/config/set/mod.rs new file mode 100644 index 0000000000000..828adecdeffa6 --- /dev/null +++ b/vdev/src/commands/config/set/mod.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::{Args, Subcommand}; + +mod repo; + +/// Modify the config file +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Repo(repo::Cli), +} + +impl Cli { + pub fn exec(self) -> Result<()> { + match self.command { + Commands::Repo(cli) => cli.exec(), + } + } +} diff --git a/vdev/src/commands/config/set/org.rs b/vdev/src/commands/config/set/org.rs new file mode 100644 index 0000000000000..fc6e37fb02c4f --- /dev/null +++ b/vdev/src/commands/config/set/org.rs @@ -0,0 +1,21 @@ +use anyhow::Result; +use clap::Args; + +use crate::{app, config}; + +/// Set the target Datadog org +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + name: String, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let mut config = app::config().clone(); + config.org = self.name.to_string(); + config::save(config)?; + + Ok(()) + } +} diff --git a/vdev/src/commands/config/set/repo.rs b/vdev/src/commands/config/set/repo.rs new file mode 100644 index 0000000000000..521ddc84edac5 --- /dev/null +++ b/vdev/src/commands/config/set/repo.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use clap::Args; + +use crate::{app, config, platform}; + +/// Set the path to the Vector repository +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + path: String, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let path = platform::canonicalize_path(self.path); + + let mut config = app::config().clone(); + config.repo = path; + config::save(config)?; + + Ok(()) + } +} diff --git a/vdev/src/commands/exec.rs b/vdev/src/commands/exec.rs new file mode 100644 index 0000000000000..ea6935e4b35d9 --- /dev/null +++ b/vdev/src/commands/exec.rs @@ -0,0 +1,26 @@ +use std::process::Command; + +use anyhow::Result; +use clap::Args; + +use crate::app::CommandExt as _; + +/// Execute a command within the repository +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec<String>, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let mut command = Command::with_path(&self.args[0]); + if self.args.len() > 1 { + command.args(&self.args[1..]); + } + + let status = command.status()?; + std::process::exit(status.code().unwrap_or(1)); + } +} diff --git a/vdev/src/commands/integration/mod.rs b/vdev/src/commands/integration/mod.rs new file mode 100644 index 0000000000000..4d6f5bddaff21 --- /dev/null +++ b/vdev/src/commands/integration/mod.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use clap::{Args, Subcommand}; + +mod show; +mod start; +mod stop; +mod test; + +/// Manage integrations +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Show(show::Cli), + Start(start::Cli), + Stop(stop::Cli), + Test(test::Cli), +} + +impl Cli { + pub fn exec(self) -> Result<()> { + match self.command { + Commands::Show(cli) => cli.exec(), + Commands::Start(cli) => cli.exec(), + Commands::Stop(cli) => cli.exec(), + Commands::Test(cli) => cli.exec(), + } + } +} diff --git a/vdev/src/commands/integration/show.rs b/vdev/src/commands/integration/show.rs new file mode 100644 index 0000000000000..4735ba64b6092 --- /dev/null +++ b/vdev/src/commands/integration/show.rs @@ -0,0 +1,56 @@ +use anyhow::{Context, Result}; +use clap::Args; +use std::path::PathBuf; + +use crate::app; +use crate::testing::{config::IntegrationTestConfig, state}; + +/// Show information about integrations +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + /// The desired integration + integration: Option<String>, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + match self.integration { + None => { + let mut entries = vec![]; + let root_dir: PathBuf = [app::path(), "scripts", "integration"].iter().collect(); + for entry in root_dir + .read_dir() + .with_context(|| format!("failed to read directory {}", root_dir.display()))? + { + let entry = entry?; + if entry.path().is_dir() { + entries.push(entry.file_name().into_string().unwrap()); + } + } + entries.sort(); + + for integration in &entries { + display!("{integration}"); + } + } + Some(integration) => { + let (_test_dir, config) = IntegrationTestConfig::load(&integration)?; + let envs_dir = state::envs_dir(&integration); + let active_envs = state::active_envs(&envs_dir)?; + + display!("Test args: {}", config.args.join(" ")); + + display!("Environments:"); + for environment in config.environments().keys() { + if active_envs.contains(environment) { + display!(" {} (active)", environment); + } else { + display!(" {}", environment); + } + } + } + } + Ok(()) + } +} diff --git a/vdev/src/commands/integration/start.rs b/vdev/src/commands/integration/start.rs new file mode 100644 index 0000000000000..029f74fc62aba --- /dev/null +++ b/vdev/src/commands/integration/start.rs @@ -0,0 +1,54 @@ +use std::process::Command; + +use anyhow::{bail, Result}; +use clap::Args; + +use crate::app::CommandExt as _; +use crate::testing::{config::IntegrationTestConfig, runner::*, state}; + +/// Start an environment +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + /// The desired integration + integration: String, + + /// The desired environment + environment: String, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let (test_dir, config) = IntegrationTestConfig::load(&self.integration)?; + + let envs_dir = state::envs_dir(&self.integration); + let runner = IntegrationTestRunner::new(self.integration)?; + runner.ensure_network()?; + + let mut command = Command::new("cargo"); + command.current_dir(&test_dir); + command.env(NETWORK_ENV_VAR, runner.network_name()); + command.args(["run", "--quiet", "--", "start"]); + + let environments = config.environments(); + let json = match environments.get(&self.environment) { + Some(config) => serde_json::to_string(config)?, + None => bail!("unknown environment: {}", self.environment), + }; + command.arg(&json); + + if state::env_exists(&envs_dir, &self.environment) { + bail!("environment is already up"); + } + + if let Some(env_vars) = config.env { + command.envs(env_vars); + } + + waiting!("Starting environment {}", &self.environment); + command.run()?; + + state::save_env(&envs_dir, &self.environment, &json)?; + Ok(()) + } +} diff --git a/vdev/src/commands/integration/stop.rs b/vdev/src/commands/integration/stop.rs new file mode 100644 index 0000000000000..7aa8d63389682 --- /dev/null +++ b/vdev/src/commands/integration/stop.rs @@ -0,0 +1,61 @@ +use anyhow::{bail, Result}; +use clap::Args; +use std::process::Command; + +use crate::app::CommandExt as _; +use crate::testing::{config::IntegrationTestConfig, runner::*, state}; + +/// Stop an environment +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + /// The desired integration + integration: String, + + /// The desired environment + environment: String, + + /// Use the currently defined configuration if the environment is not up + #[arg(short, long)] + force: bool, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let (test_dir, config) = IntegrationTestConfig::load(&self.integration)?; + let envs_dir = state::envs_dir(&self.integration); + let runner = IntegrationTestRunner::new(self.integration.clone())?; + + let mut command = Command::new("cargo"); + command.current_dir(&test_dir); + command.env(NETWORK_ENV_VAR, runner.network_name()); + command.args(["run", "--quiet", "--", "stop"]); + + if state::env_exists(&envs_dir, &self.environment) { + command.arg(state::read_env_config(&envs_dir, &self.environment)?); + } else if self.force { + let environments = config.environments(); + if let Some(config) = environments.get(&self.environment) { + command.arg(serde_json::to_string(config)?); + } else { + bail!("unknown environment: {}", self.environment); + } + } else { + bail!("environment is not up"); + } + + if let Some(env_vars) = config.env { + command.envs(env_vars); + } + + waiting!("Stopping environment {}", &self.environment); + command.run()?; + + state::remove_env(&envs_dir, &self.environment)?; + if state::active_envs(&envs_dir)?.is_empty() { + runner.stop()?; + } + + Ok(()) + } +} diff --git a/vdev/src/commands/integration/test.rs b/vdev/src/commands/integration/test.rs new file mode 100644 index 0000000000000..0109f9e6659bc --- /dev/null +++ b/vdev/src/commands/integration/test.rs @@ -0,0 +1,107 @@ +use anyhow::{bail, Result}; +use clap::Args; +use std::collections::BTreeMap; +use std::process::Command; + +use crate::app::CommandExt as _; +use crate::testing::{config::IntegrationTestConfig, runner::*, state}; + +/// Execute tests +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + /// The desired integration + integration: String, + + /// The desired environment + environment: Option<String>, + + /// Extra test command arguments + args: Option<Vec<String>>, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let (test_dir, config) = IntegrationTestConfig::load(&self.integration)?; + let runner = IntegrationTestRunner::new(self.integration.clone())?; + let envs_dir = state::envs_dir(&self.integration); + let envs = config.environments(); + + let env_vars: BTreeMap<_, _> = config + .env + .clone() + .map_or(BTreeMap::default(), |map| map.into_iter().collect()); + + let mut args: Vec<_> = config.args.into_iter().collect(); + if let Some(configured_args) = self.args { + args.extend(configured_args); + } + + if let Some(environment) = &self.environment { + if !state::env_exists(&envs_dir, environment) { + bail!("environment {environment} is not up"); + } + + return runner.test(&env_vars, &args); + } + + runner.ensure_network()?; + + let active_envs = state::active_envs(&envs_dir)?; + for (env_name, env_config) in envs { + if !(active_envs.is_empty() || active_envs.contains(&env_name)) { + continue; + } + + let env_active = state::env_exists(&envs_dir, &env_name); + if !env_active { + let mut command = Command::new("cargo"); + command.current_dir(&test_dir); + command.env(NETWORK_ENV_VAR, runner.network_name()); + command.args(["run", "--quiet", "--", "start"]); + + let json = serde_json::to_string(&env_config)?; + command.arg(&json); + + if let Some(env_vars) = &config.env { + command.envs(env_vars); + } + + waiting!("Starting environment {}", env_name); + command.run()?; + + state::save_env(&envs_dir, &env_name, &json)?; + } + + runner.test(&env_vars, &args)?; + + if !env_active { + let mut command = Command::new("cargo"); + command.current_dir(&test_dir); + command.env(NETWORK_ENV_VAR, runner.network_name()); + command.args([ + "run", + "--quiet", + "--", + "stop", + &state::read_env_config(&envs_dir, &env_name)?, + ]); + + if let Some(env_vars) = &config.env { + command.envs(env_vars); + } + + waiting!("Stopping environment {}", env_name); + command.run()?; + + state::remove_env(&envs_dir, &env_name)?; + } + } + + if active_envs.is_empty() { + runner.stop()?; + } + + Ok(()) + } +} diff --git a/vdev/src/commands/meta/mod.rs b/vdev/src/commands/meta/mod.rs new file mode 100644 index 0000000000000..2aa4498785a38 --- /dev/null +++ b/vdev/src/commands/meta/mod.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use clap::{Args, Subcommand}; + +mod starship; + +/// Collection of useful utilities +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Starship(starship::Cli), +} + +impl Cli { + pub fn exec(self) -> Result<()> { + match self.command { + Commands::Starship(cli) => cli.exec(), + } + } +} diff --git a/vdev/src/commands/meta/starship.rs b/vdev/src/commands/meta/starship.rs new file mode 100644 index 0000000000000..cb009e0500a23 --- /dev/null +++ b/vdev/src/commands/meta/starship.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use clap::Args; +use std::fs::File; +use std::io::{prelude::*, BufReader}; +use std::path::PathBuf; + +use crate::app; + +const VERSION_START: &str = "version = "; + +/// Custom Starship prompt plugin +#[derive(Args, Debug)] +#[command(hide = true)] +pub struct Cli {} + +impl Cli { + pub fn exec(self) -> Result<()> { + let mut contexts = vec![]; + + let path: PathBuf = [app::path(), "Cargo.toml"].iter().collect(); + if let Ok(file) = File::open(path) { + let reader = BufReader::new(file); + + for line in reader.lines() { + let line = line?; + if line.starts_with(VERSION_START) { + contexts.push(format!( + "version: {}", + &line[VERSION_START.len() + 1..line.len() - 1] + )); + break; + } + } + }; + + display!("vector{{ {} }}", contexts.join(", ")); + + Ok(()) + } +} diff --git a/vdev/src/commands/mod.rs b/vdev/src/commands/mod.rs new file mode 100644 index 0000000000000..48ac7b126b3db --- /dev/null +++ b/vdev/src/commands/mod.rs @@ -0,0 +1,55 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use clap_verbosity_flag::{InfoLevel, Verbosity}; + +mod build; +mod complete; +mod config; +mod exec; +mod integration; +mod meta; +mod status; +mod test; + +/// Vector's unified dev tool +#[derive(Parser, Debug)] +#[command( + version, + bin_name = "vdev", + infer_subcommands = true, + disable_help_subcommand = true +)] +pub struct Cli { + #[clap(flatten)] + pub verbose: Verbosity<InfoLevel>, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Build(build::Cli), + Complete(complete::Cli), + Config(config::Cli), + Exec(exec::Cli), + Integration(integration::Cli), + Meta(meta::Cli), + Status(status::Cli), + Test(test::Cli), +} + +impl Cli { + pub fn exec(self) -> Result<()> { + match self.command { + Commands::Build(cli) => cli.exec(), + Commands::Complete(cli) => cli.exec(), + Commands::Config(cli) => cli.exec(), + Commands::Exec(cli) => cli.exec(), + Commands::Integration(cli) => cli.exec(), + Commands::Meta(cli) => cli.exec(), + Commands::Status(cli) => cli.exec(), + Commands::Test(cli) => cli.exec(), + } + } +} diff --git a/vdev/src/commands/status.rs b/vdev/src/commands/status.rs new file mode 100644 index 0000000000000..ab191ce870c1a --- /dev/null +++ b/vdev/src/commands/status.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use clap::Args; + +use crate::git; + +/// Show information about the current environment +#[derive(Args, Debug)] +#[command()] +pub struct Cli {} + +impl Cli { + pub fn exec(self) -> Result<()> { + display!("Branch: {}", git::current_branch()?); + display!("Changed files: {}", git::changed_files()?.len()); + + Ok(()) + } +} diff --git a/vdev/src/commands/test.rs b/vdev/src/commands/test.rs new file mode 100644 index 0000000000000..92871811efc70 --- /dev/null +++ b/vdev/src/commands/test.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use clap::Args; +use std::collections::BTreeMap; + +use crate::testing::{config::RustToolchainConfig, runner::get_agent_test_runner}; + +/// Execute tests +#[derive(Args, Debug)] +#[command()] +pub struct Cli { + /// Extra test command arguments + args: Option<Vec<String>>, + + /// Whether to run tests in a container + #[arg(short = 'C', long)] + container: bool, + + /// Environment variables in the form KEY[=VALUE] + #[arg(short, long)] + env: Option<Vec<String>>, +} + +impl Cli { + pub fn exec(self) -> Result<()> { + let toolchain_config = RustToolchainConfig::parse()?; + let runner = get_agent_test_runner(self.container, toolchain_config.channel); + + let mut env_vars = BTreeMap::new(); + if let Some(extra_env_vars) = &self.env { + for entry in extra_env_vars { + if let Some((key, value)) = entry.split_once('=') { + env_vars.insert(key.to_string(), value.to_string()); + } else { + env_vars.insert(entry.to_string(), String::new()); + } + } + } + + let mut args = vec!["--workspace".to_string()]; + if let Some(extra_args) = &self.args { + args.extend(extra_args.clone()); + + if !(self.container || extra_args.contains(&"--features".to_string())) { + if cfg!(windows) { + args.extend(["--features".to_string(), "default-msvc".to_string()]); + } else { + args.extend(["--features".to_string(), "default".to_string()]); + } + } + } + + runner.test(&env_vars, &args) + } +} diff --git a/vdev/src/config.rs b/vdev/src/config.rs new file mode 100644 index 0000000000000..4bfc84e149cbc --- /dev/null +++ b/vdev/src/config.rs @@ -0,0 +1,24 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +const APP_NAME: &str = "vdev"; +const FILE_STEM: &str = "config"; + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct Config { + pub repo: String, +} + +pub fn path() -> Result<PathBuf> { + confy::get_configuration_file_path(APP_NAME, FILE_STEM) + .with_context(|| "unable to find the config file") +} + +pub fn load() -> Result<Config> { + confy::load(APP_NAME, FILE_STEM).with_context(|| "unable to load config") +} + +pub fn save(config: Config) -> Result<()> { + confy::store(APP_NAME, FILE_STEM, config).with_context(|| "unable to save config") +} diff --git a/vdev/src/git.rs b/vdev/src/git.rs new file mode 100644 index 0000000000000..1c05b4384eb41 --- /dev/null +++ b/vdev/src/git.rs @@ -0,0 +1,56 @@ +use std::{collections::HashSet, process::Command}; + +use anyhow::Result; + +use crate::app::CommandExt as _; + +pub fn current_branch() -> Result<String> { + let output = capture_output(&["rev-parse", "--abbrev-ref", "HEAD"])?; + Ok(output.trim_end().to_string()) +} + +pub fn changed_files() -> Result<Vec<String>> { + let mut files = HashSet::new(); + + // Committed e.g.: + // A relative/path/to/file.added + // M relative/path/to/file.modified + let output = capture_output(&["diff", "--name-status", "origin/master..."])?; + for line in output.lines() { + if !is_warning_line(line) { + if let Some((_, path)) = line.split_once('\t') { + files.insert(path.to_string()); + } + } + } + + // Tracked + let output = capture_output(&["diff", "--name-only", "HEAD"])?; + for line in output.lines() { + if !is_warning_line(line) { + files.insert(line.to_string()); + } + } + + // Untracked + let output = capture_output(&["ls-files", "--others", "--exclude-standard"])?; + for line in output.lines() { + files.insert(line.to_string()); + } + + let mut sorted = Vec::from_iter(files); + sorted.sort(); + + Ok(sorted) +} + +fn capture_output(args: &[&str]) -> Result<String> { + let mut command = Command::with_path("git"); + command.args(args); + + command.capture_output() +} + +fn is_warning_line(line: &str) -> bool { + line.starts_with("warning: ") || line.contains("original line endings") +} diff --git a/vdev/src/macros.rs b/vdev/src/macros.rs new file mode 100644 index 0000000000000..a48bfb377aea8 --- /dev/null +++ b/vdev/src/macros.rs @@ -0,0 +1,51 @@ +macro_rules! display { + ($($arg:tt)*) => {{ + use owo_colors::OwoColorize; + println!( + "{}", + format!($($arg)*) + // Simply bold rather than bright white for terminals with white backgrounds + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bold()) + ); + }}; +} + +#[allow(unused_macros)] +macro_rules! critical { + ($($arg:tt)*) => {{ + use owo_colors::OwoColorize; + eprintln!( + "{}", + format!($($arg)*) + .if_supports_color(owo_colors::Stream::Stderr, |text| text.bright_red()) + ); + }}; +} + +macro_rules! define_display_macro { + // https://github.com/rust-lang/rust/issues/35853#issuecomment-415993963 + // https://github.com/rust-lang/rust/issues/83527#issuecomment-1281176235 + ($name:ident, $level:ident, $style:ident, $d:tt) => ( + #[allow(unused_macros)] + macro_rules! $name { + ($d($d arg:tt)*) => {{ + use owo_colors::OwoColorize; + if log::Level::$level <= *$crate::app::verbosity() { + eprintln!( + "{}", + format!($d($d arg)*) + .if_supports_color(owo_colors::Stream::Stderr, |text| text.$style()) + ); + } + }}; + } + ); +} + +define_display_macro!(trace, Trace, underline, $); +define_display_macro!(debug, Debug, italic, $); +define_display_macro!(info, Info, bold, $); +define_display_macro!(success, Info, bright_cyan, $); +define_display_macro!(waiting, Info, bright_magenta, $); +define_display_macro!(warn, Warn, bright_yellow, $); +define_display_macro!(error, Error, bright_red, $); diff --git a/vdev/src/main.rs b/vdev/src/main.rs new file mode 100644 index 0000000000000..a1bf9caf8f879 --- /dev/null +++ b/vdev/src/main.rs @@ -0,0 +1,41 @@ +#![deny(clippy::pedantic)] +#![allow( + clippy::module_name_repetitions, + clippy::unused_self, + clippy::wildcard_imports, + clippy::unnecessary_wraps +)] + +#[macro_use] +mod macros; +mod app; +mod commands; +mod config; +mod git; +mod platform; +mod testing; + +use anyhow::Result; +use clap::Parser; +use std::env; + +use commands::Cli; + +fn main() -> Result<()> { + let cli = Cli::parse(); + + app::set_global_verbosity(cli.verbose.log_level_filter()); + app::set_global_config(config::load()?); + + let path = if app::config().repo.is_empty() { + env::current_dir() + .expect("Could not determine current directory") + .display() + .to_string() + } else { + app::config().repo.clone() + }; + app::set_global_path(path); + + cli.exec() +} diff --git a/vdev/src/platform.rs b/vdev/src/platform.rs new file mode 100644 index 0000000000000..1e9c1754f0766 --- /dev/null +++ b/vdev/src/platform.rs @@ -0,0 +1,34 @@ +use cached::proc_macro::once; +use directories::ProjectDirs; + +use std::env::consts::ARCH; +use std::path::{Path, PathBuf}; + +pub fn canonicalize_path(path: impl AsRef<Path>) -> String { + let path = path.as_ref(); + dunce::canonicalize(path) + .unwrap_or_else(|err| panic!("Could not canonicalize path {path:?}: {err}")) + .display() + .to_string() +} + +#[once] +pub fn data_dir() -> PathBuf { + _project_dirs().data_local_dir().to_path_buf() +} + +#[once] +pub fn default_target() -> String { + if cfg!(windows) { + format!("{ARCH}-pc-windows-msvc") + } else if cfg!(macos) { + format!("{ARCH}-apple-darwin") + } else { + format!("{ARCH}-unknown-linux-gnu") + } +} + +#[once] +fn _project_dirs() -> ProjectDirs { + ProjectDirs::from("", "vector", "vdev").expect("Could not determine the project directory") +} diff --git a/vdev/src/testing/config.rs b/vdev/src/testing/config.rs new file mode 100644 index 0000000000000..548bfc50c4406 --- /dev/null +++ b/vdev/src/testing/config.rs @@ -0,0 +1,104 @@ +use anyhow::{bail, Context, Result}; +use hashlink::LinkedHashMap; +use itertools::{self, Itertools}; +use serde::Deserialize; + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::app; + +const FILE_NAME: &str = "test.yaml"; + +#[derive(Deserialize, Debug)] +pub struct RustToolchainRootConfig { + pub toolchain: RustToolchainConfig, +} + +#[derive(Deserialize, Debug)] +pub struct RustToolchainConfig { + pub channel: String, +} + +impl RustToolchainConfig { + pub fn parse() -> Result<Self> { + let repo_path = app::path(); + let config_file: PathBuf = [repo_path, "rust-toolchain.toml"].iter().collect(); + let contents = fs::read_to_string(&config_file) + .with_context(|| format!("failed to read {}", config_file.display()))?; + let config: RustToolchainRootConfig = toml::from_str(&contents) + .with_context(|| format!("failed to parse {}", config_file.display()))?; + + Ok(config.toolchain) + } +} + +#[derive(Deserialize, Clone, Debug)] +pub struct IntegrationTestConfig { + pub args: Vec<String>, + pub env: Option<BTreeMap<String, String>>, + matrix: Vec<LinkedHashMap<String, Vec<String>>>, +} + +impl IntegrationTestConfig { + fn parse_file(config_file: &Path) -> Result<Self> { + let contents = fs::read_to_string(config_file) + .with_context(|| format!("failed to read {}", config_file.display()))?; + let config: IntegrationTestConfig = serde_yaml::from_str(&contents).with_context(|| { + format!( + "failed to parse integration test configuration file {}", + config_file.display() + ) + })?; + + Ok(config) + } + + pub fn environments(&self) -> LinkedHashMap<String, LinkedHashMap<String, String>> { + let mut environments = LinkedHashMap::new(); + + for matrix in &self.matrix { + for product in matrix.values().multi_cartesian_product() { + let mut config = LinkedHashMap::new(); + for (variable, &value) in matrix.keys().zip(product.iter()) { + config.insert(variable.clone(), value.clone()); + } + + environments.insert(product.iter().join("-"), config); + } + } + + environments + } + + pub fn load(integration: &str) -> Result<(PathBuf, Self)> { + let test_dir: PathBuf = [app::path(), "scripts", "integration", integration] + .iter() + .collect(); + if !test_dir.is_dir() { + bail!("unknown integration: {}", integration); + } + + let config = Self::parse_file(&test_dir.join(FILE_NAME))?; + Ok((test_dir, config)) + } + + #[allow(dead_code)] + pub fn collect_all(root: &str) -> Result<BTreeMap<String, Self>> { + let mut configs = BTreeMap::new(); + let tests_dir: PathBuf = [root, "scripts", "integration"].iter().collect(); + for entry in tests_dir.read_dir()? { + let entry = entry?; + if !entry.path().is_dir() { + continue; + } + + let config_file: PathBuf = [entry.path().to_str().unwrap(), FILE_NAME].iter().collect(); + let config = Self::parse_file(&config_file)?; + configs.insert(entry.file_name().into_string().unwrap(), config); + } + + Ok(configs) + } +} diff --git a/vdev/src/testing/mod.rs b/vdev/src/testing/mod.rs new file mode 100644 index 0000000000000..fa895888889d4 --- /dev/null +++ b/vdev/src/testing/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod runner; +pub mod state; diff --git a/vdev/src/testing/runner.rs b/vdev/src/testing/runner.rs new file mode 100644 index 0000000000000..015094203f93e --- /dev/null +++ b/vdev/src/testing/runner.rs @@ -0,0 +1,358 @@ +use std::collections::{BTreeMap, HashSet}; +use std::path::PathBuf; +use std::process::Command; + +use anyhow::Result; +use atty::Stream; + +use super::config::RustToolchainConfig; +use crate::app::{self, CommandExt as _}; + +pub const NETWORK_ENV_VAR: &str = "VECTOR_NETWORK"; +const MOUNT_PATH: &str = "/home/vector"; +const TARGET_PATH: &str = "/home/target"; +const VOLUME_TARGET: &str = "vector_target"; +const VOLUME_CARGO_GIT: &str = "vector_cargo_git"; +const VOLUME_CARGO_REGISTRY: &str = "vector_cargo_registry"; +const TEST_COMMAND: &[&str] = &[ + "cargo", + "nextest", + "run", + "--no-fail-fast", + "--no-default-features", +]; + +enum RunnerState { + Running, + Restarting, + Created, + Exited, + Paused, + Dead, + Missing, + Unknown, +} + +pub fn get_agent_test_runner(container: bool, rust_version: String) -> Box<dyn TestRunner> { + if container { + Box::new(DockerTestRunner::new(rust_version)) + } else { + Box::new(LocalTestRunner::new()) + } +} + +pub trait TestRunner { + fn test(&self, env_vars: &BTreeMap<String, String>, args: &[String]) -> Result<()>; +} + +pub trait ContainerTestRunnerBase: TestRunner { + fn container_name(&self) -> String; + + fn image_name(&self) -> String; + + fn network_name(&self) -> String { + "host".to_string() + } + + fn stop(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.args(["stop", "--time", "0", &self.container_name()]); + + command.wait(format!("Stopping container {}", self.container_name())) + } +} + +trait ContainerTestRunner: ContainerTestRunnerBase { + fn get_rust_version(&self) -> &str; + + fn state(&self) -> Result<RunnerState> { + let mut command = Command::new("docker"); + command.args(["ps", "-a", "--format", "{{.Names}} {{.State}}"]); + + for line in command.capture_output()?.lines() { + if let Some((name, state)) = line.split_once(' ') { + if name == self.container_name() { + return Ok(match state { + "created" => RunnerState::Created, + "dead" => RunnerState::Dead, + "exited" => RunnerState::Exited, + "paused" => RunnerState::Paused, + "restarting" => RunnerState::Restarting, + "running" => RunnerState::Running, + _ => RunnerState::Unknown, + }); + } + } + } + + Ok(RunnerState::Missing) + } + + fn verify_state(&self) -> Result<()> { + match self.state()? { + RunnerState::Running | RunnerState::Restarting => (), + RunnerState::Created | RunnerState::Exited => self.start()?, + RunnerState::Paused => self.unpause()?, + RunnerState::Dead | RunnerState::Unknown => { + self.remove()?; + self.create()?; + self.start()?; + } + RunnerState::Missing => { + self.build()?; + self.ensure_volumes()?; + self.create()?; + self.start()?; + } + } + + Ok(()) + } + + fn ensure_volumes(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.args(["volume", "ls", "--format", "{{.Name}}"]); + + let mut volumes = HashSet::new(); + volumes.insert(VOLUME_TARGET); + volumes.insert(VOLUME_CARGO_GIT); + volumes.insert(VOLUME_CARGO_REGISTRY); + for volume in command.capture_output()?.lines() { + volumes.take(volume); + } + + for volume in &volumes { + let mut command = Command::new("docker"); + command.args(["volume", "create", volume]); + + command.wait(format!("Creating volume {volume}"))?; + } + + Ok(()) + } + + fn build(&self) -> Result<()> { + let dockerfile: PathBuf = [app::path(), "scripts", "integration", "Dockerfile"] + .iter() + .collect(); + let mut command = Command::new("docker"); + command.current_dir(app::path()); + command.arg("build"); + if atty::is(Stream::Stdout) { + command.args(["--progress", "tty"]); + } + command.args([ + "--pull", + "--tag", + &self.image_name(), + "--file", + dockerfile.to_str().unwrap(), + "--build-arg", + &format!("RUST_VERSION={}", self.get_rust_version()), + ".", + ]); + + waiting!("Building image {}", self.image_name()); + command.run() + } + + fn start(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.args(["start", &self.container_name()]); + + command.wait(format!("Starting container {}", self.container_name())) + } + + fn remove(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.args(["rm", &self.container_name()]); + + command.wait(format!("Removing container {}", self.container_name())) + } + + fn unpause(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.args(["unpause", &self.container_name()]); + + command.wait(format!("Unpausing container {}", self.container_name())) + } + + fn create(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.arg("create"); + command.args([ + "--name", + &self.container_name(), + "--network", + &self.network_name(), + "--workdir", + MOUNT_PATH, + "--volume", + &format!("{}:{MOUNT_PATH}", app::path()), + "--volume", + &format!("{VOLUME_TARGET}:{TARGET_PATH}"), + "--volume", + &format!("{VOLUME_CARGO_GIT}:/usr/local/cargo/git"), + "--volume", + &format!("{VOLUME_CARGO_REGISTRY}:/usr/local/cargo/registry"), + &self.image_name(), + "/bin/sleep", + "infinity", + ]); + + command.wait(format!("Creating container {}", self.container_name())) + } +} + +pub struct IntegrationTestRunner { + integration: String, + rust_version: String, +} + +impl IntegrationTestRunner { + pub fn new(integration: String) -> Result<Self> { + let rust_version = RustToolchainConfig::parse()?.channel; + Ok(Self { + integration, + rust_version, + }) + } + + pub fn ensure_network(&self) -> Result<()> { + let mut command = Command::new("docker"); + command.args(["network", "ls", "--format", "{{.Name}}"]); + + if command + .capture_output()? + .lines() + .any(|network| network == self.network_name()) + { + return Ok(()); + } + + let mut command = Command::new("docker"); + command.args(["network", "create", &self.network_name()]); + + command.wait("Creating network") + } +} + +impl TestRunner for IntegrationTestRunner { + fn test(&self, env_vars: &BTreeMap<String, String>, args: &[String]) -> Result<()> { + self.verify_state()?; + + let mut command = Command::new("docker"); + command.arg("exec"); + if atty::is(Stream::Stdout) { + command.arg("--tty"); + } + + command.args(["--env", &format!("CARGO_BUILD_TARGET_DIR={TARGET_PATH}")]); + for (key, value) in env_vars { + command.env(key, value); + command.args(["--env", key]); + } + + command.arg(&self.container_name()); + command.args(TEST_COMMAND); + command.args(args); + + command.run() + } +} + +impl ContainerTestRunnerBase for IntegrationTestRunner { + fn network_name(&self) -> String { + format!("vector-integration-tests-{}", self.integration) + } + + fn container_name(&self) -> String { + format!( + "vector-test-runner-{}-{}", + self.integration, self.rust_version + ) + } + + fn image_name(&self) -> String { + format!("{}:latest", self.container_name()) + } +} + +impl ContainerTestRunner for IntegrationTestRunner { + fn get_rust_version(&self) -> &str { + &self.rust_version + } +} + +pub struct DockerTestRunner { + rust_version: String, +} + +impl DockerTestRunner { + pub fn new(rust_version: String) -> Self { + Self { rust_version } + } +} + +impl TestRunner for DockerTestRunner { + fn test(&self, env_vars: &BTreeMap<String, String>, args: &[String]) -> Result<()> { + self.verify_state()?; + + let mut command = Command::new("docker"); + command.arg("exec"); + if atty::is(Stream::Stdout) { + command.arg("--tty"); + } + + command.args(["--env", &format!("CARGO_BUILD_TARGET_DIR={TARGET_PATH}")]); + for (key, value) in env_vars { + command.env(key, value); + command.args(["--env", key]); + } + + command.arg(&self.container_name()); + command.args(TEST_COMMAND); + command.args(args); + + command.run() + } +} + +impl ContainerTestRunnerBase for DockerTestRunner { + fn container_name(&self) -> String { + format!("vector-test-runner-{}", self.rust_version) + } + + fn image_name(&self) -> String { + // The upstream container we publish artifacts to on a successful master build. + "docker.io/timberio/vector-dev:sha-3eadc96742a33754a5859203b58249f6a806972a".to_string() + } +} + +impl ContainerTestRunner for DockerTestRunner { + fn get_rust_version(&self) -> &str { + &self.rust_version + } +} + +pub struct LocalTestRunner {} + +impl LocalTestRunner { + pub fn new() -> Self { + Self {} + } +} + +impl TestRunner for LocalTestRunner { + fn test(&self, env_vars: &BTreeMap<String, String>, args: &[String]) -> Result<()> { + let mut command = Command::new(TEST_COMMAND[0]); + command.args(&TEST_COMMAND[1..]); + command.args(args); + + for (key, value) in env_vars { + command.env(key, value); + } + + command.run() + } +} diff --git a/vdev/src/testing/state.rs b/vdev/src/testing/state.rs new file mode 100644 index 0000000000000..29ecfb260ad00 --- /dev/null +++ b/vdev/src/testing/state.rs @@ -0,0 +1,75 @@ +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; + +use crate::platform; + +const CONFIG_FILE: &str = "config.json"; + +pub fn envs_dir(integration: &str) -> PathBuf { + [ + &platform::data_dir(), + Path::new("integration/envs"), + Path::new(integration), + ] + .iter() + .collect() +} + +pub fn env_exists(envs_dir: &Path, environment: &str) -> bool { + let dir: PathBuf = [envs_dir, Path::new(environment)].iter().collect(); + dir.is_dir() +} + +pub fn active_envs(envs_dir: &Path) -> Result<HashSet<String>> { + let mut environments = HashSet::new(); + if !envs_dir.is_dir() { + return Ok(environments); + } + + for entry in envs_dir + .read_dir() + .with_context(|| format!("failed to read directory {}", envs_dir.display()))? + { + let entry = entry + .with_context(|| format!("failed to read directory entry {}", envs_dir.display()))?; + if entry.path().is_dir() { + environments.insert(entry.file_name().into_string().unwrap()); + } + } + + Ok(environments) +} + +pub fn save_env(envs_dir: &Path, environment: &str, config: &str) -> Result<()> { + let mut path = envs_dir.join(environment); + if !path.is_dir() { + fs::create_dir_all(&path) + .with_context(|| format!("failed to create directory {}", path.display()))?; + } + + path.push(CONFIG_FILE); + fs::write(&path, config).with_context(|| format!("failed to write file {}", path.display()))?; + + Ok(()) +} + +pub fn read_env_config(envs_dir: &Path, environment: &str) -> Result<String> { + let mut config_file = envs_dir.join(environment); + config_file.push(CONFIG_FILE); + + fs::read_to_string(&config_file) + .with_context(|| format!("failed to write file {}", config_file.display())) +} + +pub fn remove_env(envs_dir: &Path, environment: &str) -> Result<()> { + let env_path = envs_dir.join(environment); + if env_path.is_dir() { + fs::remove_dir_all(&env_path) + .with_context(|| format!("failed to remove directory {env_path:?}"))?; + } + + Ok(()) +}