From 714ba7699fb419791866f7e9e8c324618af1dea8 Mon Sep 17 00:00:00 2001 From: ASleepyCat Date: Thu, 5 Oct 2023 22:44:03 +1000 Subject: [PATCH 1/5] Add support for OpenTofu --- Cargo.lock | 521 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- README.md | 1 + src/main.rs | 341 +++++++++++++++++++++++++--------- 4 files changed, 772 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e4ab8d..4b2a1af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.4" @@ -91,6 +106,23 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -112,6 +144,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.2" @@ -200,6 +238,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + [[package]] name = "cipher" version = "0.4.4" @@ -367,6 +418,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -461,6 +524,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -468,6 +546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -476,12 +555,34 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -500,8 +601,11 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -603,9 +707,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hmac" @@ -659,6 +763,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -695,6 +805,34 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -708,6 +846,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.4.0" @@ -778,6 +939,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.2", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kstring" version = "2.0.0" @@ -862,6 +1037,36 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -881,6 +1086,43 @@ dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb006c62700c309e112bed42fcb2372959c0b42ed4aea00dcd26ae2b9703af2" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.21.2", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-timeout", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -964,12 +1206,41 @@ dependencies = [ "sha2", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "pin-project-lite" version = "0.2.12" @@ -1102,7 +1373,7 @@ version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -1134,6 +1405,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1153,6 +1439,49 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1168,6 +1497,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -1222,6 +1570,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.3" @@ -1271,6 +1629,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.8" @@ -1280,6 +1650,29 @@ dependencies = [ "autocfg", ] +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.4.9" @@ -1300,6 +1693,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1423,6 +1822,7 @@ dependencies = [ "dialoguer", "home", "html-to-string-macro", + "octocrab", "once_cell", "pathsearch", "regex", @@ -1431,6 +1831,7 @@ dependencies = [ "tempdir", "tf-semver", "tfconfig", + "tokio", "toml", "zip", ] @@ -1462,8 +1863,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", + "itoa", "serde", "time-core", + "time-macros", ] [[package]] @@ -1472,6 +1875,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +[[package]] +name = "time-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1500,9 +1912,31 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2 0.5.3", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -1513,6 +1947,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -1561,6 +2005,48 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1574,10 +2060,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -1626,6 +2125,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.4.0" @@ -1635,6 +2140,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1777,6 +2283,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 5d897b6..744bc43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,13 +19,15 @@ clap_complete = "4.4.3" dialoguer = "0.11.0" home = "0.5.4" html-to-string-macro = "0.2.5" +octocrab = "0.31.0" once_cell = "1.18.0" pathsearch = "0.2.0" regex = "1.9.6" -reqwest = { version = "0.11.22", features = ["json", "blocking"] } +reqwest = { version = "0.11.22", features = ["json"] } semver = { version = "1.0.17", package = "tf-semver" } serde = { version = "1.0.188", features = ["derive"] } tempdir = "0.3.7" tfconfig = "0.2.2" +tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] } toml = "0.8.2" zip = "0.6.4" diff --git a/README.md b/README.md index 0e3e0c0..0b6dee3 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ You can also use a configuration file to automatically set certain flags or argu ```toml bin = "/location/of/terraform/binary" list_all = false +opentofu = false version = "1.0.0" ``` diff --git a/src/main.rs b/src/main.rs index ebb8248..435c1a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ use anyhow::{bail, Context, Ok, Result}; use clap::{CommandFactory, Parser}; +use core::fmt; use dialoguer::{theme::ColorfulTheme, Select}; use regex::Regex; -use reqwest::blocking::Response; +use reqwest::Response; use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use std::{ @@ -21,7 +22,6 @@ const ARCHIVE_URL: &str = "https://releases.hashicorp.com/terraform"; const CONFIG_FILE_NAME: &str = ".tfswitch.toml"; const DEFAULT_LOCATION: &str = ".local/bin"; const DEFAULT_CACHE_LOCATION: &str = ".cache/tfswitcher"; -const PROGRAM_NAME: &str = "terraform"; #[derive(Parser, Default, Debug, Serialize, Deserialize, PartialEq)] #[command(version, about)] @@ -36,6 +36,11 @@ struct Args { #[serde(default)] list_all: bool, + /// Install OpenTofu + #[arg(short, long)] + #[serde(default)] + opentofu: bool, + #[arg(env = "TF_VERSION")] #[serde(rename = "version")] install_version: Option, @@ -46,8 +51,144 @@ struct Args { generator: Option, } -fn get_http(url: &str) -> Result { - let response = reqwest::blocking::get(url) +#[derive(Debug)] +enum ProgramName { + Terraform, + OpenTofu, +} + +impl fmt::Display for ProgramName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProgramName::Terraform => write!(f, "terraform"), + ProgramName::OpenTofu => write!(f, "tofu"), + } + } +} + +impl Args { + fn get_program_name(&self) -> ProgramName { + if self.opentofu { + return ProgramName::OpenTofu; + } + ProgramName::Terraform + } +} + +#[derive(Clone, Debug, PartialEq)] +struct ReleaseInfo { + version: String, + url: String, + zip_name: String, +} + +impl ReleaseInfo { + fn new(version: String, url: String, zip_name: String) -> ReleaseInfo { + ReleaseInfo { + version, + url, + zip_name, + } + } +} + +trait ListVersions { + fn get_versions(&self) -> Vec; +} + +impl ListVersions for Vec { + fn get_versions(&self) -> Vec { + self.iter().map(|r| r.version.to_owned()).collect() + } +} + +enum Downloader { + Terraform, + OpenTofu, +} + +impl Downloader { + async fn get_versions(&self, args: &Args) -> Result> { + match self { + Downloader::Terraform => Ok(get_versions_terraform(args).await?), + Downloader::OpenTofu => Ok(get_versions_opentofu(args).await?), + } + } +} + +async fn get_versions_terraform(args: &Args) -> Result> { + let response = get_http(ARCHIVE_URL).await?; + let contents = response + .text() + .await + .with_context(|| "failed to get Terraform versions")?; + + Ok(capture_terraform_versions(args, &contents)) +} + +fn capture_terraform_versions(args: &Args, contents: &str) -> Vec { + let re = if args.list_all { + Regex::new(r"terraform_(?(\d+\.\d+\.\d+)(?:-[a-zA-Z0-9-]+)?)") + .expect("Invalid regex") + } else { + Regex::new(r"terraform_(?\d+\.\d+\.\d+)<").expect("Invalid regex") + }; + + let target = get_target_platform(); + let versions = re + .captures_iter(contents) + .filter_map(|c| { + c.name("version").map(|v| { + let version = v.as_str().to_owned(); + let zip_name = format!("terraform_{version}_{target}.zip"); + let url = format!("{ARCHIVE_URL}/{version}/{zip_name}"); + ReleaseInfo::new(version, url, zip_name) + }) + }) + .collect(); + + versions +} + +async fn get_versions_opentofu(args: &Args) -> Result> { + let releases = octocrab::instance() + .repos("opentofu", "opentofu") + .releases() + .list() + .send() + .await + .with_context(|| "failed to get releases from opentofu github repo")?; + + let mut versions = vec![]; + let target = get_target_platform(); + for release in releases { + if release.prerelease && !args.list_all { + continue; + } + + if let Some(asset) = release + .assets + .into_iter() + .find(|asset| asset.name.ends_with(format!("{target}.zip").as_str())) + { + let version = match release.tag_name.strip_prefix('v') { + Some(v) => v.to_owned(), + None => release.tag_name, + }; + versions.push(ReleaseInfo::new( + version, + asset.browser_download_url.into(), + asset.name, + )); + } + } + + Ok(versions) +} + +async fn get_http(url: &str) -> Result { + let response = reqwest::get(url) + .await .with_context(|| format!("failed to send HTTP request to {url}"))? .error_for_status() .with_context(|| format!("server returned error from {url}"))?; @@ -55,7 +196,8 @@ fn get_http(url: &str) -> Result { Ok(response) } -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { let mut args = Args::parse(); parse_config_arguments(".".into(), &mut args)?; @@ -70,11 +212,14 @@ fn main() -> Result<()> { } let Some(program_path) = find_terraform_program_path(&args) else { - bail!("could not find path to install Terraform"); + bail!(format!( + "could not find path to install {:?}", + args.get_program_name() + )); }; - match get_version_to_install(args)? { - Some(version) => Ok(install_version(&program_path, &version)?), + match get_version_to_install(&args).await? { + Some(version) => Ok(install_version(&args, &program_path, version).await?), None => bail!("no version to install"), } } @@ -85,6 +230,7 @@ fn parse_config_arguments(cwd: PathBuf, args: &mut Args) -> Result<()> { args.binary_location = config.binary_location } args.list_all |= config.list_all; + args.opentofu |= config.opentofu; if args.install_version.is_none() { args.install_version = config.install_version } @@ -125,61 +271,42 @@ fn find_terraform_program_path(args: &Args) -> Option { return args.binary_location.clone(); } - if let Some(path) = pathsearch::find_executable_in_path(PROGRAM_NAME) { + let program_name = args.get_program_name(); + + if let Some(path) = pathsearch::find_executable_in_path(&program_name.to_string()) { return Some(path); } match home::home_dir() { Some(mut path) => { - path.push(format!("{DEFAULT_LOCATION}/{PROGRAM_NAME}")); - println!("Could not locate {PROGRAM_NAME}, installing to {path:?}\nMake sure to include the directory in your $PATH environment variable"); + path.push(format!("{DEFAULT_LOCATION}/{program_name}")); + println!("Could not locate {program_name:?}, installing to {path:?}\nMake sure to include the directory in your $PATH environment variable"); Some(path) } None => None, } } -fn get_version_to_install(args: Args) -> Result> { - if args.install_version.is_some() { - return Ok(args.install_version); - } +async fn get_version_to_install(args: &Args) -> Result> { + let downloader = if args.opentofu { + Downloader::OpenTofu + } else { + Downloader::Terraform + }; + let versions = downloader.get_versions(args).await?; - let contents = get_terraform_versions(ARCHIVE_URL)?; - let versions = capture_terraform_versions(&args, &contents); + if let Some(version) = &args.install_version { + return Ok(versions.into_iter().find(|v| v.version.eq(version))); + } if let Some(version_from_module) = get_version_from_module(Path::new("."), &versions)? { - return Ok(Some(version_from_module.to_owned())); + return Ok(Some(version_from_module)); } - get_version_from_user_prompt(&versions) -} - -fn get_terraform_versions(url: &str) -> Result { - let response = get_http(url)?; - let contents = response - .text() - .with_context(|| "failed to get Terraform versions")?; - - Ok(contents) -} - -fn capture_terraform_versions<'a>(args: &Args, contents: &'a str) -> Vec<&'a str> { - let re = if args.list_all { - Regex::new(r#"terraform_(?(\d+\.\d+\.\d+)(?:-[a-zA-Z0-9-]+)?)"#) - .expect("Invalid regex") - } else { - Regex::new(r#"terraform_(?\d+\.\d+\.\d+)<"#).expect("Invalid regex") - }; - - let versions = re - .captures_iter(contents) - .filter_map(|c| c.name("version").map(|v| v.as_str())) - .collect(); - - versions + get_version_from_user_prompt(args.get_program_name(), &versions) } -fn get_version_from_module<'a>(cwd: &Path, versions: &'a [&'a str]) -> Result> { +fn get_version_from_module(cwd: &Path, versions: &Vec) -> Result> { let module = tfconfig::load_module(cwd, false).with_context(|| "failed to load terraform modules")?; let version_constraint = match module.required_core.first() { @@ -192,37 +319,57 @@ fn get_version_from_module<'a>(cwd: &Path, versions: &'a [&'a str]) -> Result Result> { +fn get_version_from_user_prompt( + program_name: ProgramName, + versions: &Vec, +) -> Result> { match Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Select a Terraform version to install") - .items(versions) + .with_prompt(format!("Select a {program_name:?} version to install")) + .items(&versions.get_versions()) .default(0) .interact_opt() .with_context(|| "failed to get version from user prompt")? { - Some(selection) => Ok(Some(versions[selection].to_owned())), + Some(selection) => Ok(versions.get(selection).cloned()), None => Ok(None), } } -fn install_version(program_path: &Path, version: &str) -> Result<()> { - println!("Terraform {version} will be installed to {program_path:?}"); +async fn install_version(args: &Args, program_path: &Path, release: ReleaseInfo) -> Result<()> { + println!( + "{:?} {} will be installed to {program_path:?}", + args.get_program_name(), + release.version + ); + + let archive = get_zip(release).await?; + extract_zip_archive(args.get_program_name(), program_path, archive) +} + +async fn get_zip(release: ReleaseInfo) -> Result>>> { + if let Some(cursor) = get_cached_zip(home::home_dir().as_mut(), &release.zip_name)? { + let archive = ZipArchive::new(cursor).with_context(|| "failed to read cached archive")?; + return Ok(archive); + } + + download_and_save_zip(release).await +} +fn get_target_platform() -> String { let os = consts::OS; let arch = get_arch(consts::ARCH); - let archive = get_terraform_version_zip(version, os, arch)?; - extract_zip_archive(program_path, archive) + format!("{os}_{arch}") } fn get_arch(arch: &str) -> &str { @@ -234,21 +381,6 @@ fn get_arch(arch: &str) -> &str { } } -fn get_terraform_version_zip( - version: &str, - os: &str, - arch: &str, -) -> Result>>> { - let zip_name = format!("terraform_{version}_{os}_{arch}.zip"); - - if let Some(cursor) = get_cached_zip(home::home_dir().as_mut(), &zip_name)? { - let archive = ZipArchive::new(cursor).with_context(|| "failed to read cached archive")?; - return Ok(archive); - } - - download_and_save_terraform_version_zip(version, &zip_name) -} - fn get_cached_zip( home_dir: Option<&mut PathBuf>, zip_name: &str, @@ -271,16 +403,13 @@ fn get_cached_zip( } } -fn download_and_save_terraform_version_zip( - version: &str, - zip_name: &str, -) -> Result>>> { - let url = format!("{ARCHIVE_URL}/{version}/{zip_name}"); - println!("Downloading archive from {url}"); +async fn download_and_save_zip(release: ReleaseInfo) -> Result>>> { + println!("Downloading archive from {}", release.url); - let response = get_http(&url)?; + let response = get_http(&release.url).await?; let contents = response .bytes() + .await .with_context(|| "failed to read HTTP response")? .to_vec(); @@ -288,7 +417,7 @@ fn download_and_save_terraform_version_zip( Some(mut path) => { path.push(DEFAULT_CACHE_LOCATION); println!("Caching archive to {path:?}"); - if let Err(e) = cache_zip_archive(&mut path, zip_name, &contents) { + if let Err(e) = cache_zip_archive(&mut path, &release.zip_name, &contents) { println!("Unable to cache archive: {e}"); }; } @@ -308,11 +437,12 @@ fn cache_zip_archive(cache_location: &mut PathBuf, zip_name: &str, buffer: &[u8] } fn extract_zip_archive( + program_name: ProgramName, program_path: &Path, mut archive: ZipArchive>>, ) -> Result<()> { let mut file = archive - .by_index(0) + .by_name(&program_name.to_string()) .with_context(|| "could not get item in archive")?; let file_name = file.name(); println!("Extracting {file_name} to {program_path:?}"); @@ -428,10 +558,11 @@ mod tests { }); #[test] - fn test_parse_config_arguments_list_all_flag_disabled_from_cli() -> Result<()> { - let config_file = "list_all = true"; + fn test_parse_config_arguments_bool_flags_disabled_from_cli() -> Result<()> { + let config_file = r#"list_all = true +opentofu = true"#; - let tmp_dir = TempDir::new("test_parse_config_arguments_list_all_flag_disabled_from_cli")?; + let tmp_dir = TempDir::new("test_parse_config_arguments_bool_flags_disabled_from_cli")?; let tmp_dir_path = tmp_dir.path(); let file_path = tmp_dir_path.join(CONFIG_FILE_NAME); fs::write(file_path, config_file)?; @@ -439,23 +570,26 @@ mod tests { let mut args = Args::default(); parse_config_arguments(tmp_dir_path.to_path_buf(), &mut args)?; assert!(args.list_all); + assert!(args.opentofu); Ok(()) } #[test] - fn test_parse_config_arguments_list_all_flag_enabled_from_cli() -> Result<()> { - let tmp_dir = TempDir::new("test_parse_config_arguments_list_all_flag_enabled_from_cli")?; + fn test_parse_config_arguments_bool_flags_enabled_from_cli() -> Result<()> { + let tmp_dir = TempDir::new("test_parse_config_arguments_bool_flags_enabled_from_cli")?; let tmp_dir_path = tmp_dir.path(); let file_path = tmp_dir_path.join(CONFIG_FILE_NAME); File::create(file_path)?; let mut args = Args { list_all: true, + opentofu: true, ..Default::default() }; parse_config_arguments(tmp_dir_path.to_path_buf(), &mut args)?; assert!(args.list_all); + assert!(args.opentofu); Ok(()) } @@ -465,11 +599,13 @@ mod tests { let expected_config_file = Args { binary_location: Some("test_load_config_file_in_cwd".into()), list_all: true, + opentofu: true, install_version: Some("test_load_config_file_in_cwd".to_owned()), generator: None, }; let config_file = r#"bin = "test_load_config_file_in_cwd" list_all = true +opentofu = true version = "test_load_config_file_in_cwd""#; let tmp_dir = TempDir::new("test_load_config_file_in_cwd")?; @@ -488,11 +624,13 @@ version = "test_load_config_file_in_cwd""#; let expected_config_file = Args { binary_location: Some("test_load_config_file_in_home".into()), list_all: true, + opentofu: true, install_version: Some("test_load_config_file_in_home".to_owned()), generator: None, }; let config_file = r#"bin = "test_load_config_file_in_home" list_all = true +opentofu = true version = "test_load_config_file_in_home""#; let tmp_dir = TempDir::new("test_load_config_file_in_home")?; @@ -519,7 +657,18 @@ version = "test_load_config_file_in_home""#; #[test] fn test_capture_terraform_versions() -> Result<()> { - let expected_versions = vec!["1.3.0", "1.2.0", "1.1.0", "1.0.0", "0.15.0"]; + let target = get_target_platform(); + let expected_versions: Vec = + vec!["1.3.0", "1.2.0", "1.1.0", "1.0.0", "0.15.0"] + .iter() + .map(|&v| { + ReleaseInfo::new( + v.into(), + format!("{ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), + format!("terraform_{v}_{target}.zip"), + ) + }) + .collect(); let actual_versions = capture_terraform_versions(&Args::default(), &LINES); assert_eq!(expected_versions, actual_versions); @@ -529,7 +678,8 @@ version = "test_load_config_file_in_home""#; #[test] fn test_capture_terraform_versions_list_all() -> Result<()> { - let expected_versions = vec![ + let target = get_target_platform(); + let expected_versions: Vec = vec![ "1.3.0", "1.3.0-rc1", "1.3.0-beta1", @@ -548,7 +698,16 @@ version = "test_load_config_file_in_home""#; "0.15.0-rc1", "0.15.0-beta1", "0.15.0-alpha20210107", - ]; + ] + .iter() + .map(|&v| { + ReleaseInfo::new( + v.into(), + format!("{ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), + format!("terraform_{v}_{target}.zip"), + ) + }) + .collect(); let args = Args { list_all: true, ..Default::default() @@ -562,8 +721,8 @@ version = "test_load_config_file_in_home""#; #[test] fn test_get_version_from_module() -> Result<()> { - const EXPECTED_VERSION: &str = "1.0.0"; - let versions = vec![EXPECTED_VERSION]; + let expected_release = ReleaseInfo::new("1.0.0".into(), "".to_string(), "".to_string()); + let versions = vec![expected_release.clone()]; let tmp_dir = TempDir::new("test_get_version_from_module")?; let tmp_dir_path = tmp_dir.path(); @@ -571,7 +730,7 @@ version = "test_load_config_file_in_home""#; fs::write(file_path, r#"terraform { required_version = "~>1.0.0" }"#)?; let actual_version = get_version_from_module(tmp_dir_path, &versions)?; - assert_eq!(Some(EXPECTED_VERSION), actual_version); + assert_eq!(Some(expected_release), actual_version); Ok(()) } From 5e9be096af77de75feef6f68fa177ded46031ca2 Mon Sep 17 00:00:00 2001 From: ASleepyCat Date: Thu, 5 Oct 2023 22:55:16 +1000 Subject: [PATCH 2/5] Update README and Cargo.toml to include OpenTofu --- Cargo.toml | 4 ++-- README.md | 13 +------------ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 744bc43..72015a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,10 @@ version = "0.3.0" edition = "2021" authors = ["ASleepyCat dyeom340@gmail.com"] license = "MIT" -description = "A Terraform version switcher" +description = "A Terraform and OpenTofu version switcher" readme = "README.md" repository = "https://github.com/ASleepyCat/tfswitcher" -keywords = ["cli", "terraform", "tfswitcher", "tfswitch"] +keywords = ["cli", "terraform", "opentofu", "tofu", "tfswitcher", "tfswitch"] categories = ["command-line-utilities"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 0b6dee3..0edd601 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tfswitcher -[`tfswitch`](https://github.com/warrensbox/terraform-switcher/)-like program but written in Rust. +Terraform and OpenTofu version switcher written in Rust. ## Installation @@ -35,19 +35,8 @@ Alternatively, you can source the tab-completion script inside your shell's star echo "source <(tfswitcher -c bash)" >> ~/.bashrc ``` -## Motivations - -* Improved performance on WSL (if `$PATH` contains Windows directories) -* Better code quality - * This is somewhat subjective, but I found debugging on `tfswitch` to be pretty cumbersome with all the `os.Exit()`s there are -* I wanted to try out Rust - ## Caveats -This is not a complete reimplementation of `tfswitch`, as there are some missing flags that haven't been implemented. If you rely on these missing flags, raise an issue and I'll add it in. - -This is also my first non-trivial public Rust project; if there is a mistake I've made that doesn't conform to standard Rust coding practices, please raise an issue about it. - This has not been tested on Windows or macOS, so YMMV. ## Where's `v0.1.0`? From b1e1f23d42e111b742295a80aeb398602804ef2b Mon Sep 17 00:00:00 2001 From: ASleepyCat Date: Thu, 5 Oct 2023 23:09:37 +1000 Subject: [PATCH 3/5] Rename Downloader enum --- src/main.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 435c1a4..6ec7de4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,16 +102,16 @@ impl ListVersions for Vec { } } -enum Downloader { +enum VersionList { Terraform, OpenTofu, } -impl Downloader { +impl VersionList { async fn get_versions(&self, args: &Args) -> Result> { match self { - Downloader::Terraform => Ok(get_versions_terraform(args).await?), - Downloader::OpenTofu => Ok(get_versions_opentofu(args).await?), + VersionList::Terraform => Ok(get_versions_terraform(args).await?), + VersionList::OpenTofu => Ok(get_versions_opentofu(args).await?), } } } @@ -288,12 +288,12 @@ fn find_terraform_program_path(args: &Args) -> Option { } async fn get_version_to_install(args: &Args) -> Result> { - let downloader = if args.opentofu { - Downloader::OpenTofu + let version_list = if args.opentofu { + VersionList::OpenTofu } else { - Downloader::Terraform + VersionList::Terraform }; - let versions = downloader.get_versions(args).await?; + let versions = version_list.get_versions(args).await?; if let Some(version) = &args.install_version { return Ok(versions.into_iter().find(|v| v.version.eq(version))); From aec0f782a4f035b38679a48cbfde4bde23a4559b Mon Sep 17 00:00:00 2001 From: ASleepyCat Date: Fri, 6 Oct 2023 18:50:47 +1000 Subject: [PATCH 4/5] Manually construct OpenTofu download URL --- src/main.rs | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6ec7de4..7b875bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,8 @@ use zip::ZipArchive; #[cfg(unix)] use std::os::unix::prelude::PermissionsExt; -const ARCHIVE_URL: &str = "https://releases.hashicorp.com/terraform"; +const TERRAFORM_ARCHIVE_URL: &str = "https://releases.hashicorp.com/terraform"; +const OPENTOFU_ARCHIVE_URL: &str = "https://github.com/opentofu/opentofu/releases/download"; const CONFIG_FILE_NAME: &str = ".tfswitch.toml"; const DEFAULT_LOCATION: &str = ".local/bin"; const DEFAULT_CACHE_LOCATION: &str = ".cache/tfswitcher"; @@ -117,7 +118,7 @@ impl VersionList { } async fn get_versions_terraform(args: &Args) -> Result> { - let response = get_http(ARCHIVE_URL).await?; + let response = get_http(TERRAFORM_ARCHIVE_URL).await?; let contents = response .text() .await @@ -141,7 +142,7 @@ fn capture_terraform_versions(args: &Args, contents: &str) -> Vec { c.name("version").map(|v| { let version = v.as_str().to_owned(); let zip_name = format!("terraform_{version}_{target}.zip"); - let url = format!("{ARCHIVE_URL}/{version}/{zip_name}"); + let url = format!("{TERRAFORM_ARCHIVE_URL}/{version}/{zip_name}"); ReleaseInfo::new(version, url, zip_name) }) }) @@ -166,21 +167,14 @@ async fn get_versions_opentofu(args: &Args) -> Result> { continue; } - if let Some(asset) = release - .assets - .into_iter() - .find(|asset| asset.name.ends_with(format!("{target}.zip").as_str())) - { - let version = match release.tag_name.strip_prefix('v') { - Some(v) => v.to_owned(), - None => release.tag_name, - }; - versions.push(ReleaseInfo::new( - version, - asset.browser_download_url.into(), - asset.name, - )); - } + let version = match release.tag_name.strip_prefix('v') { + Some(v) => v.to_owned(), + None => release.tag_name.clone(), + }; + let zip_name = format!("{}_{version}_{target}.zip", ProgramName::OpenTofu); + let browser_download_url = + format!("{OPENTOFU_ARCHIVE_URL}/{}/{zip_name}", release.tag_name); + versions.push(ReleaseInfo::new(version, browser_download_url, zip_name)); } Ok(versions) @@ -664,7 +658,7 @@ version = "test_load_config_file_in_home""#; .map(|&v| { ReleaseInfo::new( v.into(), - format!("{ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), + format!("{TERRAFORM_ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), format!("terraform_{v}_{target}.zip"), ) }) @@ -703,7 +697,7 @@ version = "test_load_config_file_in_home""#; .map(|&v| { ReleaseInfo::new( v.into(), - format!("{ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), + format!("{TERRAFORM_ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), format!("terraform_{v}_{target}.zip"), ) }) From e3d4229d66e4d29bf4f6273906584919e6edab23 Mon Sep 17 00:00:00 2001 From: ASleepyCat Date: Fri, 6 Oct 2023 22:33:15 +1000 Subject: [PATCH 5/5] Don't store zip name and download URLs --- src/main.rs | 116 +++++++++++++++++++++++++--------------------------- 1 file changed, 55 insertions(+), 61 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7b875bc..0a09383 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ struct Args { generator: Option, } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] enum ProgramName { Terraform, OpenTofu, @@ -78,17 +78,30 @@ impl Args { #[derive(Clone, Debug, PartialEq)] struct ReleaseInfo { + program_name: ProgramName, version: String, - url: String, - zip_name: String, } impl ReleaseInfo { - fn new(version: String, url: String, zip_name: String) -> ReleaseInfo { + fn new(program_name: ProgramName, version: String) -> ReleaseInfo { ReleaseInfo { + program_name, version, - url, - zip_name, + } + } + + fn get_zip_name(&self) -> String { + let target = get_target_platform(); + format!("{}_{}_{target}.zip", self.program_name, self.version) + } + + fn get_download_url(&self) -> String { + let zip_name = self.get_zip_name(); + match self.program_name { + ProgramName::Terraform => { + format!("{TERRAFORM_ARCHIVE_URL}/{}/{zip_name}", self.version) + } + ProgramName::OpenTofu => format!("{OPENTOFU_ARCHIVE_URL}/v{}/{zip_name}", self.version), } } } @@ -135,16 +148,11 @@ fn capture_terraform_versions(args: &Args, contents: &str) -> Vec { Regex::new(r"terraform_(?\d+\.\d+\.\d+)<").expect("Invalid regex") }; - let target = get_target_platform(); let versions = re .captures_iter(contents) .filter_map(|c| { - c.name("version").map(|v| { - let version = v.as_str().to_owned(); - let zip_name = format!("terraform_{version}_{target}.zip"); - let url = format!("{TERRAFORM_ARCHIVE_URL}/{version}/{zip_name}"); - ReleaseInfo::new(version, url, zip_name) - }) + c.name("version") + .map(|v| ReleaseInfo::new(args.get_program_name(), v.as_str().to_owned())) }) .collect(); @@ -160,22 +168,17 @@ async fn get_versions_opentofu(args: &Args) -> Result> { .await .with_context(|| "failed to get releases from opentofu github repo")?; - let mut versions = vec![]; - let target = get_target_platform(); - for release in releases { - if release.prerelease && !args.list_all { - continue; - } - - let version = match release.tag_name.strip_prefix('v') { - Some(v) => v.to_owned(), - None => release.tag_name.clone(), - }; - let zip_name = format!("{}_{version}_{target}.zip", ProgramName::OpenTofu); - let browser_download_url = - format!("{OPENTOFU_ARCHIVE_URL}/{}/{zip_name}", release.tag_name); - versions.push(ReleaseInfo::new(version, browser_download_url, zip_name)); - } + let versions = releases + .into_iter() + .filter(|r| !r.prerelease || args.list_all) + .map(|r| { + let version = match r.tag_name.strip_prefix('v') { + Some(v) => v.to_owned(), + None => r.tag_name.clone(), + }; + ReleaseInfo::new(args.get_program_name(), version) + }) + .collect(); Ok(versions) } @@ -274,7 +277,9 @@ fn find_terraform_program_path(args: &Args) -> Option { match home::home_dir() { Some(mut path) => { path.push(format!("{DEFAULT_LOCATION}/{program_name}")); - println!("Could not locate {program_name:?}, installing to {path:?}\nMake sure to include the directory in your $PATH environment variable"); + println!( + "Could not locate {program_name:?}, installing to {path:?}\nMake sure to include the directory in your $PATH environment variable" + ); Some(path) } None => None, @@ -282,6 +287,13 @@ fn find_terraform_program_path(args: &Args) -> Option { } async fn get_version_to_install(args: &Args) -> Result> { + if let Some(version) = &args.install_version { + return Ok(Some(ReleaseInfo::new( + args.get_program_name(), + version.into(), + ))); + } + let version_list = if args.opentofu { VersionList::OpenTofu } else { @@ -289,10 +301,6 @@ async fn get_version_to_install(args: &Args) -> Result> { }; let versions = version_list.get_versions(args).await?; - if let Some(version) = &args.install_version { - return Ok(versions.into_iter().find(|v| v.version.eq(version))); - } - if let Some(version_from_module) = get_version_from_module(Path::new("."), &versions)? { return Ok(Some(version_from_module)); } @@ -346,12 +354,12 @@ async fn install_version(args: &Args, program_path: &Path, release: ReleaseInfo) release.version ); - let archive = get_zip(release).await?; + let archive = get_zip(&release).await?; extract_zip_archive(args.get_program_name(), program_path, archive) } -async fn get_zip(release: ReleaseInfo) -> Result>>> { - if let Some(cursor) = get_cached_zip(home::home_dir().as_mut(), &release.zip_name)? { +async fn get_zip(release: &ReleaseInfo) -> Result>>> { + if let Some(cursor) = get_cached_zip(home::home_dir().as_mut(), &release.get_zip_name())? { let archive = ZipArchive::new(cursor).with_context(|| "failed to read cached archive")?; return Ok(archive); } @@ -397,10 +405,10 @@ fn get_cached_zip( } } -async fn download_and_save_zip(release: ReleaseInfo) -> Result>>> { - println!("Downloading archive from {}", release.url); - - let response = get_http(&release.url).await?; +async fn download_and_save_zip(release: &ReleaseInfo) -> Result>>> { + let url = release.get_download_url(); + println!("Downloading archive from {url}"); + let response = get_http(&url).await?; let contents = response .bytes() .await @@ -411,7 +419,7 @@ async fn download_and_save_zip(release: ReleaseInfo) -> Result { path.push(DEFAULT_CACHE_LOCATION); println!("Caching archive to {path:?}"); - if let Err(e) = cache_zip_archive(&mut path, &release.zip_name, &contents) { + if let Err(e) = cache_zip_archive(&mut path, &release.get_zip_name(), &contents) { println!("Unable to cache archive: {e}"); }; } @@ -651,17 +659,10 @@ version = "test_load_config_file_in_home""#; #[test] fn test_capture_terraform_versions() -> Result<()> { - let target = get_target_platform(); let expected_versions: Vec = vec!["1.3.0", "1.2.0", "1.1.0", "1.0.0", "0.15.0"] - .iter() - .map(|&v| { - ReleaseInfo::new( - v.into(), - format!("{TERRAFORM_ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), - format!("terraform_{v}_{target}.zip"), - ) - }) + .into_iter() + .map(|v| ReleaseInfo::new(ProgramName::Terraform, v.into())) .collect(); let actual_versions = capture_terraform_versions(&Args::default(), &LINES); @@ -672,7 +673,6 @@ version = "test_load_config_file_in_home""#; #[test] fn test_capture_terraform_versions_list_all() -> Result<()> { - let target = get_target_platform(); let expected_versions: Vec = vec![ "1.3.0", "1.3.0-rc1", @@ -693,14 +693,8 @@ version = "test_load_config_file_in_home""#; "0.15.0-beta1", "0.15.0-alpha20210107", ] - .iter() - .map(|&v| { - ReleaseInfo::new( - v.into(), - format!("{TERRAFORM_ARCHIVE_URL}/{v}/terraform_{v}_{target}.zip"), - format!("terraform_{v}_{target}.zip"), - ) - }) + .into_iter() + .map(|v| ReleaseInfo::new(ProgramName::Terraform, v.into())) .collect(); let args = Args { list_all: true, @@ -715,7 +709,7 @@ version = "test_load_config_file_in_home""#; #[test] fn test_get_version_from_module() -> Result<()> { - let expected_release = ReleaseInfo::new("1.0.0".into(), "".to_string(), "".to_string()); + let expected_release = ReleaseInfo::new(ProgramName::Terraform, "1.0.0".into()); let versions = vec![expected_release.clone()]; let tmp_dir = TempDir::new("test_get_version_from_module")?;