diff --git a/.gitignore b/.gitignore index 4dc88fb..9e9b000 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,13 @@ -# acos -dist/ - -# VSCode: https://github.com/github/gitignore/blob/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Global/VisualStudioCode.gitignore -.vscode/* -#!.vscode/settings.json -#!.vscode/tasks.json -#!.vscode/launch.json -#!.vscode/extensions.json -#!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -# Go: https://github.com/github/gitignore/blob/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Go.gitignore -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories -vendor/ - -# Go workspace file -go.work +# Rust build artifacts +/target +/dist + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..54312d3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3018 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "acos" +version = "0.1.0" +dependencies = [ + "anyhow", + "aws-config", + "aws-sdk-costexplorer", + "aws-sdk-iam", + "aws-sdk-organizations", + "aws-sdk-sts", + "chrono", + "clap", + "comfy-table", + "dialoguer", + "serde", + "serde_json", + "tokio", +] + +[[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 = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517aa062d8bd9015ee23d6daa5e1c1372328412fdae4e6c4c1be9b69c6ad37a2" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "sha1", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.4.0", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-costexplorer" +version = "1.115.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c21e197eee0fae41daaefede19ecda9e77d43ccc0d2b8171dc5c6ed4ca3c6c1d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-iam" +version = "1.109.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61764c78f3011bd62844207c14491cd8385348760691f5614c4645df03a289f" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-organizations" +version = "1.115.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0825a893662ae83bee39c8912f05f83dc627f5ff2ca597367755faa583fa77c7" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4055e6099b2ec264abdc0d9bbfffce306c1601809275c861594779a0b04b45" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f009ba0284c5d696425fd7b4dcc5b189f5726f4041b7a5794daecb3a68d598" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.104.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa6622798e19e6a76b690562085dd4771c736cd48343464a53ab4ae2f2c9f84" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7083fb918b38474ac65ffbf8a69fc8792d36879f4ac5f1667b43aec61efe9a5" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.14", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517089205f18ab4adc5a3e02888cb139bbbbb2e168eac9f396216925d1fbeaf5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc117c179ecf39a62a0a3f49f600e9ac26a7ad7dd172177999f83933af776c32" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056b66dbce2f81cc0c1e2b05bb402eb58f8a3530479d650efadd5bbae9a4050b" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "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-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.14", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper 1.9.0", + "hyper-util", + "rustls 0.23.40", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[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 = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.40", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6558f01 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "acos" +version = "0.1.0" +edition = "2021" +description = "An interactive CLI tool to retrieve and show your AWS costs" +license = "Apache-2.0" + +[dependencies] +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-costexplorer = "1" +aws-sdk-organizations = "1" +aws-sdk-sts = "1" +aws-sdk-iam = "1" +tokio = { version = "1", features = ["full"] } +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = { version = "0.4", features = ["serde"] } +dialoguer = "0.11" +comfy-table = "7" +anyhow = "1" diff --git a/Makefile b/Makefile index 82e88a4..3298a79 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ BIN=acos -#COMMIT_SHA=$(shell git rev-parse --short HEAD) .PHONY: help ## help: prints this help message @@ -11,26 +10,33 @@ help: ## build: build the application build: clean @echo "Building..." - @go build -o ./dist/${BIN} ./cmd/acos + @cargo build --release + @mkdir -p ./dist + @cp ./target/release/${BIN} ./dist/${BIN} .PHONY: run -## run: runs go run ./cmd/acos +## run: runs the application run: - go run -race ./cmd/acos + cargo run .PHONY: clean ## clean: cleans the binary clean: @echo "Cleaning" - @go clean + @cargo clean + @rm -rf ./dist -.PHONE: test -## test: runs go test with default values +.PHONY: test +## test: runs tests test: - go test -v -count=1 -race ./... + cargo test -.PHONY: setup -## setup: setup go modules -setup: - @go mod tidy \ - && go mod vendor +.PHONY: fmt +## fmt: formats the code +fmt: + cargo fmt + +.PHONY: lint +## lint: runs clippy linter +lint: + cargo clippy -- -D warnings diff --git a/README.md b/README.md index cce05fb..4d93cba 100644 --- a/README.md +++ b/README.md @@ -21,25 +21,37 @@ With the `--ou` option, it requires `organizations:ListAccountsForParent` IAM pe ## Installation +### From source + ```shell -$ go install github.com/toricls/acos/cmd/acos@latest +$ cargo install --git https://github.com/toricls/acos +``` + +### Build locally + +```shell +$ git clone https://github.com/toricls/acos.git +$ cd acos +$ make build +$ ./dist/acos --help ``` ## Usage ```shell $ acos --help -Usage of acos: - -accountIds string - Optional - Comma-separated AWS account IDs to retrieve costs. The interactive account selector is skipped when this flag is set. - -asOf string - Optional - The date to retrieve the cost data. The format should be 'YYYY-MM-DD'. The default value is today in UTC. - -comparedTo string - Optional - The cost of this month will be compared to either one of 'YESTERDAY' or 'LAST_WEEK'. This flag is ignored when the -json flag is set. (default "YESTERDAY") - -json - Optional - Print JSON instead of a table. - -ou string - Optional - The ID of an AWS Organizational Unit (OU) or Root to list direct-children AWS accounts. It must start with 'ou-' or 'r-' prefix. This flag is ignored when the -accountIds flag is set. +An interactive CLI tool to retrieve and show your AWS costs + +Usage: acos [OPTIONS] + +Options: + --ou The ID of an AWS Organizational Unit (OU) or Root to list direct-children AWS accounts + --as-of The date to retrieve the cost data. The format should be 'YYYY-MM-DD' + --compared-to The cost of this month will be compared to either 'YESTERDAY' or 'LAST_WEEK' [default: YESTERDAY] + --json Print JSON instead of table + --account-ids Comma-separated AWS account IDs to retrieve costs + -h, --help Print help + -V, --version Print version ``` ### Accounts within AWS Organization @@ -48,62 +60,60 @@ Usage of acos: $ acos ? Select accounts: 567890123456 - my-prod, 123456789012 - my-sandbox +--------------+--------------+----------------+------------------+----------------+ -| ACCOUNT ID | ACCOUNT NAME | THIS MONTH ($) | VS YESTERDAY ($) | LAST MONTH ($) | +| Account ID | Account Name | This Month ($) | vs Yesterday ($) | Last Month ($) | +--------------+--------------+----------------+------------------+----------------+ | 123456789012 | my-sandbox | 0.038331 | + 0.002255 | 0.127884 | | 567890123456 | my-prod | 5820.334869 | + 324.526062 | 10765.384186 | +--------------+--------------+----------------+------------------+----------------+ -| TOTAL | 5820.373201 | + 324.528317 | 10765.512070 | +| | Total | 5820.373201 | + 324.528317 | 10765.512070 | +--------------+--------------+----------------+------------------+----------------+ As of 2023-07-18. ``` ### Accounts under specific AWS Organizational Unit (OU) -Use `--ou` option to filter selectable AWS accounts by specific OU. Note that the `--ou` option is ignored when the `--accountIds` option is set. - -```shell +Use `--ou` option to filter selectable AWS accounts by specific OU. Note that the `--ou` option is ignored when the `--account-ids` option is set. ```shell $ acos --ou ou-xxxx-12345678 Retrieving AWS accounts under the OU 'ou-xxxx-12345678'... ? Select accounts: 234567890123 - my-dev, 123456789012 - my-sandbox +--------------+--------------+----------------+------------------+----------------+ -| ACCOUNT ID | ACCOUNT NAME | THIS MONTH ($) | VS YESTERDAY ($) | LAST MONTH ($) | +| Account ID | Account Name | This Month ($) | vs Yesterday ($) | Last Month ($) | +--------------+--------------+----------------+------------------+----------------+ | 123456789012 | my-sandbox | 0.038331 | + 0.002255 | 0.127884 | | 234567890123 | my-dev | 420.102431 | + 25.801012 | 980.440598 | +--------------+--------------+----------------+------------------+----------------+ -| TOTAL | 420.140762 | + 25.803267 | 980.568482 | +| | Total | 420.140762 | + 25.803267 | 980.568482 | +--------------+--------------+----------------+------------------+----------------+ As of 2023-07-18. ``` ### Specific AWS accounts -Use `--accountIds` option to retrieve costs for specific AWS accounts. +Use `--account-ids` option to retrieve costs for specific AWS accounts. ```shell -% ./dist/acos --accountIds 123456789012,567890123456 +$ acos --account-ids 123456789012,567890123456 Account IDs specified. Retrieving accounts information... +--------------+--------------+----------------+------------------+----------------+ -| ACCOUNT ID | ACCOUNT NAME | THIS MONTH ($) | VS YESTERDAY ($) | LAST MONTH ($) | +| Account ID | Account Name | This Month ($) | vs Yesterday ($) | Last Month ($) | +--------------+--------------+----------------+------------------+----------------+ | 123456789012 | my-sandbox | 0.038331 | + 0.002255 | 0.127884 | | 567890123456 | my-prod | 5820.334869 | + 324.526062 | 10765.384186 | +--------------+--------------+----------------+------------------+----------------+ -| TOTAL | 5820.373201 | + 324.528317 | 10765.512070 | +| | Total | 5820.373201 | + 324.528317 | 10765.512070 | +--------------+--------------+----------------+------------------+----------------+ As of 2023-07-18. ``` -The `--json` option would be the best fit here because the `--accountIds` option skips the interactive account selector. You may use this combination of options to retrieve costs in a machine-readable format on a cron'd regular basis for example. +The `--json` option would be the best fit here because the `--account-ids` option skips the interactive account selector. You may use this combination of options to retrieve costs in a machine-readable format on a cron'd regular basis for example. ```shell -# Using --accountIds and --json options -% ./dist/acos --accountIds 123456789012,567890123456 --json | jq . +# Using --account-ids and --json options +$ acos --account-ids 123456789012,567890123456 --json | jq . { - "AsOf": "2023-07-18T14:18:00.182227402Z", + "AsOf": "2023-07-18T00:00:00Z", "Costs": [ { "AccountID": "123456789012", @@ -127,7 +137,6 @@ The `--json` option would be the best fit here because the `--accountIds` option ## Todo -- Add some tests - ~Support OU-based accounts listing~ done - ~Support command arguments~, configuration file, and/or env vars for repeated use - ~Support JSON format output for piped commands chaining~ done diff --git a/cmd/acos/account.go b/cmd/acos/account.go deleted file mode 100644 index 7e93ee6..0000000 --- a/cmd/acos/account.go +++ /dev/null @@ -1,158 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/AlecAivazis/survey/v2" - "github.com/toricls/acos" -) - -const ERR_AWS_ORGANIZATION_NOT_ENABLED = "This AWS account is not part of AWS Organizations organization. " -const ERR_INFUFFICIENT_IAM_PERMISSIONS = "Failed to perform \"%s\". Make sure you have sufficient IAM permissions. See https://github.com/toricls/acos#prerequisites for the details.\n" - -type GetAccountsOption struct { - AccountIds []string - OuId string -} - -// getAccounts returns a list of AWS accounts which the caller has access to. -func getAccounts(ctx context.Context, opt GetAccountsOption) (acos.Accounts, error) { - var availableAccnts acos.Accounts - var err error - - if len(opt.AccountIds) > 0 { - fmt.Fprintln(os.Stderr, "Account IDs specified. Retrieving accounts information...") - availableAccnts, err = getAccountsByIds(ctx, opt.AccountIds) - if !acos.IsOrganizationEnabled(err) { - fmt.Fprint(os.Stderr, ERR_AWS_ORGANIZATION_NOT_ENABLED) - } else if !acos.HasPermissionToOrganizationsApi(err) { - fmt.Fprintf(os.Stderr, ERR_INFUFFICIENT_IAM_PERMISSIONS, "organizations:ListAccounts") - } - } else if len(opt.OuId) > 0 { - fmt.Fprintf(os.Stderr, "Retrieving AWS accounts under the OU '%s'...\n", opt.OuId) - availableAccnts, err = getAccountsByOu(ctx, opt.OuId) - if !acos.OuExists(err) { - // To avoid duplicated error messages, we override the AWS error by our own. - err = fmt.Errorf("error the OU \"%s\" doesn't exist", opt.OuId) - // Stop the process here and we don't fall back to using the "getCallerAccount" func - // because the specified OU ID is not just valid. - return nil, err - } else if !acos.IsOrganizationEnabled(err) { - fmt.Fprint(os.Stderr, ERR_AWS_ORGANIZATION_NOT_ENABLED) - } else if !acos.HasPermissionToOrganizationsApi(err) { - fmt.Fprintf(os.Stderr, ERR_INFUFFICIENT_IAM_PERMISSIONS, "organizations:ListAccountsForParent") - } - } else { - availableAccnts, err = getAccountsInOrg(ctx) - if !acos.IsOrganizationEnabled(err) { - fmt.Fprint(os.Stderr, ERR_AWS_ORGANIZATION_NOT_ENABLED) - } else if !acos.HasPermissionToOrganizationsApi(err) { - fmt.Fprintf(os.Stderr, ERR_INFUFFICIENT_IAM_PERMISSIONS, "organizations:ListAccounts") - } - } - - if err != nil { - fmt.Fprintln(os.Stderr, "Falling back to using \"sts:GetCallerIdentity\" and \"iam:ListAccountAliases\" to obtain your AWS account information... ") - availableAccnts, err = getCallerAccount(ctx) - } - return availableAccnts, err -} - -func getAccountsByIds(ctx context.Context, accountIds []string) (acos.Accounts, error) { - accounts, err := acos.ListAccounts(ctx) - if err != nil { - return nil, err - } - availableAccnts := make(acos.Accounts, len(accountIds)) - for _, id := range accountIds { - if len(id) == 0 { - continue - } - if a, ok := accounts[id]; ok { - availableAccnts[id] = acos.Account{ - Id: a.Id, - Name: a.Name, - } - } else { - fmt.Fprintf(os.Stderr, "Account ID '%s' is not found in your AWS organization\n", id) - } - } - return availableAccnts, nil -} - -func getAccountsByOu(ctx context.Context, ouId string) (acos.Accounts, error) { - // Should regex the ouId before calling the API? - // Doc - https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListAccountsForParent.html#organizations-ListAccountsForParent-request-ParentId - return acos.ListAccountsByOu(ctx, ouId) -} - -func getAccountsInOrg(ctx context.Context) (acos.Accounts, error) { - return acos.ListAccounts(ctx) -} - -// getCallerAccount returns the AWS account information of the caller. -// -// This function expects to be used when the caller is not part of AWS Organizations organization, -// or when the caller doesn't have IAM permissions to perform "organizations:ListAccounts". -func getCallerAccount(ctx context.Context) (acos.Accounts, error) { - accnt, err := acos.GetCallerAccount(ctx) - if err != nil { - return nil, err - } - acosAccounts := make(acos.Accounts) - acosAccounts[accnt[0]] = acos.Account{ - Id: &accnt[0], - Name: &accnt[1], - } - return acosAccounts, nil -} - -// promptAccountsSelection prompts the user to select AWS accounts to retrieve costs. -// It returns an error if the `accnts` arg doesn't contain any Account. -// If the `accnts` arg contains only one Account, it never prompts the user. -func promptAccountsSelection(accnts acos.Accounts) (acos.Accounts, error) { - if len(accnts) == 0 { - return nil, fmt.Errorf("error no accounts found") - } else if len(accnts) == 1 { - // No need to prompt the user to select accounts if there is only one account. - return accnts, nil - } - - opts := make([]string, len(accnts)) - accntIds := make([]string, len(accnts)) - i := 0 - for _, a := range accnts { - opts[i] = fmt.Sprintf("%s - %s", *a.Id, *a.Name) - accntIds[i] = *a.Id - i++ - } - q := &survey.MultiSelect{ - Message: "Select accounts:", - Options: opts, - PageSize: 10, - } - - var selIdx []int - err := survey.AskOne( - q, - &selIdx, - survey.WithPageSize(10), - survey.WithKeepFilter(true), // Assuming people often use prefix/suffix to group related AWS account names like "myproduct-prod", "myproduct-dev". - survey.WithStdio(os.Stdin, os.Stderr, os.Stderr), // Use stderr for the prompt message to avoid messing up the JSON output. - ) - if err != nil { - return nil, err - } - if len(selIdx) == 0 { - return nil, fmt.Errorf("error no accounts selected") - } - - result := make(acos.Accounts) - for _, v := range selIdx { - accntId := accntIds[v] - result[accntId] = accnts[accntId] - } - return result, nil -} diff --git a/cmd/acos/account_test.go b/cmd/acos/account_test.go deleted file mode 100644 index f0518f8..0000000 --- a/cmd/acos/account_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "reflect" - "testing" - - "github.com/toricls/acos" -) - -func toPointer(s string) *string { - return &s -} - -func Test_selectAccounts(t *testing.T) { - type args struct { - accnts acos.Accounts - } - tests := []struct { - name string - args args - want acos.Accounts - wantErr bool - }{ - { - name: "error when empty accounts", - args: args{ - accnts: acos.Accounts{}, - }, - want: nil, - wantErr: true, - }, - { - name: "no prompts when only one account", - args: args{ - accnts: acos.Accounts{ - "123456789012": acos.Account{ - Id: toPointer("123456789012"), - Name: toPointer("test"), - }, - }, - }, - want: acos.Accounts{ - "123456789012": acos.Account{ - Id: toPointer("123456789012"), - Name: toPointer("test"), - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := promptAccountsSelection(tt.args.accnts) - if (err != nil) != tt.wantErr { - t.Errorf("selectAccounts() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("selectAccounts() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/cmd/acos/main.go b/cmd/acos/main.go deleted file mode 100644 index 2610c79..0000000 --- a/cmd/acos/main.go +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "os" - "sort" - "strings" - "time" - - "github.com/olekukonko/tablewriter" - - "github.com/toricls/acos" -) - -func main() { - // Flags - var ouId, asOfStr, comparedTo, commaSeparatedAccountIds string - var useJson bool - flag.StringVar(&ouId, "ou", "", "Optional - The ID of an AWS Organizational Unit (OU) or Root to list direct-children AWS accounts. It must start with 'ou-' or 'r-' prefix. This flag is ignored when the -accountIds flag is set.") - flag.StringVar(&asOfStr, "asOf", "", "Optional - The date to retrieve the cost data. The format should be 'YYYY-MM-DD'. The default value is today in UTC.") - flag.StringVar(&comparedTo, "comparedTo", "YESTERDAY", "Optional - The cost of this month will be compared to either one of 'YESTERDAY' or 'LAST_WEEK'. This flag is ignored when the -json flag is set.") - flag.BoolVar(&useJson, "json", false, "Optional - Print JSON instead of table.") - flag.StringVar(&commaSeparatedAccountIds, "accountIds", "", "Optional - Comma-separated AWS account IDs to retrieve costs. The interactive account selector is skipped when this flag is set.") - flag.Parse() - - var asOf time.Time - if len(asOfStr) > 0 { - if t, err := time.Parse("2006-01-02", asOfStr); err != nil { - fmt.Fprintln(os.Stderr, "error invalid date format for the --asOf flag. It should be 'YYYY-MM-DD'.") - os.Exit(1) - } else { - asOf = t - } - } else { - asOf = time.Now().UTC() - } - - switch comparedTo { - case "YESTERDAY": - case "LAST_WEEK": - break - default: - fmt.Fprintln(os.Stderr, "error invalid value for the -comparedTo flag. It should be either 'YESTERDAY' or 'LAST_WEEK'.") - os.Exit(2) - } - - var accountIds []string - if len(commaSeparatedAccountIds) > 0 { - accountIds = strings.Split(commaSeparatedAccountIds, ",") - } - - // Choose AWS accounts to show costs - ctx := context.Background() - var candidateAccounts, selectedAccounts acos.Accounts - var err error - candidateAccounts, err = getAccounts(ctx, GetAccountsOption{ - AccountIds: accountIds, - OuId: ouId, - }) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(3) - } - if len(accountIds) > 0 { - // Skip the interactive account selector when the -accountIds flag is set. - selectedAccounts = candidateAccounts - } else { - selectedAccounts, err = promptAccountsSelection(candidateAccounts) - } - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(4) - } - - // Get costs - var costs acos.Costs - if costs, err = acos.GetCosts(ctx, selectedAccounts, acos.NewGetCostsOption(asOf)); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(5) - } - - // Sort map keys by AWS Account ID - keys := make([]string, 0, len(costs)) - for k := range costs { - keys = append(keys, k) - } - sort.Strings(keys) - // Map to array - costArray := make([]acos.Cost, 0, len(costs)) - for _, k := range keys { - costArray = append(costArray, (costs)[k]) - } - - if useJson { - if err := printJson(costArray, asOf); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(6) - } - } else { - // Print table - printTable(costArray, comparedTo, asOf) - } -} - -func printJson(costs []acos.Cost, asOf time.Time) error { - jsonStr, err := json.Marshal(struct { - AsOf time.Time - Costs []acos.Cost - }{asOf, costs}) - if err != nil { - return err - } - fmt.Println(string(jsonStr)) - return nil -} - -func printTable(costs []acos.Cost, comparedTo string, asOf time.Time) { - t := tablewriter.NewWriter(os.Stdout) - incrHeaderTxt := "vs Yesterday ($)" - if comparedTo == "LAST_WEEK" { - incrHeaderTxt = "vs Last Week ($)" - } - t.SetHeader([]string{"Account ID", "Account Name", "This Month ($)", incrHeaderTxt, "Last Month ($)"}) - t.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT}) - totalThisMonth, totalIncrease, totalLastMonth := 0.0, 0.0, 0.0 - for _, c := range costs { - thisMonth := fmt.Sprintf("%f", c.AmountThisMonth) - incr := c.LatestDailyCostIncrease - if comparedTo == "LAST_WEEK" { - incr = c.LatestWeeklyCostIncrease - } - incrStr := fmt.Sprintf("%s %f", getAmountPrefix(incr), incr) - lastMonth := fmt.Sprintf("%f", c.AmountLastMonth) - t.Append([]string{c.AccountID, c.AccountName, thisMonth, incrStr, lastMonth}) - totalThisMonth += c.AmountThisMonth - totalIncrease += incr - totalLastMonth += c.AmountLastMonth - } - t.SetFooter([]string{"", "Total", fmt.Sprintf("%f", totalThisMonth), fmt.Sprintf("%s %f", getAmountPrefix(totalIncrease), totalIncrease), fmt.Sprintf("%f", totalLastMonth)}) - t.SetFooterAlignment(tablewriter.ALIGN_RIGHT) - t.SetCaption(true, fmt.Sprintf("As of %s.", asOf.Format("2006-01-02"))) - t.Render() -} - -func getAmountPrefix(amount float64) string { - if amount > 0.0 { - return "+" - } else if amount < 0.0 { - return "-" - } - return "" -} diff --git a/config.go b/config.go deleted file mode 100644 index e706ade..0000000 --- a/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package acos - -import ( - "context" - "log" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" -) - -var cfg aws.Config - -func init() { - var err error - cfg, err = config.LoadDefaultConfig(context.TODO()) - if err != nil { - log.Fatalf("unable to load AWS SDK config, %v", err) - } -} diff --git a/cost.go b/cost.go deleted file mode 100644 index 559b2a6..0000000 --- a/cost.go +++ /dev/null @@ -1,252 +0,0 @@ -package acos - -import ( - "context" - "fmt" - "strconv" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - - "github.com/aws/aws-sdk-go-v2/service/costexplorer" - "github.com/aws/aws-sdk-go-v2/service/costexplorer/types" -) - -type CeGetCostAndUsageAPI interface { - GetCostAndUsage(ctx context.Context, params *costexplorer.GetCostAndUsageInput, optFns ...func(*costexplorer.Options)) (*costexplorer.GetCostAndUsageOutput, error) -} - -var ( - // AWS clients - ceClient CeGetCostAndUsageAPI -) - -func init() { - ceClient = costexplorer.NewFromConfig(cfg) -} - -const ( - ceDataGranularity = "DAILY" - ceCostMetric = "UnblendedCost" - ceCostGroupBy = "LINKED_ACCOUNT" -) - -// AcosGetCostsOption represents options for GetCosts. The default values are: -// - ExcludeCredit : true -// - ExcludeUpfront: true -// - ExcludeRefund : false -// - ExcludeSupport: false -type AcosGetCostsOption struct { - ExcludeCredit bool - ExcludeUpfront bool - ExcludeRefund bool - ExcludeSupport bool - - // acos requires the following dates to show - THIS_MONTH, vs YESTERDAY, vs LAST_WEEK, and LAST_MONTH - dates struct { - asOf string - oneWeekAgo string - firstDayOfLastMonth string - firstDayOfThisMonth string // Just for flagging within the sum-up logic - } -} - -func NewGetCostsOption(asOfInUTC time.Time) AcosGetCostsOption { - opt := AcosGetCostsOption{ - ExcludeCredit: true, - ExcludeUpfront: true, - ExcludeRefund: false, - ExcludeSupport: false, - } - - oneWeekAgo := asOfInUTC.Add(time.Duration(-7) * 24 * time.Hour) - year, month, _ := asOfInUTC.Date() - firstDayOfThisMonth := time.Date(year, month, 1, 0, 0, 0, 0, asOfInUTC.Location()) - firstDayOfLastMonth := time.Date(year, month-1, 1, 0, 0, 0, 0, asOfInUTC.Location()) - - dateFmt := "2006-01-02" // Use the same format as the AWS API response, "types.ResultByTime.TimePeriod.Start/End". - opt.dates.asOf = asOfInUTC.Format(dateFmt) - opt.dates.oneWeekAgo = oneWeekAgo.Format(dateFmt) - opt.dates.firstDayOfThisMonth = firstDayOfThisMonth.Format(dateFmt) - opt.dates.firstDayOfLastMonth = firstDayOfLastMonth.Format(dateFmt) - return opt -} - -// Cost represents a cost for a given account. -type Cost struct { - AccountID string - AccountName string - LatestDailyCostIncrease float64 - LatestWeeklyCostIncrease float64 - AmountLastMonth float64 - AmountThisMonth float64 -} - -// Costs represents a map of Cost. The map key is the account ID of the respective Cost. -type Costs map[string]Cost // map[accountId]Cost - -// Group wraps up AWS Organization Group struct. -type Group types.Group - -func (g *Group) getAccountId() string { - return g.Keys[0] -} - -func (g *Group) getAmount() float64 { - if f, err := strconv.ParseFloat(*g.Metrics[ceCostMetric].Amount, 32); err == nil { - return f - } - // TODO: debug log the error - return 0 -} - -// GetCosts returns the costs for given accounts. -// It raises an error when the `accounts` arg doesn't contain any account. -func GetCosts(ctx context.Context, accounts Accounts, opt AcosGetCostsOption) (Costs, error) { - accountIds := accounts.AccountIds() - if len(accountIds) == 0 { - return nil, fmt.Errorf("error no account to retrieve cost: GetCosts requires at least one account in Accounts") - } - ceOpt := acosOptToCostExplorerOpt(opt, accountIds) - - // The following GetCostAndUsage API won't return any result in some cases (e.g. when the account is newly created). - // We create and fill the result map with the account IDs and names first, and then fill the amount later, - // to make sure the result map always contains all the accounts. - costs := make(map[string]Cost) - for _, a := range accounts { - costs[*a.Id] = Cost{ - AccountID: *a.Id, - AccountName: *a.Name, - LatestDailyCostIncrease: 0, - LatestWeeklyCostIncrease: 0, - AmountLastMonth: 0, - AmountThisMonth: 0, - } - } - - var nextToken *string - for { - ceOpt.NextPageToken = nextToken - - out, err := ceClient.GetCostAndUsage(ctx, &ceOpt) - if err != nil { - return nil, err - } - - thisMonth := false - lastWeek := false - for _, r := range out.ResultsByTime { - - if *r.TimePeriod.Start == opt.dates.firstDayOfThisMonth { - // We assume that the AWS API returns sorted "out.ResultsByTime" slice items - // by "r.TimePeriod.Start". - // So we can assume that the rest of the items are also for this month, when - // the "r.TimePeriod.Start" equals to the "first day of this month". - // - // See the official doc for more details about the response data structure: - // https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html#API_GetCostAndUsage_ResponseSyntax - thisMonth = true - } - // Flag "lastWeek" if the first week has passed of this month. - if thisMonth && *r.TimePeriod.Start == opt.dates.oneWeekAgo { - lastWeek = true - } - - for _, g := range r.Groups { - grp := Group(g) - accntId := grp.getAccountId() - amount := grp.getAmount() - - c := costs[accntId] - if thisMonth { - c.AmountThisMonth += amount - } else { - c.AmountLastMonth += amount - } - - // Store yesterday's cost as the "latest daily cost increase". - // - // The types.ResultByTime item, that represents yesterday's cost, should has - // today's date in "r.TimePeriod.End", and yesterday's date in "r.TimePeriod.Start". - // We only check the "r.TimePeriod.End" value here, because we we called the AWS API - // with the "DAILY" granularity. - if *r.TimePeriod.End == opt.dates.asOf { - if opt.dates.asOf != opt.dates.firstDayOfThisMonth { // Unless today is the first day of month. - c.LatestDailyCostIncrease = amount - } - } - - // Add the cost onto the "latest weekly cost increase". - if lastWeek { - c.LatestWeeklyCostIncrease += amount - } - - costs[accntId] = c - } - } - - nextToken = out.NextPageToken - if nextToken == nil { - break - } - } - - return costs, nil -} - -// acosOptToCostExplorerOpt returns the AWS Cost Explorer's GetCostAndUsageInput param built from the acos options. -func acosOptToCostExplorerOpt(opt AcosGetCostsOption, accountIds []string) costexplorer.GetCostAndUsageInput { - // Base input parameter - in := costexplorer.GetCostAndUsageInput{ - Granularity: ceDataGranularity, - Metrics: []string{ceCostMetric}, - TimePeriod: &types.DateInterval{ - // Get the cost for the last month and this month - Start: aws.String(opt.dates.firstDayOfLastMonth), - End: aws.String(opt.dates.asOf), - }, - GroupBy: []types.GroupDefinition{ - { - Type: types.GroupDefinitionTypeDimension, - Key: aws.String(ceCostGroupBy), - }, - }, - Filter: &types.Expression{ - And: []types.Expression{ - { - Dimensions: &types.DimensionValues{ - Key: ceCostGroupBy, - Values: accountIds, - }, - }, - }, - }, - } - - // Exclude options - v := []string{} - if opt.ExcludeCredit { - v = append(v, "Credit") - } - if opt.ExcludeUpfront { - v = append(v, "Upfront") - } - if opt.ExcludeRefund { - v = append(v, "Refund") - } - if opt.ExcludeSupport { - v = append(v, "Support") - } - if len(v) > 0 { - in.Filter.And = append(in.Filter.And, types.Expression{ - Not: &types.Expression{ - Dimensions: &types.DimensionValues{ - Key: "RECORD_TYPE", - Values: v, - }, - }, - }) - } - - return in -} diff --git a/cost_test.go b/cost_test.go deleted file mode 100644 index 50ec7c5..0000000 --- a/cost_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package acos - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/aws/aws-sdk-go-v2/service/costexplorer" - "github.com/aws/aws-sdk-go-v2/service/costexplorer/types" -) - -func TestGetCosts(t *testing.T) { - type args struct { - ctx context.Context - accounts Accounts - opt AcosGetCostsOption - } - tests := []struct { - name string - args args - want Costs - wantErr bool - }{ - { - name: "error when empty accounts", - args: args{ - ctx: context.Background(), - accounts: Accounts{}, - opt: NewGetCostsOption(time.Now().UTC()), - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetCosts(tt.args.ctx, tt.args.accounts, tt.args.opt) - if (err != nil) != tt.wantErr { - t.Errorf("GetCosts() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetCosts() = %v, want %v", got, tt.want) - } - }) - } -} - -type mockGetCostAndUsageAPI func(ctx context.Context, params *costexplorer.GetCostAndUsageInput, optFns ...func(*costexplorer.Options)) (*costexplorer.GetCostAndUsageOutput, error) - -func (m mockGetCostAndUsageAPI) GetCostAndUsage(ctx context.Context, params *costexplorer.GetCostAndUsageInput, optFns ...func(*costexplorer.Options)) (*costexplorer.GetCostAndUsageOutput, error) { - return m(ctx, params, optFns...) -} - -func TestWithMock_GetCosts(t *testing.T) { - ceClient = mockGetCostAndUsageAPI(func(ctx context.Context, params *costexplorer.GetCostAndUsageInput, optFns ...func(*costexplorer.Options)) (*costexplorer.GetCostAndUsageOutput, error) { - t.Helper() - out := &costexplorer.GetCostAndUsageOutput{ - ResultsByTime: []types.ResultByTime{}, - GroupDefinitions: []types.GroupDefinition{}, - DimensionValueAttributes: []types.DimensionValuesWithAttributes{}, - } - return out, nil - }) - - type args struct { - ctx context.Context - accounts Accounts - opt AcosGetCostsOption - } - tests := []struct { - name string - args args - want Costs - wantErr bool - }{ - { - name: "selected account must exists in result", // even GetCostAndUsage API returns no result. This is because newly created account has no billed cost in most cases. - args: args{ - ctx: context.Background(), - accounts: Accounts{ - "123456789012": Account{ - Id: toPointer("123456789012"), - Name: toPointer("test"), - }, - }, - opt: NewGetCostsOption(time.Now().UTC()), - }, - want: Costs{ - "123456789012": Cost{ - AccountID: "123456789012", - AccountName: "test", - LatestDailyCostIncrease: 0, - LatestWeeklyCostIncrease: 0, - AmountLastMonth: 0, - AmountThisMonth: 0, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetCosts(tt.args.ctx, tt.args.accounts, tt.args.opt) - if (err != nil) != tt.wantErr { - t.Errorf("GetCosts() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetCosts() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 6172cd2..0000000 --- a/go.mod +++ /dev/null @@ -1,35 +0,0 @@ -module github.com/toricls/acos - -go 1.20 - -require ( - github.com/AlecAivazis/survey/v2 v2.3.6 - github.com/aws/aws-sdk-go-v2 v1.19.0 - github.com/aws/aws-sdk-go-v2/config v1.18.28 - github.com/aws/aws-sdk-go-v2/service/costexplorer v1.25.13 - github.com/aws/aws-sdk-go-v2/service/iam v1.21.1 - github.com/aws/aws-sdk-go-v2/service/organizations v1.19.9 - github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 - github.com/olekukonko/tablewriter v0.0.5 -) - -require ( - github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect - github.com/aws/smithy-go v1.13.5 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.8 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/stretchr/testify v1.7.2 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect - golang.org/x/text v0.3.8 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 78b5d65..0000000 --- a/go.sum +++ /dev/null @@ -1,79 +0,0 @@ -github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= -github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k= -github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw= -github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A= -github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA= -github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 h1:hMUCiE3Zi5AHrRNGf5j985u0WyqI6r2NULhUfo0N/No= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU6Cb1H0U/TLEcYYp66mYqsPpcc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY= -github.com/aws/aws-sdk-go-v2/service/costexplorer v1.25.13 h1:8ni3fNbaB68IQkiaxRRcp84xbj+8IFI279obk4Y80bY= -github.com/aws/aws-sdk-go-v2/service/costexplorer v1.25.13/go.mod h1:xwe/PIa8Vj9cPclzL+WwslzsY2VCc8RJTaPaGBTbSVk= -github.com/aws/aws-sdk-go-v2/service/iam v1.21.1 h1:VTCWgsrromZqnlRgfziqqWWcW7LFkQLwJVYgf/5zgWA= -github.com/aws/aws-sdk-go-v2/service/iam v1.21.1/go.mod h1:LBsjrFczXiQLASO6FtDGTeHuZh6oHuIH6VKaOozFghg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4= -github.com/aws/aws-sdk-go-v2/service/organizations v1.19.9 h1:zGbv0aBHnx6LAZeClqK9fpO1C8IIGyToP0u62pEQiBA= -github.com/aws/aws-sdk-go-v2/service/organizations v1.19.9/go.mod h1:OvoCgJ4TLrd6ADYbUcWMc58RfeY6SM/2V1l5WqTkQUs= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 h1:e5mnydVdCVWxP+5rPAGi2PYxC7u2OZgH1ypC114H04U= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= -github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/organization.go b/organization.go deleted file mode 100644 index 6428a2f..0000000 --- a/organization.go +++ /dev/null @@ -1,100 +0,0 @@ -package acos - -import ( - "context" - "errors" - - "github.com/aws/aws-sdk-go-v2/service/organizations" - "github.com/aws/aws-sdk-go-v2/service/organizations/types" -) - -var ( - // AWS clients - organizationsClient *organizations.Client -) - -func init() { - organizationsClient = organizations.NewFromConfig(cfg) -} - -// Account wraps up AWS Organization Account struct. -type Account types.Account - -type Accounts map[string]Account // map[accountId]Account - -// AccountIds returns a list of account IDs. -func (a *Accounts) AccountIds() []string { - accountIds := make([]string, len(*a)) - i := 0 - for key := range *a { - accountIds[i] = key - i++ - } - return accountIds -} - -// ListAccounts returns a list of AWS accounts within an AWS Organization organization. -func ListAccounts(ctx context.Context) (Accounts, error) { - var nextToken *string - accnts := make(map[string]Account) - for { - out, err := organizationsClient.ListAccounts( - ctx, - &organizations.ListAccountsInput{ - NextToken: nextToken, - }, - ) - if err != nil { - return nil, err - } - for _, acc := range out.Accounts { - accnts[*acc.Id] = Account(acc) - } - nextToken = out.NextToken - if nextToken == nil { - break - } - } - return accnts, nil -} - -// ListAccountsByOu returns a list of direct-children AWS accounts of an AWS Organization OU. -func ListAccountsByOu(ctx context.Context, ouId string) (Accounts, error) { - var nextToken *string - accnts := make(map[string]Account) - for { - out, err := organizationsClient.ListAccountsForParent( - ctx, - &organizations.ListAccountsForParentInput{ - ParentId: &ouId, - NextToken: nextToken, - }, - ) - if err != nil { - return nil, err - } - for _, acc := range out.Accounts { - accnts[*acc.Id] = Account(acc) - } - nextToken = out.NextToken - if nextToken == nil { - break - } - } - return accnts, nil -} - -func IsOrganizationEnabled(err error) bool { - var errType *types.AWSOrganizationsNotInUseException - return !errors.As(err, &errType) -} - -func HasPermissionToOrganizationsApi(err error) bool { - var errType *types.AccessDeniedException - return !errors.As(err, &errType) -} - -func OuExists(err error) bool { - var errType *types.ParentNotFoundException - return !errors.As(err, &errType) -} diff --git a/organization_test.go b/organization_test.go deleted file mode 100644 index 17ce7ba..0000000 --- a/organization_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package acos - -import ( - "reflect" - "testing" -) - -func toPointer(s string) *string { - return &s -} - -func TestAccounts_AccountIds(t *testing.T) { - tests := []struct { - name string - a *Accounts - want []string - }{ - { - name: "empty", - a: &Accounts{}, - want: []string{}, - }, - { - name: "one", - a: &Accounts{ - "123456789012": Account{ - Id: toPointer("123456789012"), - Name: toPointer("test"), - }, - }, - want: []string{"123456789012"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.a.AccountIds(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Accounts.GetAccountIds() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ec9d23d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,6 @@ +use aws_config::SdkConfig; + +/// Load the default AWS SDK configuration. +pub async fn load_aws_config() -> SdkConfig { + aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await +} diff --git a/src/cost.rs b/src/cost.rs new file mode 100644 index 0000000..522f2a2 --- /dev/null +++ b/src/cost.rs @@ -0,0 +1,247 @@ +use std::collections::HashMap; + +use anyhow::{bail, Result}; +use aws_sdk_costexplorer::types::{ + DateInterval, DimensionValues, Expression, GroupDefinition, GroupDefinitionType, +}; +use aws_sdk_costexplorer::Client as CeClient; +use chrono::{Datelike, NaiveDate}; +use serde::Serialize; + +use crate::organization::{Accounts, AccountsExt}; + +const CE_DATA_GRANULARITY: aws_sdk_costexplorer::types::Granularity = + aws_sdk_costexplorer::types::Granularity::Daily; +const CE_COST_METRIC: &str = "UnblendedCost"; +const CE_COST_GROUP_BY: &str = "LINKED_ACCOUNT"; + +/// Options for GetCosts. +#[derive(Debug, Clone)] +pub struct GetCostsOption { + pub exclude_credit: bool, + pub exclude_upfront: bool, + pub exclude_refund: bool, + pub exclude_support: bool, + + pub as_of: String, + pub one_week_ago: String, + pub first_day_of_last_month: String, + pub first_day_of_this_month: String, +} + +impl GetCostsOption { + pub fn new(as_of_date: NaiveDate) -> Self { + let one_week_ago = as_of_date - chrono::Duration::days(7); + let first_day_of_this_month = + NaiveDate::from_ymd_opt(as_of_date.year(), as_of_date.month(), 1).unwrap(); + + let (prev_year, prev_month) = if as_of_date.month() == 1 { + (as_of_date.year() - 1, 12) + } else { + (as_of_date.year(), as_of_date.month() - 1) + }; + let first_day_of_last_month = NaiveDate::from_ymd_opt(prev_year, prev_month, 1).unwrap(); + + let fmt = "%Y-%m-%d"; + Self { + exclude_credit: true, + exclude_upfront: true, + exclude_refund: false, + exclude_support: false, + as_of: as_of_date.format(fmt).to_string(), + one_week_ago: one_week_ago.format(fmt).to_string(), + first_day_of_this_month: first_day_of_this_month.format(fmt).to_string(), + first_day_of_last_month: first_day_of_last_month.format(fmt).to_string(), + } + } +} + +/// Represents a cost for a given account. +#[derive(Debug, Clone, Serialize)] +pub struct Cost { + #[serde(rename = "AccountID")] + pub account_id: String, + #[serde(rename = "AccountName")] + pub account_name: String, + #[serde(rename = "LatestDailyCostIncrease")] + pub latest_daily_cost_increase: f64, + #[serde(rename = "LatestWeeklyCostIncrease")] + pub latest_weekly_cost_increase: f64, + #[serde(rename = "AmountLastMonth")] + pub amount_last_month: f64, + #[serde(rename = "AmountThisMonth")] + pub amount_this_month: f64, +} + +/// A map of account ID -> Cost. +pub type Costs = HashMap; + +/// Get costs for the given accounts. +pub async fn get_costs( + client: &CeClient, + accounts: &Accounts, + opt: &GetCostsOption, +) -> Result { + let account_ids = accounts.account_ids(); + if account_ids.is_empty() { + bail!("error no account to retrieve cost: GetCosts requires at least one account in Accounts"); + } + + // Initialize costs map with all accounts (some may have no billing data yet) + let mut costs: Costs = HashMap::new(); + for (id, acc) in accounts { + costs.insert( + id.clone(), + Cost { + account_id: acc.id.clone(), + account_name: acc.name.clone(), + latest_daily_cost_increase: 0.0, + latest_weekly_cost_increase: 0.0, + amount_last_month: 0.0, + amount_this_month: 0.0, + }, + ); + } + + let ce_input = build_cost_explorer_input(opt, &account_ids); + + let mut next_token: Option = None; + loop { + let mut req = client + .get_cost_and_usage() + .granularity(CE_DATA_GRANULARITY) + .set_metrics(Some(vec![CE_COST_METRIC.to_string()])) + .time_period(ce_input.time_period.clone()) + .set_group_by(Some(ce_input.group_by.clone())) + .set_filter(Some(ce_input.filter.clone())); + + if let Some(token) = &next_token { + req = req.next_page_token(token); + } + + let output = req.send().await?; + + let mut this_month = false; + let mut last_week = false; + + for r in output.results_by_time() { + let period = r.time_period().unwrap(); + let start = period.start(); + let end = period.end(); + + if start == opt.first_day_of_this_month { + this_month = true; + } + if this_month && start == opt.one_week_ago { + last_week = true; + } + + for g in r.groups() { + let account_id = g.keys().first().map(|s| s.as_str()).unwrap_or_default(); + let amount: f64 = g + .metrics() + .and_then(|m| m.get(CE_COST_METRIC)) + .and_then(|v| v.amount()) + .and_then(|a| a.parse::().ok()) + .unwrap_or(0.0); + + if let Some(c) = costs.get_mut(account_id) { + if this_month { + c.amount_this_month += amount; + } else { + c.amount_last_month += amount; + } + + // Yesterday's cost + if end == opt.as_of && opt.as_of != opt.first_day_of_this_month { + c.latest_daily_cost_increase = amount; + } + + // Weekly cost increase + if last_week { + c.latest_weekly_cost_increase += amount; + } + } + } + } + + next_token = output.next_page_token().map(|s| s.to_string()); + if next_token.is_none() { + break; + } + } + + Ok(costs) +} + +struct CeInput { + time_period: DateInterval, + group_by: Vec, + filter: Expression, +} + +fn build_cost_explorer_input(opt: &GetCostsOption, account_ids: &[String]) -> CeInput { + let time_period = DateInterval::builder() + .start(&opt.first_day_of_last_month) + .end(&opt.as_of) + .build() + .unwrap(); + + let group_by = vec![GroupDefinition::builder() + .r#type(GroupDefinitionType::Dimension) + .key(CE_COST_GROUP_BY) + .build()]; + + // Build filter: account IDs AND exclude record types + let account_filter = Expression::builder() + .dimensions( + DimensionValues::builder() + .key(aws_sdk_costexplorer::types::Dimension::LinkedAccount) + .set_values(Some(account_ids.to_vec())) + .build(), + ) + .build(); + + let mut and_expressions = vec![account_filter]; + + // Exclude record types + let mut exclude_values = Vec::new(); + if opt.exclude_credit { + exclude_values.push("Credit".to_string()); + } + if opt.exclude_upfront { + exclude_values.push("Upfront".to_string()); + } + if opt.exclude_refund { + exclude_values.push("Refund".to_string()); + } + if opt.exclude_support { + exclude_values.push("Support".to_string()); + } + + if !exclude_values.is_empty() { + let exclude_expr = Expression::builder() + .not( + Expression::builder() + .dimensions( + DimensionValues::builder() + .key(aws_sdk_costexplorer::types::Dimension::RecordType) + .set_values(Some(exclude_values)) + .build(), + ) + .build(), + ) + .build(); + and_expressions.push(exclude_expr); + } + + let filter = Expression::builder() + .set_and(Some(and_expressions)) + .build(); + + CeInput { + time_period, + group_by, + filter, + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e86e06c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,354 @@ +mod config; +mod cost; +mod organization; +mod sts; + +use std::io::Write; +use std::process; + +use anyhow::Result; +use aws_sdk_costexplorer::Client as CeClient; +use aws_sdk_iam::Client as IamClient; +use aws_sdk_organizations::Client as OrganizationsClient; +use aws_sdk_sts::Client as StsClient; +use chrono::NaiveDate; +use clap::Parser; +use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Table}; +use dialoguer::MultiSelect; +use serde::Serialize; + +use cost::{Cost, GetCostsOption}; +use organization::{Account, Accounts}; + +const ERR_AWS_ORGANIZATION_NOT_ENABLED: &str = + "This AWS account is not part of AWS Organizations organization. "; +const ERR_INSUFFICIENT_IAM_PERMISSIONS: &str = + "Failed to perform \"{}\". Make sure you have sufficient IAM permissions. See https://github.com/toricls/acos#prerequisites for the details."; + +/// An interactive CLI tool to retrieve and show your AWS costs. +#[derive(Parser, Debug)] +#[command(name = "acos", version, about)] +struct Cli { + /// The ID of an AWS Organizational Unit (OU) or Root to list direct-children AWS accounts. + /// It must start with 'ou-' or 'r-' prefix. This flag is ignored when --account-ids is set. + #[arg(long = "ou", default_value = "")] + ou: String, + + /// The date to retrieve the cost data. The format should be 'YYYY-MM-DD'. + /// The default value is today in UTC. + #[arg(long = "as-of", default_value = "")] + as_of: String, + + /// The cost of this month will be compared to either one of 'YESTERDAY' or 'LAST_WEEK'. + /// This flag is ignored when --json is set. + #[arg(long = "compared-to", default_value = "YESTERDAY")] + compared_to: String, + + /// Print JSON instead of table. + #[arg(long = "json", default_value_t = false)] + json: bool, + + /// Comma-separated AWS account IDs to retrieve costs. + /// The interactive account selector is skipped when this flag is set. + #[arg(long = "account-ids", default_value = "")] + account_ids: String, +} + +#[tokio::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("{}", e); + process::exit(1); + } +} + +async fn run() -> Result<()> { + let cli = Cli::parse(); + + // Parse as_of date + let as_of = if cli.as_of.is_empty() { + chrono::Utc::now().date_naive() + } else { + match NaiveDate::parse_from_str(&cli.as_of, "%Y-%m-%d") { + Ok(d) => d, + Err(_) => { + eprintln!( + "error invalid date format for the --as-of flag. It should be 'YYYY-MM-DD'." + ); + process::exit(1); + } + } + }; + + // Validate compared_to + match cli.compared_to.as_str() { + "YESTERDAY" | "LAST_WEEK" => {} + _ => { + eprintln!("error invalid value for the --compared-to flag. It should be either 'YESTERDAY' or 'LAST_WEEK'."); + process::exit(2); + } + } + + // Parse account IDs + let account_ids: Vec = if cli.account_ids.is_empty() { + vec![] + } else { + cli.account_ids + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + }; + + // Load AWS config and create clients + let aws_cfg = config::load_aws_config().await; + let org_client = OrganizationsClient::new(&aws_cfg); + let sts_client = StsClient::new(&aws_cfg); + let iam_client = IamClient::new(&aws_cfg); + let ce_client = CeClient::new(&aws_cfg); + + // Get candidate accounts + let candidate_accounts = get_accounts( + &org_client, + &sts_client, + &iam_client, + &account_ids, + &cli.ou, + ) + .await?; + + // Select accounts + let selected_accounts = if !account_ids.is_empty() { + // Skip interactive selector when account IDs are explicitly provided + candidate_accounts + } else { + prompt_accounts_selection(&candidate_accounts)? + }; + + // Get costs + let opt = GetCostsOption::new(as_of); + let costs = cost::get_costs(&ce_client, &selected_accounts, &opt).await?; + + // Sort by account ID + let mut cost_array: Vec = costs.into_values().collect(); + cost_array.sort_by(|a, b| a.account_id.cmp(&b.account_id)); + + if cli.json { + print_json(&cost_array, &as_of)?; + } else { + print_table(&cost_array, &cli.compared_to, &as_of); + } + + Ok(()) +} + +async fn get_accounts( + org_client: &OrganizationsClient, + sts_client: &StsClient, + iam_client: &IamClient, + account_ids: &[String], + ou_id: &str, +) -> Result { + let result = if !account_ids.is_empty() { + eprintln!("Account IDs specified. Retrieving accounts information..."); + get_accounts_by_ids(org_client, account_ids).await + } else if !ou_id.is_empty() { + eprintln!( + "Retrieving AWS accounts under the OU '{}'...", + ou_id + ); + let r = organization::list_accounts_by_ou(org_client, ou_id).await; + if let Err(ref e) = r { + if organization::ou_not_found(e) { + anyhow::bail!("error the OU \"{}\" doesn't exist", ou_id); + } + } + r + } else { + organization::list_accounts(org_client).await + }; + + match result { + Ok(accounts) => Ok(accounts), + Err(ref e) => { + if organization::is_organization_not_enabled(e) { + eprint!("{}", ERR_AWS_ORGANIZATION_NOT_ENABLED); + } else if organization::has_no_permission_to_organizations_api(e) { + eprintln!( + "{}", + ERR_INSUFFICIENT_IAM_PERMISSIONS.replace("{}", "organizations:ListAccounts") + ); + } + eprintln!("Falling back to using \"sts:GetCallerIdentity\" and \"iam:ListAccountAliases\" to obtain your AWS account information... "); + get_caller_account(sts_client, iam_client).await + } + } +} + +async fn get_accounts_by_ids( + org_client: &OrganizationsClient, + account_ids: &[String], +) -> Result { + let all_accounts = organization::list_accounts(org_client).await?; + let mut result = Accounts::new(); + + for id in account_ids { + if id.is_empty() { + continue; + } + if let Some(acc) = all_accounts.get(id) { + result.insert(id.clone(), acc.clone()); + } else { + eprintln!("Account ID '{}' is not found in your AWS organization", id); + } + } + + Ok(result) +} + +async fn get_caller_account(sts_client: &StsClient, iam_client: &IamClient) -> Result { + let (account_id, account_name) = sts::get_caller_account(sts_client, iam_client).await?; + let mut accounts = Accounts::new(); + accounts.insert( + account_id.clone(), + Account { + id: account_id, + name: account_name, + }, + ); + Ok(accounts) +} + +fn prompt_accounts_selection(accounts: &Accounts) -> Result { + if accounts.is_empty() { + anyhow::bail!("error no accounts found"); + } + + if accounts.len() == 1 { + return Ok(accounts.clone()); + } + + // Sort accounts for consistent ordering + let mut sorted_accounts: Vec<(&String, &Account)> = accounts.iter().collect(); + sorted_accounts.sort_by_key(|(id, _)| id.to_string()); + + let options: Vec = sorted_accounts + .iter() + .map(|(_, acc)| format!("{} - {}", acc.id, acc.name)) + .collect(); + + let selections = MultiSelect::new() + .with_prompt("Select accounts") + .items(&options) + .interact_on(&dialoguer::console::Term::stderr())?; + + if selections.is_empty() { + anyhow::bail!("error no accounts selected"); + } + + let mut result = Accounts::new(); + for idx in selections { + let (id, acc) = &sorted_accounts[idx]; + result.insert(id.to_string(), (*acc).clone()); + } + + Ok(result) +} + +#[derive(Serialize)] +struct JsonOutput { + #[serde(rename = "AsOf")] + as_of: String, + #[serde(rename = "Costs")] + costs: Vec, +} + +fn print_json(costs: &[Cost], as_of: &NaiveDate) -> Result<()> { + let output = JsonOutput { + as_of: format!("{}T00:00:00Z", as_of.format("%Y-%m-%d")), + costs: costs.to_vec(), + }; + let json_str = serde_json::to_string(&output)?; + println!("{}", json_str); + Ok(()) +} + +fn print_table(costs: &[Cost], compared_to: &str, as_of: &NaiveDate) { + let incr_header = if compared_to == "LAST_WEEK" { + "vs Last Week ($)" + } else { + "vs Yesterday ($)" + }; + + let mut table = Table::new(); + table.set_content_arrangement(ContentArrangement::Dynamic); + table.set_header(vec![ + Cell::new("Account ID"), + Cell::new("Account Name"), + Cell::new("This Month ($)").set_alignment(CellAlignment::Right), + Cell::new(incr_header).set_alignment(CellAlignment::Right), + Cell::new("Last Month ($)").set_alignment(CellAlignment::Right), + ]); + + let mut total_this_month = 0.0_f64; + let mut total_increase = 0.0_f64; + let mut total_last_month = 0.0_f64; + + for c in costs { + let incr = if compared_to == "LAST_WEEK" { + c.latest_weekly_cost_increase + } else { + c.latest_daily_cost_increase + }; + + let incr_str = format!("{} {:.6}", get_amount_prefix(incr), incr.abs()); + + table.add_row(vec![ + Cell::new(&c.account_id), + Cell::new(&c.account_name), + Cell::new(format!("{:.6}", c.amount_this_month)).set_alignment(CellAlignment::Right), + Cell::new(&incr_str).set_alignment(CellAlignment::Right), + Cell::new(format!("{:.6}", c.amount_last_month)).set_alignment(CellAlignment::Right), + ]); + + total_this_month += c.amount_this_month; + total_increase += incr; + total_last_month += c.amount_last_month; + } + + // Footer row + let total_incr_str = format!( + "{} {:.6}", + get_amount_prefix(total_increase), + total_increase.abs() + ); + table.add_row(vec![ + Cell::new("").add_attribute(Attribute::Bold), + Cell::new("Total").add_attribute(Attribute::Bold), + Cell::new(format!("{:.6}", total_this_month)) + .set_alignment(CellAlignment::Right) + .add_attribute(Attribute::Bold), + Cell::new(&total_incr_str) + .set_alignment(CellAlignment::Right) + .add_attribute(Attribute::Bold), + Cell::new(format!("{:.6}", total_last_month)) + .set_alignment(CellAlignment::Right) + .add_attribute(Attribute::Bold), + ]); + + println!("{table}"); + println!("As of {}.", as_of.format("%Y-%m-%d")); + + // Flush stdout + let _ = std::io::stdout().flush(); +} + +fn get_amount_prefix(amount: f64) -> &'static str { + if amount > 0.0 { + "+" + } else if amount < 0.0 { + "-" + } else { + "" + } +} diff --git a/src/organization.rs b/src/organization.rs new file mode 100644 index 0000000..43f9ff7 --- /dev/null +++ b/src/organization.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; + +use anyhow::Result; +use aws_sdk_organizations::Client as OrganizationsClient; + +/// Represents an AWS account with its ID and name. +#[derive(Debug, Clone)] +pub struct Account { + pub id: String, + pub name: String, +} + +/// A map of account ID -> Account. +pub type Accounts = HashMap; + +/// Extension trait for Accounts to get account IDs. +pub trait AccountsExt { + fn account_ids(&self) -> Vec; +} + +impl AccountsExt for Accounts { + fn account_ids(&self) -> Vec { + self.keys().cloned().collect() + } +} + +/// List all accounts in the AWS Organization. +pub async fn list_accounts(client: &OrganizationsClient) -> Result { + let mut accounts = HashMap::new(); + let mut next_token: Option = None; + + loop { + let mut req = client.list_accounts(); + if let Some(token) = &next_token { + req = req.next_token(token); + } + + let output = req.send().await?; + + for acc in output.accounts() { + let id = acc.id().unwrap_or_default().to_string(); + let name = acc.name().unwrap_or_default().to_string(); + accounts.insert(id.clone(), Account { id, name }); + } + + next_token = output.next_token().map(|s| s.to_string()); + if next_token.is_none() { + break; + } + } + + Ok(accounts) +} + +/// List accounts under a specific OU (Organizational Unit). +pub async fn list_accounts_by_ou( + client: &OrganizationsClient, + ou_id: &str, +) -> Result { + let mut accounts = HashMap::new(); + let mut next_token: Option = None; + + loop { + let mut req = client.list_accounts_for_parent().parent_id(ou_id); + if let Some(token) = &next_token { + req = req.next_token(token); + } + + let output = req.send().await?; + + for acc in output.accounts() { + let id = acc.id().unwrap_or_default().to_string(); + let name = acc.name().unwrap_or_default().to_string(); + accounts.insert(id.clone(), Account { id, name }); + } + + next_token = output.next_token().map(|s| s.to_string()); + if next_token.is_none() { + break; + } + } + + Ok(accounts) +} + +/// Check if the error indicates that AWS Organizations is not enabled. +pub fn is_organization_not_enabled(err: &anyhow::Error) -> bool { + let err_str = format!("{:?}", err); + err_str.contains("AWSOrganizationsNotInUseException") +} + +/// Check if the error indicates insufficient permissions for Organizations API. +pub fn has_no_permission_to_organizations_api(err: &anyhow::Error) -> bool { + let err_str = format!("{:?}", err); + err_str.contains("AccessDeniedException") +} + +/// Check if the error indicates the OU does not exist. +pub fn ou_not_found(err: &anyhow::Error) -> bool { + let err_str = format!("{:?}", err); + err_str.contains("ParentNotFoundException") +} diff --git a/src/sts.rs b/src/sts.rs new file mode 100644 index 0000000..b131173 --- /dev/null +++ b/src/sts.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use aws_sdk_iam::Client as IamClient; +use aws_sdk_sts::Client as StsClient; + +/// Get the caller's account ID and name (alias). +/// Returns (account_id, account_name). +pub async fn get_caller_account( + sts_client: &StsClient, + iam_client: &IamClient, +) -> Result<(String, String)> { + let identity = sts_client.get_caller_identity().send().await?; + let account_id = identity.account().unwrap_or_default().to_string(); + + // Try to fetch human-readable account name via IAM alias + let aliases_output = iam_client.list_account_aliases().send().await?; + let account_name = if let Some(first) = aliases_output.account_aliases().first() { + first.clone() + } else { + "Name not configured".to_string() + }; + + Ok((account_id, account_name)) +} diff --git a/sts.go b/sts.go deleted file mode 100644 index 2c2ed8f..0000000 --- a/sts.go +++ /dev/null @@ -1,40 +0,0 @@ -package acos - -import ( - "context" - - "github.com/aws/aws-sdk-go-v2/service/iam" - "github.com/aws/aws-sdk-go-v2/service/sts" -) - -var ( - // AWS clients - stsClient *sts.Client - iamClient *iam.Client -) - -func init() { - stsClient = sts.NewFromConfig(cfg) - iamClient = iam.NewFromConfig(cfg) -} - -// GetCallerAccount returns the account ID and name for the current user session. -func GetCallerAccount(ctx context.Context) ([]string, error) { - res := make([]string, 2) - out, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) - if err != nil { - return res, err - } - res[0] = *out.Account // Account ID - // Try to fetch human-readable account name - out2, err := iamClient.ListAccountAliases(ctx, &iam.ListAccountAliasesInput{}) - if err != nil { - return res, err - } - if len(out2.AccountAliases) > 0 { - res[1] = out2.AccountAliases[0] // Alias as account name - } else { - res[1] = "Name not configured" - } - return res, nil -}