diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..9a5d853 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-gnu-gcc" +rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc"] + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea6233f..552d3fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,63 @@ on: pull_request: - branches: - - master + branches: [master] + types: [synchronize, opened] name: CI +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" jobs: check: - name: Check, Format, Lint + name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: mozilla-actions/sccache-action@v0.0.3 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - name: Check - uses: actions-rs/cargo@v1 - with: - command: check + run: cargo check + + format: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - name: Format - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all --check + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - name: Lint - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings + run: cargo clippy + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Test + run: cargo test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55124ce..5a37a4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,33 +12,25 @@ env: jobs: check: - name: Check, Format, Lint + name: Check, Format, Lint, Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: mozilla-actions/sccache-action@v0.0.3 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - name: Check - uses: actions-rs/cargo@v1 - with: - command: check + run: cargo check - name: Format - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all --check - name: Lint - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings + run: cargo clippy + + - name: Test + run: cargo test build: name: Build on ${{ matrix.os }} for ${{ matrix.arch }} @@ -56,8 +48,9 @@ jobs: arch: aarch64 steps: - - uses: actions/checkout@v3 - - uses: mozilla-actions/sccache-action@v0.0.3 + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: Swatinem/rust-cache@v2 - name: Compute Linux Target id: linux-target @@ -84,10 +77,7 @@ jobs: run: sudo apt-get update && sudo apt install musl-tools - name: Build Target ${{ steps.target.outputs.target }} - uses: actions-rs/cargo@v1 - with: - command: build - args: --release --target ${{ steps.target.outputs.target }} + run: cargo build --release --target ${{ steps.target.outputs.target }} - name: Upload Build Artifact uses: actions/upload-artifact@v3 @@ -102,7 +92,9 @@ jobs: - build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: mozilla-actions/sccache-action@v0.0.4 + - uses: Swatinem/rust-cache@v2 - name: Download build artifacts for x86_64-unknown-linux-gnu uses: actions/download-artifact@v3 diff --git a/Cargo.lock b/Cargo.lock index 8db2a94..215fb1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -82,6 +91,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + [[package]] name = "argon2" version = "0.5.1" @@ -246,17 +261,25 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bwenv" -version = "1.0.0" +version = "1.2.0" dependencies = [ + "anyhow", "bitwarden", "clap", "clap-markdown", - "log", + "colored", + "format_serde_error", + "inquire", "openssl", + "semver", "serde", - "simple_logger", + "serde_yaml", + "tabular", + "tempfile", "tokio", "toml", + "tracing", + "tracing-subscriber", "uuid", ] @@ -357,7 +380,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", ] [[package]] @@ -374,11 +397,10 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ - "is-terminal", "lazy_static", "windows-sys 0.48.0", ] @@ -414,6 +436,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -462,38 +509,21 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - [[package]] name = "errno" -version = "0.3.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fnv" @@ -525,6 +555,19 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "format_serde_error" +version = "0.3.0" +source = "git+https://github.com/AlexanderThaller/format_serde_error.git?branch=main#b114501c468bfe4f0a8c3f48f84530414bdeeaa1" +dependencies = [ + "colored", + "serde", + "serde_json", + "serde_yaml", + "toml", + "unicode-segmentation", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -564,6 +607,24 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -605,7 +666,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -618,12 +679,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - [[package]] name = "heck" version = "0.4.1" @@ -742,17 +797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" -dependencies = [ - "equivalent", - "hashbrown 0.14.0", + "hashbrown", ] [[package]] @@ -766,21 +811,27 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.8.0" +name = "inquire" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "a3bc24f3f114e409501843dda15ef84dccd13404a364101b5b7ca94f5e756513" +dependencies = [ + "bitflags 2.4.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "ipnet" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.48.0", -] +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "itoa" @@ -808,9 +859,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -818,11 +869,17 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -840,6 +897,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.6.0" @@ -878,6 +944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -900,6 +967,25 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -1007,7 +1093,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", ] [[package]] @@ -1038,6 +1124,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1056,7 +1148,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", "windows-sys 0.45.0", ] @@ -1143,18 +1235,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1199,14 +1291,49 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.3.5" +name = "regex" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ - "bitflags 1.3.2", + "aho-corasick", + "memchr", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "reqwest" version = "0.11.20" @@ -1275,15 +1402,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.9" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1356,24 +1483,30 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" -version = "1.0.188" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", ] [[package]] @@ -1417,28 +1550,31 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", ] [[package]] -name = "serde_spanned" -version = "0.6.3" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ + "form_urlencoded", + "itoa", + "ryu", "serde", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "serde_yaml" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "form_urlencoded", - "itoa", + "indexmap", "ryu", "serde", + "yaml-rust", ] [[package]] @@ -1463,6 +1599,36 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1482,17 +1648,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "simple_logger" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2230cd5c29b815c9b699fb610b49a5ed65588f3509d9f0108be3a885da629333" -dependencies = [ - "colored", - "log", - "windows-sys 0.42.0", -] - [[package]] name = "slab" version = "0.4.9" @@ -1569,26 +1724,34 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tabular" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a2882c514780a1973df90de9d68adcd8871bacc9a6331c3f28e6d2ff91a3d1" +dependencies = [ + "unicode-width", +] + [[package]] name = "tempfile" -version = "3.8.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1608,7 +1771,17 @@ checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -1653,7 +1826,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", ] [[package]] @@ -1682,62 +1855,78 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", ] [[package]] -name = "toml_datetime" -version = "0.6.3" +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "serde", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "toml_edit" -version = "0.20.2" +name = "tracing-attributes" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "indexmap 2.0.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "proc-macro2", + "quote", + "syn 2.0.52", ] [[package]] -name = "tower-service" -version = "0.3.2" +name = "tracing-core" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] [[package]] -name = "tracing" -version = "0.1.37" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "cfg-if", - "pin-project-lite", + "log", + "once_cell", "tracing-core", ] [[package]] -name = "tracing-core" -version = "0.1.31" +name = "tracing-subscriber" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", + "nu-ansi-term", "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1782,6 +1971,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "url" version = "2.4.1" @@ -1808,6 +2009,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1856,7 +2063,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -1890,7 +2097,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1933,21 +2140,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -1966,6 +2158,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -1996,6 +2197,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2008,6 +2224,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2020,6 +2242,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2032,6 +2260,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2044,6 +2278,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2056,6 +2296,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2068,6 +2314,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2081,13 +2333,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "winnow" -version = "0.5.15" +name = "windows_x86_64_msvc" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" -dependencies = [ - "memchr", -] +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" @@ -2099,6 +2348,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index af25b64..c80d497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,26 @@ [package] name = "bwenv" -version = "1.0.0" +version = "1.2.0" edition = "2021" [dependencies] bitwarden = { version = "0.3.1", features = ["secrets"] } clap = { version = "4.4.6", features = ["derive", "cargo", "env"] } clap-markdown = "0.1.3" -log = "0.4.20" serde = "1.0.188" -simple_logger = { version = "4.2.0", default-features = false, features = [ - "colors", -] } tokio = { version = "1.33.0", features = ["full"] } -toml = "0.8.2" +toml = "0.5" uuid = "1.4.1" openssl = { version = "0.10", features = ["vendored"] } +semver = "1.0.22" +serde_yaml = "0.8.26" +format_serde_error = { git = "https://github.com/AlexanderThaller/format_serde_error.git", branch = "main" } +anyhow = "1.0.81" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +inquire = "0.7.2" +tabular = "0.2.0" +colored = "2.1.0" -[target.aarch64-unknown-linux-musl] -linker = "aarch64-linux-gnu-gcc" -rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc"] - -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" -rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc"] +[dev-dependencies] +tempfile = "3.10.1" diff --git a/README.md b/README.md index 37f7b38..714a116 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,29 @@ CLI for injecting secrets from Bitwarden Secrets Manager into a process ## Installation ### Using `cargo` + +Install latest version: + ```sh cargo install --git ssh://git@github.com/titanom/bwenv-rs.git ``` +or a specific version (e.g. v1.2.0): + +```sh +cargo install --git ssh://git@github.com/titanom/bwenv-rs.git --rev v1.2.0 +``` + ### Manual Download + Download the latest release from GitHub for your operating system. Available targets include: + - aarch64-apple-darwin (MacOS ARM) - x84_64-unknown-linux-gnu (GNU-based x86_64 linux distros) Unzip the archive & add it to PATH: + ```sh # replace with the name of the downloaded archive unzip ~/Downloads/.zip @@ -23,20 +35,43 @@ mv ~/Downloads//bwenv ~/.local/bin chmod +x ~/.local/bin/bwenv ``` - ## Usage ```txt -Usage: bwenv [OPTIONS] [-- ...] +Usage: bwenv [OPTIONS] [-- ...] [COMMAND] + +Commands: + cache Manage the cache of a given profile + inspect Inspect the secrets of a given profile + help Print this message or the help of the given subcommand(s) Arguments: [SLOP]... + Options: - -t, --token access token for the service account [env: BWS_ACCESS_TOKEN=] - -p, --profile profile for loading project configuration [env: BWENV_PROFILE=] - -h, --help Print help (see more with '--help') - -V, --version Print version + -t, --token + Access token for the service account + + [env: BWS_ACCESS_TOKEN] + + -p, --profile + Profile for loading project configuration + + [env: BWENV_PROFILE=] + + -l, --log-level + Set the log level + + [env: BWENV_LOG_LEVEL=] + [default: info] + [possible values: error, warn, info, debug, trace] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version ``` ### `` @@ -49,6 +84,7 @@ This can be any command, you would normally run in your shell, just prefixed wit Access token for the service account of your project. Can be configured using the env variable `BWS_ACCESS_TOKEN` or using the `--token` option. Evaluation has the following order: + 1. `--token` option 2. `BWS_ACCESS_TOKEN` env variable @@ -57,18 +93,45 @@ Evaluation has the following order: Profile for loading project configuration. Can be configured using the env variable `BWENV_PROFILE`, one of the variables defined in the configuration file or using the `--profile` option. Evaluation has the following order: + 1. `--profile` option 2. `BWENV_PROFILE` env variable -3. variables of the `environment` configuration starting with the first ## Configuration -The configuration file `bwenv.toml`, located in the root of your project must be used to configure profiles & caching behavior. +### Yaml + +The configuration file `bwenv.y[a]ml`, located in the root of your project must be used to configure profiles & caching behavior. This file should be committed, don't worry about leaking project IDs - they are not secret. -The only secret, you must *never* commit, is `BWS_ACCESS_TOKEN`, which therefore can not be configured using the config file. +The only secret, you must _never_ commit, is `BWS_ACCESS_TOKEN`, which therefore can not be configured using the config file. + +```yaml +version: 1.2 + +cache: + path: node_modules/.cache + +global: + overrides: + FORCE_COLOR: 1 + +profiles: + default: + project-id: + + development: + project-id: + + production: + project-id: + overrides: + FORCE_COLOR: 0 +``` + +### Toml (Deprecated) ```toml -environment = ["MY_ENV", "NODE_ENV"] +version = "1.2" # default project if no profile option is provided, useful for local development project = "" @@ -96,17 +159,18 @@ project = "" ### Network Issues & Bitwarden Incident -If for whatever reason, the Bitwarden API is not available - set `cache.max_age` to a very large number like 31556926 (1 year) to make sure the cache is always read. +If for whatever reason, the Bitwarden API is not available - set `cache.max_age` to a very large number like 31556926 (1 year) to make sure the cache is always read. -If it is your first time running `bwenv`, your only option is to manually retrieve the secrets from the Bitwarden Website and create the cache-file yourself. +If it is your first time running `bwenv`, your only option is to manually retrieve the secrets from the Bitwarden Website and create the cache-file yourself. -The location of the file is `/bwenv/.toml`. -If you use the default project without a profile, replace `` with `no_profile`. -```toml -# replace this with the current UNIX timestamp -last_revalidation = 1694986302222 +The location of the file is `/bwenv/.yaml`. -[variables] -KEY = "" -OTHER_KEY = "" +```yaml +--- +# replace this with the current UNIX timestamp +last_revalidation: 1694986302222 +version: 1.2.0 +variables: + KEY: + OTHER_KEY: "" ``` diff --git a/bwenv.toml b/bwenv.toml index b319668..789a878 100644 --- a/bwenv.toml +++ b/bwenv.toml @@ -1,3 +1,5 @@ +version = "1.2" + environment = ["MY_ENV", "NODE_ENV"] project = "227f4033-fdbb-482b-847c-b06600dc8e69" diff --git a/bwenv.yaml b/bwenv.yaml new file mode 100644 index 0000000..e9f24dc --- /dev/null +++ b/bwenv.yaml @@ -0,0 +1,20 @@ +version: 1.2 + +cache: + path: node_modules/.cache + +global: + overrides: + FORCE_COLOR: 1 + OPENAI_API_KEYS: default overrides + +profiles: + default: + project-id: 227f4033-fdbb-482b-847c-b06600dc8e69 + + development: + project-id: 227f4033-fdbb-482b-847c-b06600dc8e69 + + production: + project-id: 227f4033-fdbb-482b-847c-b06600dc8e69 + diff --git a/src/bitwarden.rs b/src/bitwarden.rs index 714633a..ac3907f 100644 --- a/src/bitwarden.rs +++ b/src/bitwarden.rs @@ -6,6 +6,8 @@ use bitwarden::{ }; use uuid::Uuid; +use crate::config_yaml::Secrets; + pub struct BitwardenClient { _identity_url: String, _api_url: String, @@ -46,9 +48,12 @@ impl BitwardenClient { } } - pub async fn get_secrets_by_project_id(&mut self, project_id: String) -> Vec<(String, String)> { + pub async fn get_secrets_by_project_id<'a, T: AsRef>( + &mut self, + project_id: T, + ) -> Secrets<'a> { let secrets_by_project_request = SecretIdentifiersByProjectRequest { - project_id: Uuid::parse_str(&project_id).unwrap(), + project_id: Uuid::parse_str(project_id.as_ref()).unwrap(), }; let secret_identifiers = self diff --git a/src/cache.rs b/src/cache.rs index 8b1e4be..74d55ae 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,51 +1,84 @@ +use crate::time::is_date_older_than_n_seconds; +use semver::Version; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fs, future::Future, path::PathBuf, time::SystemTime}; +use std::{fs, future::Future, path::PathBuf, time::SystemTime}; +use tracing::info; + +use crate::config_yaml::Secrets; + +mod version_serde { + use semver::Version; + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(version: &Version, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&version.to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) + } +} #[derive(Debug, Serialize, Deserialize)] -pub struct CacheEntry { +pub struct CacheEntry<'a> { last_revalidation: u64, - pub variables: BTreeMap, + pub variables: Secrets<'a>, + #[serde(with = "version_serde")] + pub version: Version, } -pub struct Cache { +pub struct Cache<'a> { pub directory: PathBuf, + version: &'a Version, } -impl Cache { - pub fn new(directory: PathBuf) -> Self { - Self { +impl<'a> Cache<'a> { + pub fn new(directory: PathBuf, version: &'a Version) -> Self { + Cache::<'a> { directory: directory.join("bwenv"), + version, } } pub fn get(&self, profile: &str) -> Option { let cache_file_path = self.get_cache_file_path(profile); let cache_entry = std::fs::read_to_string(cache_file_path).ok()?; - let cache_entry: CacheEntry = toml::from_str(&cache_entry).ok()?; + let cache_entry: CacheEntry = serde_yaml::from_str(&cache_entry).ok()?; Some(cache_entry) } - pub async fn get_or_revalidate( + pub async fn get_or_revalidate<'b, RevalidateFn, ReturnValue>( &self, profile: &str, - max_age: u64, + max_age: &u64, revalidate: RevalidateFn, ) -> Option where RevalidateFn: FnOnce() -> ReturnValue, - ReturnValue: Future>, + ReturnValue: Future>, { match self.is_stale(profile, max_age) { true => { + info!(message = format!("Revalidating cache for profile {:?}", profile)); let secrets = revalidate().await; - self.set(profile, &secrets); + self.set(profile, secrets); + self.get(profile) + } + false => { + info!(message = format!("Using cached values for profile {:?}", profile)); self.get(profile) } - false => self.get(profile), } } - pub fn set(&self, profile: &str, vars: &[(String, String)]) { + pub fn set(&self, profile: &str, variables: Secrets) { let cache_file_path = self.get_cache_file_path(profile); fs::create_dir_all(self.directory.clone()).unwrap(); let cache_entry = CacheEntry { @@ -53,60 +86,110 @@ impl Cache { .duration_since(SystemTime::UNIX_EPOCH) .expect("SystemTime before UNIX EPOCH!") .as_millis() as u64, - variables: vars.iter().cloned().collect(), + version: self.version.clone(), + variables, }; - let cache_entry = toml::to_string(&cache_entry).unwrap(); + let cache_entry = serde_yaml::to_string(&cache_entry).unwrap(); std::fs::write(cache_file_path, cache_entry).unwrap(); } pub fn clear(&self, profile: &str) { + info!(message = format!("Clearing cache for profile {:?}", profile)); let cache_file_path = self.get_cache_file_path(profile); let _ = fs::remove_file(cache_file_path); } pub fn invalidate(&self, profile: &str) { + info!(message = format!("Invalidating cache for profile {:?}", profile)); if let Some(cache_entry) = self.get(profile) { let cache_file_path = self.get_cache_file_path(profile); fs::create_dir_all(self.directory.clone()).unwrap(); let cache_entry = CacheEntry { last_revalidation: 0, + version: self.version.clone(), variables: cache_entry.variables, }; - let cache_entry = toml::to_string(&cache_entry).unwrap(); + let cache_entry = serde_yaml::to_string(&cache_entry).unwrap(); std::fs::write(cache_file_path, cache_entry).unwrap(); } } - fn is_stale(&self, profile: &str, seconds: u64) -> bool { + fn is_stale(&self, profile: &str, seconds: &u64) -> bool { let cache_entry = self.get(profile); - - match cache_entry { + match &cache_entry { None => true, Some(cache_entry) => { is_date_older_than_n_seconds(cache_entry.last_revalidation, seconds) + || self.version != &cache_entry.version } } } fn get_cache_file_path(&self, profile: &str) -> PathBuf { let mut cache_file_path = self.directory.join(profile); - cache_file_path.set_extension("toml"); + cache_file_path.set_extension("yaml"); cache_file_path } - - // pub fn revalidate(&self, profile: &str) -> () { - // let cache_entry = self.get(profile).unwrap(); - // } } -fn is_date_older_than_n_seconds(unix_millis: u64, n_seconds: u64) -> bool { - let date_seconds = unix_millis / 1000; +#[cfg(test)] +mod tests { + use super::*; + use std::{borrow::Cow, collections::HashMap}; + use tempfile::tempdir; + + fn setup_test_environment() -> (PathBuf, Version) { + let temp_dir = tempdir().unwrap().into_path(); + let version = Version::parse("1.0.0").unwrap(); + (temp_dir, version) + } + + #[test] + fn test_new() { + let (temp_dir, version) = setup_test_environment(); + let cache = Cache::new(temp_dir.clone(), &version); + + assert_eq!(cache.directory, temp_dir.join("bwenv")); + assert_eq!(cache.version, &version); + } + + #[tokio::test] + async fn test_get_and_set() { + let (temp_dir, version) = setup_test_environment(); + let cache = Cache::new(temp_dir, &version); + let profile = "test_profile"; - let current_time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("SystemTime before UNIX EPOCH!"); + let variables: HashMap, Cow> = + [("key".into(), "value".into())].iter().cloned().collect(); + let secrets = Secrets(variables); - let threshold_time = current_time.as_secs() - n_seconds; + cache.set(profile, secrets.clone()); - date_seconds < threshold_time + let cache_entry = cache.get(profile).expect("Failed to get cache entry"); + assert_eq!(cache_entry.variables, secrets); + } + + #[tokio::test] + async fn test_clear_and_invalidate() { + let (temp_dir, version) = setup_test_environment(); + let cache = Cache::new(temp_dir.clone(), &version); + let profile = "test_profile"; + + let variables: HashMap, Cow> = + [("key".into(), "value".into())].iter().cloned().collect(); + let secrets = Secrets(variables); + + cache.set(profile, secrets.clone()); + assert!(cache.get(profile).is_some()); + + cache.clear(profile); + assert!(cache.get(profile).is_none()); + + cache.set(profile, secrets); + cache.invalidate(profile); + let cache_entry = cache + .get(profile) + .expect("Failed to get cache entry after invalidation"); + assert_eq!(cache_entry.last_revalidation, 0); + } } diff --git a/src/cli.rs b/src/cli.rs index 2cc2053..430804b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -12,28 +12,65 @@ pub struct Cli { #[arg( short, long, - long_help = "access token for the service account", - help = "access token for the service account", + long_help = "Access token for the service account", + help = "Access token for the service account", env = "BWS_ACCESS_TOKEN", - required = false + required = false, + hide_env_values = true )] pub token: String, #[arg( short, long, - long_help = "profile for loading project configuration", - help = "profile for loading project configuration", + long_help = "Profile for loading project configuration", + help = "Profile for loading project configuration", env = "BWENV_PROFILE", required = false )] pub profile: Option, + + #[arg( + short, + long, + value_enum, + default_value_t = LogLevel::Info, + help = "Set the log level", + env = "BWENV_LOG_LEVEL", + required = false + )] + pub log_level: LogLevel, +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl LogLevel { + pub fn as_tracing_env(&self) -> String { + match self { + LogLevel::Error => "error".to_string(), + LogLevel::Warn => "warn".to_string(), + LogLevel::Info => "info".to_string(), + LogLevel::Debug => "debug".to_string(), + LogLevel::Trace => "trace".to_string(), + } + } } #[derive(Subcommand, Debug)] pub enum Command { #[command(subcommand)] + /// Manage the cache of a given profile Cache(CacheCommand), + + /// Inspect the secrets of a given profile + Inspect(InspectArgs), } #[derive(Subcommand, Debug)] @@ -44,3 +81,15 @@ pub enum CacheCommand { /// invalidate the cache of a given profile Invalidate, } + +#[derive(Parser, Debug)] +pub struct InspectArgs { + #[arg( + short, + long, + default_value_t = false, + help = "reveal secrets in output", + long_help = "reveal secrets in output" + )] + pub reveal: bool, +} diff --git a/src/config.rs b/src/config.rs index d67891b..b126682 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,134 +1,86 @@ -use serde::Deserialize; -use std::{collections::BTreeMap, env, fs::File, io::Read, path::PathBuf}; +use crate::error::ConfigError; +use crate::fs::find_up; -use crate::error::Error; +use std::path::{Path, PathBuf}; -type Override = Option>; - -#[derive(Debug, Deserialize)] -pub struct Profile { - pub project: Option, - pub environment: Option, - pub r#override: Override, -} - -#[derive(Debug, Deserialize)] -pub struct Cache { - pub max_age: Option, - // stale_while_revalidate: Option, - pub path: String, -} - -#[derive(Debug, Deserialize)] -pub struct Config { - pub environment: Option>, - pub cache: Cache, - pub project: Option, - pub r#override: Override, - pub profile: BTreeMap, - #[serde(skip)] - pub path: String, +#[derive(Debug)] +pub enum LocalConfig { + Yaml(PathBuf), + Toml(PathBuf), } -pub struct ConfigEvaluation { - pub profile_name: String, - pub project_id: String, - pub max_age: u64, - pub r#override: Override, -} - -impl Config { - pub fn new() -> Self { - let config_file_path = find_local_config().unwrap(); - let mut config = parse_config_file(&config_file_path).unwrap(); - config.profile.insert( - String::from("no_profile"), - Profile { - environment: None, - project: config.project.to_owned(), - r#override: config.r#override.to_owned(), - }, - ); - config - } - - pub fn evaluate(&self, profile: Option) -> Result { - let max_age = self.cache.max_age.unwrap_or(86400); - - let default_environment: Vec = std::vec::Vec::new(); - let env_var_names = self.environment.as_ref().unwrap_or(&default_environment); - - let profile_name = match profile { - Some(profile) => profile, - None => get_profile_from_env(env_var_names).unwrap_or(String::from("no_profile")), - }; - - let profile = self - .profile - .get(&profile_name) - .ok_or(Error::ProfileNotConfigured)?; - - let project = profile - .project - .as_ref() - .or_else(|| { - log::error!("could not find project configuration"); - std::process::exit(1); - }) - .unwrap(); - - Ok(ConfigEvaluation { - profile_name: profile_name.to_string(), - project_id: project.to_string(), - max_age, - r#override: profile.r#override.clone(), - }) +impl LocalConfig { + pub fn as_pathbuf(&self) -> &PathBuf { + match self { + Self::Yaml(path) => path, + Self::Toml(path) => path, + } } } -fn find_up(filename: &str, max_parents: Option) -> Option { - let current_dir = env::current_dir().ok()?; - let mut current_path = current_dir.as_path(); +pub fn find_local_config(cwd: Option<&Path>) -> anyhow::Result { + let yaml_config = ["bwenv.yaml", "bwenv.yml"] + .iter() + .find_map(|filename| find_up(filename, None, cwd)); - for _ in 0..max_parents.unwrap_or(10) { - let file_path = current_path.join(filename); + if let Some(path) = yaml_config { + return Ok(LocalConfig::Yaml(path)); + } - if file_path.exists() { - return Some(file_path); - } + let toml_config = find_up("bwenv.toml", None, cwd); - match current_path.parent() { - Some(parent) => current_path = parent, - None => break, - } + if let Some(path) = toml_config { + return Ok(LocalConfig::Toml(path)); } - None + Err(ConfigError::NotFound) } -fn parse_config_file(file_path: &PathBuf) -> Result { - let mut toml_str = String::new(); - let mut file = File::open(file_path).unwrap(); - file.read_to_string(&mut toml_str).unwrap(); +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + // Helper function to write a dummy config file + fn create_config_file(dir: &Path, filename: &str) { + let file_path = dir.join(filename); + let mut file = File::create(file_path).expect("Failed to create test config file."); + writeln!(file, "name: TestConfig").expect("Failed to write to test config file."); + } - let mut config: Config = toml::from_str(&toml_str).unwrap(); - config.path = file_path.to_str().unwrap().to_owned(); + #[test] + fn finds_yaml_config_in_current_dir() { + let temp_dir = tempdir().unwrap(); + create_config_file(&temp_dir.path().to_path_buf(), "bwenv.yaml"); - Ok(config) -} + let result = find_local_config(Some(temp_dir.path())); + assert!(result.is_ok()); + let config = result.unwrap(); + assert!(matches!(config, LocalConfig::Yaml(_))); + } -fn find_local_config() -> Option { - find_up("bwenv.toml", None) -} + #[test] + fn finds_toml_config_in_parent_dir() { + let temp_dir = tempdir().unwrap(); + let child_dir = temp_dir.path().join("child"); + std::fs::create_dir(&child_dir).unwrap(); + create_config_file(&temp_dir.path().to_path_buf(), "bwenv.toml"); + + let result = find_local_config(Some(&child_dir)); + assert!(result.is_ok()); + let config = result.unwrap(); + assert!(matches!(config, LocalConfig::Toml(_))); + } -fn get_profile_from_env(env_var_names: &Vec) -> Option { - let mut existing_env_vars = Vec::new(); + #[test] + fn config_not_found_returns_error() { + let temp_dir = tempdir().unwrap(); - for env_var_name in env_var_names { - if let Ok(env_var_value) = env::var(env_var_name) { - existing_env_vars.push(env_var_value); - } + let result = find_local_config(Some(temp_dir.path())); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(matches!(error, ConfigError::NotFound)); } - - existing_env_vars.first().map(|s| s.to_string()) } diff --git a/src/config_toml.rs b/src/config_toml.rs new file mode 100644 index 0000000..41a91d3 --- /dev/null +++ b/src/config_toml.rs @@ -0,0 +1,104 @@ +use anyhow::anyhow; +use format_serde_error::{ErrorTypes, SerdeError}; +use serde::Deserialize; +use std::{collections::BTreeMap, fs::File, io::Read, path::Path}; +use tracing::info; + +use crate::config_yaml::{self, Profiles}; + +use crate::error::ConfigError; + +#[derive(Debug, Deserialize, Clone)] +pub struct Profile<'a> { + pub project: Option, + pub environment: Option, + #[serde(default)] + pub r#override: config_yaml::Secrets<'a>, +} + +#[derive(Debug, Deserialize)] +pub struct Cache { + #[serde(default)] + pub max_age: config_yaml::CacheMaxAge, + pub path: config_yaml::CachePath, +} + +#[derive(Debug, Deserialize)] +pub struct Config<'a> { + pub version: String, + pub environment: Option>, + pub cache: Cache, + pub project: Option, + pub r#override: config_yaml::Secrets<'a>, + pub profile: BTreeMap>, + #[serde(skip)] + pub path: String, +} + +fn convert_toml_profile_to_yaml_profile(toml_profile: Profile<'_>) -> config_yaml::Profile<'_> { + config_yaml::Profile { + project_id: toml_profile.project.unwrap(), + overrides: toml_profile.r#override, + } +} + +impl Config<'_> { + pub fn new>(config_file_path: P) -> anyhow::Result { + if config_file_path + .as_ref() + .extension() + .and_then(std::ffi::OsStr::to_str) + != Some("toml") + { + return Err(anyhow!("Configuration file must be a .toml file")); + } + + let mut config = parse_config_file(config_file_path)?; + config.profile.insert( + String::from("default"), + Profile { + environment: None, + project: config.project.clone(), + r#override: config.r#override.clone(), + }, + ); + Ok(config) + } + + pub fn as_yaml_config<'a>(&'a self) -> crate::config_yaml::Config<'_> { + crate::config_yaml::Config::<'a> { + version: self.version.clone(), + path: self.path.clone(), + global: Some(config_yaml::Global { + overrides: self.r#override.clone(), + }), + profiles: Profiles::new( + > as Clone>::clone(&self.profile) + .into_iter() + .map(|(key, toml_profile)| { + let yaml_profile = convert_toml_profile_to_yaml_profile(toml_profile); + (key, yaml_profile) + }) + .collect(), + ), + cache: config_yaml::Cache { + path: config_yaml::CachePath(self.cache.path.as_pathbuf().clone()), + max_age: config_yaml::CacheMaxAge(*self.cache.max_age.as_u64()), + }, + } + } +} + +fn parse_config_file<'a, P: AsRef>(file_path: P) -> Result, anyhow::Error> { + if let Some(path) = file_path.as_ref().to_str() { + info!(message = format!("Using configuration file at {:?}", path)); + } + let mut raw = String::new(); + let mut file = File::open(file_path) + .map_err(|_| ConfigError::Read) + .unwrap(); + let _ = file.read_to_string(&mut raw); + + Ok(toml::from_str::(&raw) + .map_err(|err| SerdeError::new(raw.to_string(), ErrorTypes::Toml(err)))?) +} diff --git a/src/config_yaml.rs b/src/config_yaml.rs new file mode 100644 index 0000000..4688b5d --- /dev/null +++ b/src/config_yaml.rs @@ -0,0 +1,320 @@ +use colored::Colorize; +use format_serde_error::{ErrorTypes, SerdeError}; +use semver::VersionReq; +use serde::{Deserialize, Deserializer, Serialize}; +use std::{ + borrow::Cow, + collections::HashMap, + fs::File, + io::Read, + path::{Path, PathBuf}, +}; +use tabular::{Row, Table}; +use tracing::info; + +use crate::error::ConfigError; + +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CacheMaxAge(pub u64); + +impl Default for CacheMaxAge { + fn default() -> Self { + CacheMaxAge(86400) + } +} + +impl CacheMaxAge { + pub fn as_u64(&self) -> &u64 { + &self.0 + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CachePath(pub PathBuf); + +impl Default for CachePath { + fn default() -> Self { + CachePath(PathBuf::from(".cache")) + } +} + +impl CachePath { + pub fn as_pathbuf(&self) -> &PathBuf { + &self.0 + } +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Cache { + // TODO: make private + #[serde(default)] + pub path: CachePath, + #[serde(default, rename = "max-age")] + pub max_age: CacheMaxAge, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] +pub struct Secrets<'a>(pub HashMap, Cow<'a, str>>); + +impl<'a> FromIterator<(String, String)> for Secrets<'a> { + fn from_iter>(iter: I) -> Self { + let mut map = HashMap::new(); + for (key, value) in iter { + map.insert(Cow::Owned(key), Cow::Owned(value)); + } + Secrets(map) + } +} + +impl<'a> Secrets<'a> { + pub fn as_hash_map(&self) -> &HashMap, Cow<'a, str>> { + &self.0 + } + + pub fn merge(a: &'a Secrets<'a>, b: &'a Secrets<'a>) -> Secrets<'a> { + Secrets( + a.as_hash_map() + .iter() + .chain(b.as_hash_map().iter()) + .map(|(k, v)| (Cow::Borrowed(k.as_ref()), Cow::Borrowed(v.as_ref()))) + .collect(), + ) + } + + pub fn as_vec(&mut self) -> Vec<(String, String)> { + self.as_hash_map() + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect() + } + + pub fn table(&self, reveal: bool) -> String { + let mut table = Table::new("{:>} :: {:<}"); + let map = self.as_hash_map(); + for (key, value) in map.iter() { + table.add_row(Row::new().with_cell(key).with_cell(if reveal { + value.normal() + } else { + "**redacted**".italic().dimmed() + })); + } + table.to_string() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Global<'a> { + #[serde( + default, + rename = "overrides", + deserialize_with = "deserialize_null_default" + )] + pub overrides: Secrets<'a>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Profile<'a> { + #[serde(rename = "project-id")] + pub project_id: String, + #[serde( + default, + rename = "overrides", + deserialize_with = "deserialize_null_default" + )] + pub overrides: Secrets<'a>, +} + +type ProfilesMap<'a> = HashMap>; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Profiles<'a>(ProfilesMap<'a>); + +impl<'a> Profiles<'a> { + pub fn new(hash_map: ProfilesMap<'a>) -> Self { + Self(hash_map) + } + + pub fn get(&self, key: &str) -> Result<&Profile, ConfigError> { + self.0.get(key).ok_or(ConfigError::NoProfile) + } +} + +#[derive(Debug, Serialize, Deserialize)] +// TODO: make fields private +pub struct Config<'a> { + pub version: String, + pub cache: Cache, + pub global: Option>, + pub profiles: Profiles<'a>, + #[serde(skip)] + pub path: String, +} + +#[derive(Debug)] +pub struct ConfigEvaluation<'a> { + pub version_req: VersionReq, + pub profile_name: &'a str, + pub project_id: &'a str, + pub max_age: &'a CacheMaxAge, + pub overrides: Secrets<'a>, +} + +impl<'a> Config<'a> { + pub fn new>(config_file_path: P) -> Result { + parse_config_file(config_file_path) + } + + pub fn evaluate<'b>( + &'b self, + profile_name: &'b str, + ) -> Result, ConfigError> { + let profile = self.profiles.get(profile_name)?; + + info!(message = format!("Using profile {:?}", profile_name)); + + let version = VersionReq::parse(&self.version).unwrap(); + + let global_overrides = &self.global.as_ref().unwrap().overrides; + let profile_overrides = &profile.overrides; + + let overrides = Secrets::merge(global_overrides, profile_overrides); + + Ok(ConfigEvaluation { + profile_name, + overrides, + project_id: &profile.project_id, + version_req: version, + max_age: &self.cache.max_age, + }) + } +} + +fn parse_config_file<'a, P: AsRef>(file_path: P) -> Result, anyhow::Error> { + info!(message = format!("Using configuration file at {:?}", file_path.as_ref())); + let mut raw = String::new(); + let mut file = File::open(file_path) + .map_err(|_| ConfigError::Read) + .unwrap(); + let _ = file.read_to_string(&mut raw); + + Ok(serde_yaml::from_str::(&raw) + .map_err(|err| SerdeError::new(raw.to_string(), ErrorTypes::Yaml(err)))?) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_parse_config_file_success() { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!( + temp_file, + r#" +version: "1.0.0" +cache: + path: "/tmp/cache" + max-age: 86400 +global: + overrides: {{}} +profiles: {{}} +"# + ) + .unwrap(); + + let config = parse_config_file(temp_file.path()); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.version, "1.0.0"); + assert_eq!( + config.cache.path.as_pathbuf().to_str().unwrap(), + "/tmp/cache" + ); + assert_eq!(*config.cache.max_age.as_u64(), 86400); + } + + #[test] + fn test_config_evaluate_profile_not_found() { + let config = Config { + version: "1.0.0".to_string(), + cache: Cache::default(), + global: None, + profiles: Profiles::default(), + path: String::new(), + }; + + let result = config.evaluate("nonexistent"); + assert!(matches!(result, Err(ConfigError::NoProfile))); + } + + #[test] + fn test_config_evaluate_with_overrides() { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!( + temp_file, + r#" +version: "1.0.0" +cache: + path: "/tmp/cache" + max-age: 86400 +global: + overrides: + global_key: "global_value" +profiles: + test_profile: + project-id: "test_project" + overrides: + profile_key: "profile_value" + global_key: "overridden_global_value" +"# + ) + .unwrap(); + + let config = parse_config_file(temp_file.path()).unwrap(); + let eval_result = config.evaluate("test_profile").unwrap(); + + assert_eq!(eval_result.profile_name, "test_profile"); + assert_eq!(eval_result.project_id, "test_project"); + assert_eq!( + eval_result.overrides.0.get("global_key").unwrap(), + "overridden_global_value" + ); + assert_eq!( + eval_result.overrides.0.get("profile_key").unwrap(), + "profile_value" + ); + } + + #[test] + fn test_global_overrides_without_profile() { + let config = Config { + version: "1.0.0".to_string(), + cache: Cache::default(), + global: Some(Global { + overrides: Secrets( + [("global_key".into(), "global_value".into())] + .iter() + .cloned() + .collect(), + ), + }), + profiles: Profiles::default(), + path: String::new(), + }; + + let eval_result = config.evaluate("nonexistent").err().unwrap(); + assert!(matches!(eval_result, ConfigError::NoProfile)); + } +} diff --git a/src/error.rs b/src/error.rs index 40e0d9c..778fe44 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,6 @@ -#[derive(Debug)] -pub enum Error { - _NoProfileInput, - ProfileNotConfigured, - _ProjectNotConfigured, - _RemoteProjectNotFound, - _CacheDirNorConfigured, - _InvalidConfigurationFile, - _NoConfigurationFile, +#[derive(Debug, PartialEq)] +pub enum ConfigError { + Read, + NotFound, + NoProfile, } diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..fd5b5d3 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,85 @@ +use std::path::{Path, PathBuf}; + +pub fn find_up(filename: &str, max_parents: Option, cwd: Option<&Path>) -> Option { + let mut current_directory = cwd?; + + for _ in 0..max_parents.unwrap_or(10) { + let file_path = current_directory.join(filename); + + if file_path.exists() { + return Some(file_path); + } + + match current_directory.parent() { + Some(parent) => current_directory = parent, + None => break, + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::find_up; + use std::fs::{self, File}; + use std::io::Write; + use std::path::Path; + use tempfile::tempdir; + + #[test] + fn file_found_in_current_dir() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("testfile.txt"); + + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "This is a test file.").unwrap(); + + assert_eq!( + find_up("testfile.txt", None, Some(temp_dir.path())), + Some(file_path) + ); + } + + #[test] + fn file_found_in_parent_dir() { + let temp_dir = tempdir().unwrap(); + let child_dir = temp_dir.path().join("child"); + fs::create_dir(&child_dir).unwrap(); + + let file_path = temp_dir.path().join("testfile.txt"); + File::create(&file_path).unwrap(); + + assert_eq!( + find_up("testfile.txt", None, Some(&child_dir)), + Some(file_path) + ); + } + + #[test] + fn file_not_found() { + let temp_dir = tempdir().unwrap(); + let child_dir = temp_dir.path().join("child"); + fs::create_dir(&child_dir).unwrap(); + + assert_eq!(find_up("nonexistent.txt", None, Some(&child_dir)), None); + } + + #[test] + fn file_not_found_due_to_max_parents() { + let temp_dir = tempdir().unwrap(); + let level1_dir = temp_dir.path().join("level1"); + let level2_dir = level1_dir.join("level2"); + let level3_dir = level2_dir.join("level3"); + + fs::create_dir_all(&level3_dir).unwrap(); + + let file_path = temp_dir.path().join("testfile.txt"); + let mut file = File::create(file_path).unwrap(); + writeln!(file, "This is a test file.").unwrap(); + + assert_eq!(find_up("testfile.txt", Some(1), Some(&level3_dir)), None); + + std::env::set_current_dir(Path::new("/")).unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index 408fb0a..c62cd39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,69 @@ use clap::Parser; use cli::CacheCommand; -use log::Level; +use semver::Version; use std::{ io::{self, Read, Write}, - path::PathBuf, + path::Path, process::{self, Command, Stdio}, }; +use tracing::{error, info, span, warn, Level}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + mod bitwarden; mod cache; mod cli; mod config; +mod config_toml; +mod config_yaml; mod error; +mod fs; +mod time; use cache::CacheEntry; -// use cli::Args; -use config::ConfigEvaluation; use crate::cache::Cache; -use crate::config::Config; -use crate::{bitwarden::BitwardenClient, cli::Cli}; - -// use clap_markdown; +use crate::{bitwarden::BitwardenClient, cli::Cli, config_yaml::Secrets}; #[tokio::main(flavor = "current_thread")] async fn main() { - let _ = simple_logger::init_with_level(Level::Error); - - // generate docs - // let markdown: String = clap_markdown::help_markdown::(); - // println!("{}", markdown); let cli = Cli::parse(); + tracing_subscriber::registry() + .with(EnvFilter::new(cli.log_level.as_tracing_env())) + .with( + fmt::layer() + .fmt_fields(fmt::format::PrettyFields::new()) + .event_format(fmt::format().compact().without_time().with_target(false)) + .with_writer(std::io::stdout), + ) + .init(); + + let root_span = span!(Level::INFO, env!("CARGO_PKG_NAME")); + let _guard = root_span.enter(); + + let local_config = config::find_local_config(Some(&std::env::current_dir().unwrap())).unwrap(); + + let config_path = local_config.as_pathbuf(); + + let _ = match local_config { + config::LocalConfig::Yaml(_) => { + let config = config_yaml::Config::new(config_path).unwrap(); + run_with(cli, config_path, config).await + } + config::LocalConfig::Toml(_) => { + let toml_config = config_toml::Config::new(config_path).unwrap(); + let config = toml_config.as_yaml_config(); + warn!("bwenv.toml is deprecated. Please migrate to bwenv.yaml"); + run_with(cli, config_path, config).await + } + }; +} + +async fn run_with<'a>(cli: Cli, config_path: &Path, config: config_yaml::Config<'a>) { pub fn get_program(cli: &Cli) -> Option<(String, Vec)> { let slop = &cli.slop; - match &slop.get(0) { + match &slop.first() { Some(program) => { let args = slop[1..].to_vec(); @@ -44,81 +73,100 @@ async fn main() { } } - let config = Config::new(); - - let config_path = PathBuf::from(&config.path); let root_dir = config_path.parent().unwrap(); - let cache_dir = root_dir.join(&config.cache.path); + let cache_dir = root_dir.join(config.cache.path.as_pathbuf()); - let cache = Cache::new(cache_dir); + let profile_name = cli.profile.clone().unwrap_or_else(|| { + info!(message = "No profile specified, falling back to default profile"); + String::from("default") + }); + + let config_yaml::ConfigEvaluation { + version_req, + max_age, + project_id, + overrides, + .. + } = config.evaluate(&profile_name).unwrap_or_else(|_| { + error!( + message = format!( + "Could not find configuration for profile {:?}", + profile_name + ) + ); + process::exit(1) + }); + + let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + + let cache = Cache::new(cache_dir, &version); match &cli.command { Some(cli::Command::Cache(cache_command)) => match cache_command { CacheCommand::Clear => { - if let Some(profile) = &cli.profile { - cache.clear(&profile.clone()) - } - process::exit(1); + cache.clear(&profile_name); + process::exit(0); } CacheCommand::Invalidate => { - if let Some(profile) = &cli.profile { - cache.invalidate(&profile.clone()) - } - process::exit(1); + cache.invalidate(&profile_name); + process::exit(0); } }, None => {} + Some(_) => {} } - let ConfigEvaluation { - project_id, - profile_name, - max_age, - r#override, - } = config.evaluate(cli.profile.to_owned()).unwrap(); - - let (program, program_args) = match get_program(&cli) { - Some(t) => t, - None => { - log::error!("no slop provided"); - std::process::exit(1) - } - }; + if !version_req.matches(&version) { + error!( + "Version {} does not meet the requirement {}", + version, version_req + ); + std::process::exit(1); + } - let CacheEntry { - variables: secrets, .. - } = cache - .get_or_revalidate(&profile_name, max_age, move || async { - let mut bitwarden_client = BitwardenClient::new(cli.token).await; - bitwarden_client.get_secrets_by_project_id(project_id).await + let token = cli.token.clone(); + let CacheEntry { variables, .. } = cache + .get_or_revalidate(&profile_name, max_age.as_u64(), move || async move { + let mut bitwarden_client = BitwardenClient::new(token).await; + bitwarden_client + .get_secrets_by_project_id(&project_id) + .await }) .await .unwrap(); - let mut final_secrets = std::collections::HashMap::new(); + let mut secrets = Secrets::merge(&variables, &overrides); - for (key, value) in secrets.into_iter() { - final_secrets.insert(key, value); - } + if let Some(cli::Command::Inspect(inspect_args)) = &cli.command { + let reveal = if inspect_args.reveal { + inquire::Confirm::new("reveal secrets in output") + .with_default(false) + .with_help_message("Enabling this option will display sensitive information in plain text. Use with caution, especially in shared or public environments.") + .prompt() + } else { + Ok(false) + } + .unwrap(); - if let Some(overrides) = &r#override { - for (key, value) in overrides { - final_secrets.insert(key.clone(), value.clone()); - } + print!("{}", &secrets.table(reveal)); + process::exit(1); } - let secrets = final_secrets.into_iter().collect::>(); + let (program, program_args) = match get_program(&cli) { + Some(t) => t, + None => { + error!("no slop provided"); + std::process::exit(1) + } + }; let mut cmd = Command::new(program); - + cmd.envs(secrets.as_vec()); cmd.args(program_args); - cmd.stdin(Stdio::inherit()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - cmd.envs(secrets.to_owned()); - if let Ok(mut child) = cmd.spawn() { let mut stdout = child.stdout.take().unwrap(); let mut stderr = child.stderr.take().unwrap(); diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..2c69f3f --- /dev/null +++ b/src/time.rs @@ -0,0 +1,64 @@ +use std::time::SystemTime; + +pub fn is_date_older_than_n_seconds(unix_millis: u64, n_seconds: &u64) -> bool { + let date_seconds = unix_millis / 1000; + + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!"); + + let threshold_time = current_time.as_secs() - n_seconds; + + date_seconds < threshold_time +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, SystemTime}; + + #[test] + fn date_is_older_than_n_seconds() { + let slightly_more_than_ten_seconds_ago = SystemTime::now() + .checked_sub(Duration::from_secs(11)) + .expect("Failed to calculate time") + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + * 1000; + + assert!(is_date_older_than_n_seconds( + slightly_more_than_ten_seconds_ago, + &10 + )); + } + + #[test] + fn date_is_not_older_than_n_seconds() { + let five_seconds_ago = SystemTime::now() + .checked_sub(Duration::from_secs(5)) + .expect("Failed to calculate time") + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to convert to duration") + .as_secs() + * 1000; + + assert!(!is_date_older_than_n_seconds(five_seconds_ago, &10)); + } + + #[test] + fn edge_case_exact_n_seconds_old() { + let slightly_more_than_ten_seconds_ago = SystemTime::now() + .checked_sub(Duration::from_secs(11)) + .expect("Failed to calculate time") + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + * 1000; + + assert!(is_date_older_than_n_seconds( + slightly_more_than_ten_seconds_ago, + &10 + )); + } +} diff --git a/test.sh b/test.sh deleted file mode 100755 index fa293cc..0000000 --- a/test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -echo "OPENAI_API_KEY: $OPENAI_API_KEY" -echo "FORCE_COLOR: $FORCE_COLOR"