From 61a1fb4810178d1e799358ba8fe6788608a71e3d Mon Sep 17 00:00:00 2001 From: Clete Blackwell II Date: Fri, 16 Jun 2023 08:28:37 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Initial=20commit=20of=20?= =?UTF-8?q?existing=20code=20and=20Terraform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 20 +- .releaserc.yml | 27 + .tarpaulin.toml | 6 + Cargo.lock | 2270 +++++++++++++++++ Cargo.toml | 40 + README.md | 35 +- docs/Diagram.drawio.png | Bin 0 -> 63809 bytes rustfmt.toml | 1 + scripts/build-release.sh | 9 + scripts/coverage.sh | 5 + scripts/pre-commit.sh | 15 + scripts/test.sh | 4 + src/bin/global_retention_setter.rs | 603 +++++ ...retention_setter__tests__CWMetricCall.snap | 81 + ...process_all_log_group_partial_success.snap | 81 + ..._single_already_tagged_with_retention.snap | 81 + ...ricCall_process_all_log_group_success.snap | 81 + ...process_all_log_group_partial_success.snap | 5 + ..._single_already_tagged_with_retention.snap | 5 + ..._tests__process_all_log_group_success.snap | 5 + ...etter__tests__process_log_group_fails.snap | 12 + src/cloudwatch_logs_traits.rs | 127 + src/cloudwatch_metrics_traits.rs | 47 + src/error.rs | 33 + src/event.rs | 40 + src/global.rs | 144 ++ src/lib.rs | 7 + src/main.rs | 505 ++++ src/metric_publisher.rs | 130 + src/retention_setter.rs | 99 + ...ention__global__tests__cw_logs_client.snap | 40 + ...ion__global__tests__cw_metrics_client.snap | 40 + ...metric_publisher__tests__CWMetricCall.snap | 36 + ...__CWMetricCall_publish_metrics_failed.snap | 36 + ..._CWMetricCall_publish_metrics_success.snap | 36 + ..._not_overwrite_when_retention_tag_set.snap | 21 + ..._event_fails_when_tag_log_group_fails.snap | 21 + ...l_process_event_retention_already_set.snap | 21 + ...ricCall_process_event_success_no_tags.snap | 21 + ...og_retention__tests__parse_event_fail.snap | 8 + ...ntion__tests__process_event_bad_input.snap | 8 + ..._not_overwrite_when_retention_tag_set.snap | 7 + ...fails_when_put_retention_policy_fails.snap | 8 + ..._event_fails_when_tag_log_group_fails.snap | 8 + ...__process_event_retention_already_set.snap | 7 + ..._tests__process_event_success_no_tags.snap | 7 + tf-alarm-topic.tf | 66 + tf-alarms.tf | 58 + tf-cloudwatch-logs.tf | 31 + tf-global-retention-setter.tf | 73 + tf-inputs.tf | 96 + tf-lambda-iam-role.tf | 63 + tf-locals.tf | 14 + tf-log-retention-lambda.tf | 80 + tf-lookups.tf | 40 + tf-security-group.tf | 18 + 56 files changed, 5374 insertions(+), 8 deletions(-) create mode 100644 .releaserc.yml create mode 100644 .tarpaulin.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 docs/Diagram.drawio.png create mode 100644 rustfmt.toml create mode 100755 scripts/build-release.sh create mode 100755 scripts/coverage.sh create mode 100755 scripts/pre-commit.sh create mode 100755 scripts/test.sh create mode 100644 src/bin/global_retention_setter.rs create mode 100644 src/bin/snapshots/global_retention_setter__tests__CWMetricCall.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_partial_success.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_single_already_tagged_with_retention.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_success.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__process_all_log_group_partial_success.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__process_all_log_group_single_already_tagged_with_retention.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__process_all_log_group_success.snap create mode 100644 src/bin/snapshots/global_retention_setter__tests__process_log_group_fails.snap create mode 100644 src/cloudwatch_logs_traits.rs create mode 100644 src/cloudwatch_metrics_traits.rs create mode 100644 src/error.rs create mode 100644 src/event.rs create mode 100644 src/global.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/metric_publisher.rs create mode 100644 src/retention_setter.rs create mode 100644 src/snapshots/terraform_aws_default_log_retention__global__tests__cw_logs_client.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__global__tests__cw_metrics_client.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_failed.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_success.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_do_not_overwrite_when_retention_tag_set.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_fails_when_tag_log_group_fails.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_retention_already_set.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_success_no_tags.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__parse_event_fail.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__process_event_bad_input.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__process_event_do_not_overwrite_when_retention_tag_set.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_put_retention_policy_fails.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_tag_log_group_fails.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__process_event_retention_already_set.snap create mode 100644 src/snapshots/terraform_aws_default_log_retention__tests__process_event_success_no_tags.snap create mode 100644 tf-alarm-topic.tf create mode 100644 tf-alarms.tf create mode 100644 tf-cloudwatch-logs.tf create mode 100644 tf-global-retention-setter.tf create mode 100644 tf-inputs.tf create mode 100644 tf-lambda-iam-role.tf create mode 100644 tf-locals.tf create mode 100644 tf-log-retention-lambda.tf create mode 100644 tf-lookups.tf create mode 100644 tf-security-group.tf diff --git a/.gitignore b/.gitignore index 6985cf1..7f685a4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,24 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +.terraform.lock.hcl +.terraform/ +.env +bootstrap.zip +.DS_Store +.dtmp +# Added by cargo + +/target +reports/tarpaulin-report.html + +# DrawIO Temp Files +.$*.bkp +.$*.dtmp \ No newline at end of file diff --git a/.releaserc.yml b/.releaserc.yml new file mode 100644 index 0000000..99b5f10 --- /dev/null +++ b/.releaserc.yml @@ -0,0 +1,27 @@ +# -------------------------------------------- +# +# Standard recommended semantic-release configuration. +# +# Alternative configurations +# - A .releaserc file, written in YAML or JSON, with optional extensions: .yaml/.yml/.json/.js +# - A release.config.js file that exports an object +# - A release key in the project's package.json file +# Source: https://semantic-release.gitbook.io/semantic-release/usage/configuration#configuration-file +# +#---------------------------------------------- + +--- +branches: + - master + - main +plugins: + - "@semantic-release/commit-analyzer" + - - "@semantic-release/release-notes-generator" + - "@semantic-release/changelog" + - - "@semantic-release/git" + - assets: + - CHANGELOG.md + - dist/global_retention_setter/bootstrap + - dist/log_retention_setter/bootstrap + - README.md + message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" diff --git a/.tarpaulin.toml b/.tarpaulin.toml new file mode 100644 index 0000000..db3eb21 --- /dev/null +++ b/.tarpaulin.toml @@ -0,0 +1,6 @@ +[coverage] +exclude-files = ["src/cloudwatch_logs_traits.rs", "src/cloudwatch_metrics_traits.rs", "src/error.rs"] + +[report] +out = ["Html", "Xml", "Stdout"] +output-dir = "reports" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9606228 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2270 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "assert-json-diff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" +dependencies = [ + "extend", + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "async_once" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "aws-config" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3d1e2a1f1ab3ac6c4b884e37413eaa03eb9d901e4fc68ee8f5c1d49721680e" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-sdk-sso", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes", + "hex", + "http", + "hyper", + "ring", + "time", + "tokio", + "tower", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0696a0523a39a19087747e4dafda0362dc867531e3d72a3f195564c84e5e08" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-endpoint" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a4f935ab6a1919fbfd6102a80c4fccd9ff5f47f94ba154074afe1051903261" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "aws-types", + "http", + "regex", + "tracing", +] + +[[package]] +name = "aws-http" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82976ca4e426ee9ca3ffcf919d9b2c8d14d0cd80d43cc02173737a8f07f28d4d" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-types", + "aws-types", + "bytes", + "http", + "http-body", + "lazy_static", + "percent-encoding", + "pin-project-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-cloudwatch" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca35aa6f343ca08847b0f1bbebf2cf8518e2d0e6db7409e410001113536d98cd" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "http", + "regex", + "tokio-stream", + "tower", +] + +[[package]] +name = "aws-sdk-cloudwatchlogs" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f8dbe7e86cb0c5999cb5911e052ec1e6e3f4abbbcb1333a63538b4aecdaa9b" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes", + "http", + "regex", + "tokio-stream", + "tower", +] + +[[package]] +name = "aws-sdk-sso" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0119bacf0c42f587506769390983223ba834e605f049babe514b2bd646dbb2" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes", + "http", + "regex", + "tokio-stream", + "tower", +] + +[[package]] +name = "aws-sdk-sts" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270b6a33969ebfcb193512fbd5e8ee5306888ad6c6d5d775cdbfb2d50d94de26" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "http", + "regex", + "tower", + "tracing", +] + +[[package]] +name = "aws-sig-auth" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "660a02a98ab1af83bd8d714afbab2d502ba9b18c49e7e4cddd6bf8837ff778cb" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-http", + "aws-types", + "http", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86529e7b64d902efea8fff52c1b2529368d04f90305cf632729e3713f6b57dc0" +dependencies = [ + "aws-smithy-http", + "form_urlencoded", + "hex", + "hmac", + "http", + "once_cell", + "percent-encoding", + "regex", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c712a28a4f2f2139759235c08bf98aca99d4fdf1b13c78c5f95613df0a5db9" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tokio-stream", +] + +[[package]] +name = "aws-smithy-client" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104ca17f56cde00a10207169697dfe9c6810db339d52fb352707e64875b30a44" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-protocol-test", + "aws-smithy-types", + "bytes", + "fastrand", + "http", + "http-body", + "hyper", + "hyper-rustls", + "lazy_static", + "pin-project-lite", + "serde", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873f316f1833add0d3aa54ed1b0cd252ddd88c792a0cf839886400099971e844" +dependencies = [ + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http", + "http-body", + "hyper", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "aws-smithy-http-tower" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38231d3f5dac9ac7976f44e12803add1385119ffca9e5f050d8e980733d164" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd83ff2b79e9f729746fcc8ad798676b68fe6ea72986571569a5306a277a182" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-protocol-test" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d1c9bcb35ce11055ec128dab2c66a7ed47e2dfff99883e32c21a1ab6d6bee6" +dependencies = [ + "assert-json-diff", + "http", + "pretty_assertions", + "regex", + "roxmltree", + "serde_json", + "thiserror", +] + +[[package]] +name = "aws-smithy-query" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2f0445dafe9d2cd50b44339ae3c3ed46549aad8ac696c52ad660b3e7ae8682b" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-types" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8161232eda10290f5136610a1eb9de56aceaccd70c963a26a260af20ac24794f" +dependencies = [ + "base64-simd", + "itoa", + "num-integer", + "ryu", + "time", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343ffe9a9bb3f542675f4df0e0d5933513d6ad038ca3907ad1767ba690a99684" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f15b34253b68cde08e39b0627cc6101bcca64351229484b4743392c035d057" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-types", + "http", + "rustc_version", + "tracing", +] + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[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 = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytes-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cached" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e6092f8c7ba6e65a46f6f26d7d7997201d3a6f0e69ff5d2440b930d7c0513a" +dependencies = [ + "async-trait", + "async_once", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown", + "instant", + "lazy_static", + "once_cell", + "thiserror", + "tokio", +] + +[[package]] +name = "cached_proc_macro" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751f7f4e7a091545e7f6c65bacc404eaee7e87bfb1f9ece234a1caa173dc16f2" +dependencies = [ + "cached_proc_macro_types", + "darling", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "extend" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47da3a72ec598d9c8937a7ebca8962a5c7a1f28444e38c2b33c771ba3f55f05" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "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", +] + +[[package]] +name = "h2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[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", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "insta" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a28d25139df397cbca21408bb742cf6837e04cdbebf1b07b760caf971d6a972" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "regex", + "similar", + "yaml-rust", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lambda_runtime" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db262b5e9548a371d9e1dd54ba094b75e3e46b345c11458ea2b4d4c54fbffa9" +dependencies = [ + "async-stream", + "bytes", + "futures", + "http", + "hyper", + "lambda_runtime_api_client", + "serde", + "serde_json", + "serde_path_to_error", + "tokio", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "lambda_runtime_api_client" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "690c5ae01f3acac8c9c3348b556fc443054e9b7f1deaf53e9ebab716282bf0ed" +dependencies = [ + "http", + "hyper", + "tokio", + "tower-service", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "terraform-aws-default-log-retention" +version = "1.0.0" +dependencies = [ + "async-trait", + "aws-config", + "aws-sdk-cloudwatch", + "aws-sdk-cloudwatchlogs", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-types", + "cached", + "ctor", + "env_logger", + "futures", + "http", + "insta", + "lambda_runtime", + "log", + "mockall", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing-subscriber", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tokio" +version = "1.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..767cca2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,40 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package] +name = "terraform-aws-default-log-retention" +authors = ["Clete Blackwell II "] +version = "1.0.0" +edition = "2021" +default-run = "terraform-aws-default-log-retention" + +[profile.release-lambda] +inherits = "release" +lto = true # https://doc.rust-lang.org/cargo/reference/profiles.html#lto Big difference in binary size with a similarly-large impact to compile time. +codegen-units = 1 # This is pretty extreme; eliminates parallel compilation. Makes a difference in binary size. + +[dependencies] +tokio = { version = "1.21", features = ["macros"] } +log = "0.4" +tracing-subscriber = { version = "0", features = ["env-filter"] } +# NOTE: the following crate is not part of the SDK, but it is maintained by AWS. +lambda_runtime = "0.8" +serde_json = "1.0" +serde = "1.0" +cached = "0.38" +env_logger = "0.10" +aws-sdk-cloudwatchlogs = "0.24" +aws-sdk-cloudwatch = "0.24" +aws-config = "0.54" +aws-smithy-types = "0.54" +aws-smithy-client = "0.54" +tokio-stream = "0.1" +async-trait = "0.1" + +[dev-dependencies] +aws-smithy-http = "0.54" +futures = "0.3" +http = "0.2" +insta = { version = "1.21", features = ["filters"] } +aws-smithy-client = { version = "0.54", features = ["test-util"] } +mockall = "0.11" +ctor = "0.1.26" diff --git a/README.md b/README.md index 0124ea6..9d381f6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ If `log_group_tags` is set, the module will also set a `retention`=`Set by AWS D ![diagram](./docs/Diagram.drawio.png) -# Example +# Example Usage +## Single Region ```terraform locals { notification_list = ["your.email@example.com"] @@ -27,6 +28,33 @@ locals { log_retention_name = "default-log-retention" } +module "log_retention" { + source = "git::https://github.com/StateFarmIns/terraform-aws-default-log-retention.git" + + name = local.log_retention_name + + log_level = "info" + log_retention_in_days = local.log_retention_in_days + global_log_retention_run_period = local.global_log_retention_run_period + + alarm_configuration = { + email_notification_list = local.notification_list + } +} +``` + + +## Multi-region +``` +# Define an aws provider for your primary region, and an aliased "secondary" provider for the secondary region + +locals { + notification_list = ["your.email@example.com"] + global_log_retention_run_period = 60 * 12 # Twice a day + log_retention_in_days = 30 # See tf-inputs.tf for allowed values + log_retention_name = "default-log-retention" +} + module "log_retention" { source = "git::https://github.com/StateFarmIns/terraform-aws-default-log-retention.git" @@ -41,8 +69,7 @@ module "log_retention" { } } -# Multi-region -module "log_retention_west" { +module "log_retention_secondary" { source = "git::https://github.com/StateFarmIns/terraform-aws-default-log-retention.git" providers = { @@ -59,7 +86,7 @@ module "log_retention_west" { email_notification_list = local.notification_list } - iam_role_suffix = "-west" # Required to prevent IAM role names from conflicting + iam_role_suffix = "-secondary" # Required to prevent IAM role names from conflicting } ``` diff --git a/docs/Diagram.drawio.png b/docs/Diagram.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..17cc7718d973f32cb51b3d0c46408b99100187d7 GIT binary patch literal 63809 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

SAbBh7-I~s!eXO(?YtADlGZJ$5hi5}<`!Ti} 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 literal 0 HcmV?d00001 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..9e6b99c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 160 \ No newline at end of file diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..440b2a9 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +cargo lambda build --profile release-lambda --arm64 || exit 1 + +mkdir -p dist/log_retention_setter +cp target/lambda/terraform-aws-default-log-retention/bootstrap dist/log_retention_setter/ + +mkdir -p dist/global_retention_setter +cp target/lambda/global_retention_setter/bootstrap dist/global_retention_setter/ diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 0000000..5a55462 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# TODO: This script doesn't work inside State Farm; check it out on a non-corporate laptop. + +docker run --security-opt seccomp=unconfined -v "$(pwd):/volume" xd009642/tarpaulin sh -c "cargo tarpaulin --skip-clean -- --test-threads=1" \ No newline at end of file diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh new file mode 100755 index 0000000..ea11bb7 --- /dev/null +++ b/scripts/pre-commit.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Just a little helper script to run "all the things" possible before committing/pushing. Convenience. + +./scripts/test.sh || exit 1 + +cargo clippy --fix --allow-dirty --allow-staged || exit 1 +git add . + +./scripts/coverage.sh || exit 1 +git add reports/* + +./scripts/build-release.sh || exit 1 +# git add dist/* # Do not add these files; semantic release during pipeline will do so. + +echo "Coverage reports lint fixes are staged. Please commit and push." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..5cc60fb --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cargo test -- --test-threads=1 +cargo insta review \ No newline at end of file diff --git a/src/bin/global_retention_setter.rs b/src/bin/global_retention_setter.rs new file mode 100644 index 0000000..07ead7c --- /dev/null +++ b/src/bin/global_retention_setter.rs @@ -0,0 +1,603 @@ +use aws_sdk_cloudwatchlogs::model::LogGroup; +use lambda_runtime::{Error as LambdaRuntimeError, LambdaEvent}; +use log::{debug, error, info, trace}; +use serde_json::{json, Value as JsonValue}; +use terraform_aws_default_log_retention::{ + cloudwatch_logs_traits::{DescribeLogGroupsPaginated, ListTagsForResource, PutRetentionPolicy, TagResource}, + cloudwatch_metrics_traits::PutMetricData, + error::{Error, Severity}, + global::{cloudwatch_logs, cloudwatch_metrics, initialize_logger, log_group_tags, retention}, + metric_publisher::{self, Metric, MetricName}, +}; +use tokio_stream::StreamExt; + +#[derive(Debug, PartialEq, Eq)] +enum UpdateResult { + AlreadyHasRetention, + AlreadyTaggedWithRetention, + Updated, +} + +#[tokio::main] +// Ignore for code coverage +#[cfg(not(tarpaulin_include))] +async fn main() -> Result<(), LambdaRuntimeError> { + trace!("Initializing logger..."); + initialize_logger(); + + trace!("Initializing service function..."); + let func = lambda_runtime::service_fn(func); + + trace!("Getting runtime result..."); + let result = lambda_runtime::run(func).await; + + match result { + Ok(message) => { + trace!("Received OK message: {:#?}", message); + Ok(message) + } + Err(error) => { + error!("ERROR in Lambda main: {}", error); + Err(error) + } + } +} + +// Ignore for code coverage +#[cfg(not(tarpaulin_include))] +async fn func(event: LambdaEvent) -> Result { + debug!("Recevied payload: {}. Context: {:?}", event.payload, event.context); + let client = cloudwatch_logs().await; + let cloudwatch_metric_client = cloudwatch_metrics().await; + let result = process_all_log_groups(client, cloudwatch_metric_client).await; + + match result { + Ok(message) => Ok(message), + Err(error) => { + error!("ERROR in Lambda function: {}", error); + Err(error.into()) + } + } +} + +async fn process_all_log_groups( + cloudwatch_logs_client: impl DescribeLogGroupsPaginated + ListTagsForResource + PutRetentionPolicy + TagResource, + cloudwatch_metrics_client: impl PutMetricData, +) -> Result { + let mut paginator = cloudwatch_logs_client.describe_log_groups_paginated(); + + let mut errors = vec![]; + let mut total_groups = 0; + let mut updated = 0; + let mut already_has_retention = 0; + let mut already_tagged_with_retention = 0; + + while let Some(page) = paginator.next().await { + let log_groups_page = page.expect("Could not unwrap page; unexpected behavior"); + let log_groups = log_groups_page.log_groups().unwrap_or_default(); + + for log_group in log_groups { + total_groups += 1; + match process_log_group(log_group, &cloudwatch_logs_client).await { + Ok(result) => match result { + UpdateResult::AlreadyHasRetention => already_has_retention += 1, + UpdateResult::AlreadyTaggedWithRetention => already_tagged_with_retention += 1, + UpdateResult::Updated => updated += 1, + }, + Err(e) => { + error!("Failure updating retention: {}", e); + errors.push(e); + } + } + } + } + + let metrics = vec![ + Metric::new(MetricName::Total, total_groups as f64), + Metric::new(MetricName::Updated, updated as f64), + Metric::new(MetricName::AlreadyHasRetention, already_has_retention as f64), + Metric::new(MetricName::AlreadyTaggedWithRetention, already_tagged_with_retention as f64), + Metric::new(MetricName::Errored, errors.len() as f64), + ]; + metric_publisher::publish_metrics(cloudwatch_metrics_client, metrics).await; + + match errors.is_empty() { + true => { + info!( + "Success. totalGroups={}, updated={}, alreadyHasRetention={}, alreadyTaggedWithRetention={}", + total_groups, updated, already_has_retention, already_tagged_with_retention + ); + Ok( + json!({"message": "Success", "totalGroups": total_groups, "updated": updated, "alreadyHasRetention": already_has_retention, "alreadyTaggedWithRetention": already_tagged_with_retention}), + ) + } + false => { + error!("Failed to update some log group retentions: {:?}", &errors); + Err(Error { + message: format!("Failed to update some log group retentions: {:?}", &errors), + severity: Severity::Error, + }) + } + } +} + +async fn process_log_group( + log_group: &LogGroup, + client: &(impl PutRetentionPolicy + ListTagsForResource + TagResource), +) -> Result { + let log_group_arn = log_group.arn().expect("Log group ARN unexpectedly empty.").replace(":*", ""); // Some ARNs (all ARNs?) have :* on the end, but list-tags-for-resource cannot accept that part + let log_group_name = log_group.log_group_name().expect("Log group name unexpectedly empty."); + let log_group_retention = log_group.retention_in_days().unwrap_or(0); + + debug!("Working on {}", log_group_arn); + + if log_group_retention != 0 { + debug!( + "Log group {} has retention of {} days already. Not setting.", + log_group_name, log_group_retention + ); + return Ok(UpdateResult::AlreadyHasRetention); + } + + let tags = client.list_tags_for_resource(&log_group_arn).await?; + if let Some(retention) = tags.tags().and_then(|tags| tags.get("retention")) { + info!( + "Not setting retention for {} because tag `retention`=`{}` exists on it.", + log_group_name, retention + ); + return Ok(UpdateResult::AlreadyTaggedWithRetention); + } + + let new_retention = retention(); + client.put_retention_policy(log_group_name, new_retention).await?; + info!("Set retention of {} days on {}.", new_retention, log_group_name); + + if let Some(tags) = log_group_tags() { + client.tag_resource(&log_group_arn, tags).await?; + info!("Tagged {}.", log_group_arn); + } + + Ok(UpdateResult::Updated) +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_sdk_cloudwatch::{error::PutMetricDataError, model::MetricDatum, output::PutMetricDataOutput}; + use mockall::{mock, predicate}; + use std::{cell::RefCell, collections::HashMap, pin::Pin}; + use tokio_stream::Stream; + + use async_trait::async_trait; + use aws_sdk_cloudwatchlogs::{ + error::{DescribeLogGroupsError, ListTagsForResourceError, PutRetentionPolicyError, TagResourceError}, + model::LogGroup, + output::{DescribeLogGroupsOutput, ListTagsForResourceOutput, PutRetentionPolicyOutput, TagResourceOutput}, + types::SdkError, + }; + + use terraform_aws_default_log_retention::{ + cloudwatch_logs_traits::{PutRetentionPolicy, TagResource}, + cloudwatch_metrics_traits::PutMetricData, + }; + + #[ctor::ctor] + fn init() { + std::env::set_var("log_group_tags", "{}"); + } + + #[tokio::test] + async fn test_process_all_log_group_success() { + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client.expect_describe_log_groups_paginated().returning(|| { + let first_response = DescribeLogGroupsOutput::builder() + .log_groups( + LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated:*") + .retention_in_days(0) + .build(), + ) + .log_groups( + LogGroup::builder() + .log_group_name("AnotherOneWithoutRetention") + .arn("arn:aws:logs:123:us-west-2:log-group/AnotherOneWithoutRetention:*") + .retention_in_days(0) + .build(), + ) + .build(); + let second_response = DescribeLogGroupsOutput::builder() + .log_groups( + LogGroup::builder() + .log_group_name("SecondLogGroupAlreadyHasRetention") + .arn("arn:aws:logs:123:us-west-2:log-group/SecondLogGroupAlreadyHasRetention:*") + .retention_in_days(90) + .build(), + ) + .build(); + let events = vec![first_response, second_response]; + let fake_stream = FakeDescribeLogGroupsOutputStream::new(events); + + Box::pin(fake_stream) + }); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .returning(|_| Ok(ListTagsForResourceOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("MyLogGroupWasCreated"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("AnotherOneWithoutRetention"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with( + predicate::eq("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated"), + predicate::eq(HashMap::new()), + ) + .once() + .returning(|_, _| Ok(TagResourceOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with( + predicate::eq("arn:aws:logs:123:us-west-2:log-group/AnotherOneWithoutRetention"), + predicate::eq(HashMap::new()), + ) + .once() + .returning(|_, _| Ok(TagResourceOutput::builder().build())); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_all_log_group_success", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let result = process_all_log_groups(mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect("Should not fail"); + + insta::assert_display_snapshot!(result); + } + + #[tokio::test] + async fn test_process_all_log_group_single_already_tagged_with_retention() { + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client.expect_describe_log_groups_paginated().returning(|| { + let first_response = DescribeLogGroupsOutput::builder() + .log_groups( + LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/NoRetentionAndGetTagsCallFails:*") + .retention_in_days(0) + .build(), + ) + .build(); + let events = vec![first_response]; + let fake_stream = FakeDescribeLogGroupsOutputStream::new(events); + + Box::pin(fake_stream) + }); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .returning(|_| Ok(ListTagsForResourceOutput::builder().tags("retention", "DoNotTouch").build())); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_all_log_group_single_already_tagged_with_retention", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let result = process_all_log_groups(mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect("Should not fail"); + + insta::assert_display_snapshot!(result); + } + + #[tokio::test] + async fn test_process_all_log_group_partial_success() { + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client.expect_describe_log_groups_paginated().returning(|| { + let first_response = DescribeLogGroupsOutput::builder() + .log_groups( + LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated:*") + .retention_in_days(0) + .build(), + ) + .log_groups( + LogGroup::builder() + .log_group_name("AnotherOneWithoutRetention") + .arn("arn:aws:logs:123:us-west-2:log-group/AnotherOneWithoutRetention:*") + .retention_in_days(0) + .build(), + ) + .log_groups( + LogGroup::builder() + .log_group_name("NoRetentionAndGetTagsCallFails") + .arn("arn:aws:logs:123:us-west-2:log-group/NoRetentionAndGetTagsCallFails:*") + .retention_in_days(0) + .build(), + ) + .build(); + let second_response = DescribeLogGroupsOutput::builder() + .log_groups( + LogGroup::builder() + .log_group_name("SecondLogGroupAlreadyHasRetention") + .arn("arn:aws:logs:123:us-west-2:log-group/SecondLogGroupAlreadyHasRetention:*") + .retention_in_days(90) + .build(), + ) + .build(); + let events = vec![first_response, second_response]; + let fake_stream = FakeDescribeLogGroupsOutputStream::new(events); + + Box::pin(fake_stream) + }); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq("arn:aws:logs:123:us-west-2:log-group/NoRetentionAndGetTagsCallFails")) + .returning(|_| { + // This type of error would never happen because it is "my" error type rather than an AWS error type. Luckily it doesn't matter -- we only care that an error happened. + Err(SdkError::timeout_error(Box::new(Error { + message: "Some error happened when getting tags".to_string(), + severity: Severity::Error, + }))) + }); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::ne("arn:aws:logs:123:us-west-2:log-group/NoRetentionAndGetTagsCallFails")) + .returning(|_| Ok(ListTagsForResourceOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("MyLogGroupWasCreated"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("AnotherOneWithoutRetention"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with( + predicate::eq("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated"), + predicate::eq(HashMap::new()), + ) + .once() + .returning(|_, _| Ok(TagResourceOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with( + predicate::eq("arn:aws:logs:123:us-west-2:log-group/AnotherOneWithoutRetention"), + predicate::eq(HashMap::new()), + ) + .once() + .returning(|_,_| + // This type of error would never happen because it is "my" error type rather than an AWS error type. Luckily it doesn't matter -- we only care that an error happened. + Err(SdkError::timeout_error(Box::new(Error { + message: "Some error happened".to_string(), + severity: Severity::Error, + })))); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_all_log_group_partial_success", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let result = process_all_log_groups(mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect_err("Should fail"); + + insta::assert_display_snapshot!(result); + } + + #[tokio::test] + async fn test_process_log_group_success() { + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + let log_group_arn = "arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated"; + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq(log_group_arn)) + .once() + .returning(|_| Ok(ListTagsForResourceOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("MyLogGroupWasCreated"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with(predicate::eq(log_group_arn), predicate::eq(HashMap::new())) + .once() + .returning(|_, _| Ok(TagResourceOutput::builder().build())); + + let log_group = LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated:*") + .retention_in_days(0) + .build(); + + let result = process_log_group(&log_group, &mock_cloud_watch_logs_client).await.expect("Should not fail"); + + assert_eq!(UpdateResult::Updated, result); + } + + #[tokio::test] + async fn test_process_log_group_retention_already_set() { + let mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + + let log_group = LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated:*") + .retention_in_days(30) + .build(); + + let result = process_log_group(&log_group, &mock_cloud_watch_logs_client).await.expect("Should not fail"); + + assert_eq!(UpdateResult::AlreadyHasRetention, result); + } + + #[tokio::test] + async fn test_process_log_group_no_retention_but_tag_present() { + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + + mock_cloud_watch_logs_client.expect_list_tags_for_resource().once().returning(|_| { + Ok(ListTagsForResourceOutput::builder() + .tags("retention", "I know what I'm doing and I've tagged this group. Leave me alone!") + .build()) + }); + + let log_group = LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/MyLogGroupWasCreated:*") + .retention_in_days(0) + .build(); + + let result = process_log_group(&log_group, &mock_cloud_watch_logs_client).await.expect("Should not fail"); + + assert_eq!(UpdateResult::AlreadyTaggedWithRetention, result); + } + + #[tokio::test] + async fn test_process_log_group_fails() { + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq("arn:aws:logs:123:us-west-2:log-group/NoRetentionAndGetTagsCallFails")) + .once() + .returning(|_| { + // This type of error would never happen because it is "my" error type rather than an AWS error type. Luckily it doesn't matter -- we only care that an error happened. + Err(SdkError::timeout_error(Box::new(Error { + message: "Some error happened".to_string(), + severity: Severity::Error, + }))) + }); + + let log_group = LogGroup::builder() + .log_group_name("MyLogGroupWasCreated") + .arn("arn:aws:logs:123:us-west-2:log-group/NoRetentionAndGetTagsCallFails:*") + .retention_in_days(0) + .build(); + + let result = process_log_group(&log_group, &mock_cloud_watch_logs_client).await.expect_err("Should fail"); + + insta::assert_debug_snapshot!(result); + } + + #[derive(Clone)] + struct FakeDescribeLogGroupsOutputStream { + results: RefCell>, + } + + impl FakeDescribeLogGroupsOutputStream { + fn new(results: Vec) -> Self { + Self { + results: RefCell::new(results), + } + } + } + + impl Stream for FakeDescribeLogGroupsOutputStream { + type Item = Result>; + + fn poll_next(self: Pin<&mut Self>, _: &mut std::task::Context<'_>) -> std::task::Poll> { + let mut results = self.results.borrow_mut(); + match results.is_empty() { + true => std::task::Poll::Ready(None), + false => std::task::Poll::Ready(Some(Ok(results.pop().unwrap()))), + } + } + } + + // Required to mock multiple traits at a time + // See https://docs.rs/mockall/latest/mockall/#multiple-and-inherited-traits + mock! { + // Creates MockCloudWatchLogs for use in tests + // Add more trait impls below if needed in tests + pub CloudWatchLogs {} + + #[async_trait] + impl DescribeLogGroupsPaginated for CloudWatchLogs { + fn describe_log_groups_paginated( + &self, + ) -> Pin< + Box>>>>; + } + + #[async_trait] + impl PutRetentionPolicy for CloudWatchLogs { + async fn put_retention_policy( + &self, + log_group_name: &str, + retention_in_days: i32, + ) -> Result>; + } + + #[async_trait] + impl TagResource for CloudWatchLogs { + async fn tag_resource( + &self, + log_group_arn: &str, + tags: HashMap, + ) -> Result>; + } + + #[async_trait] + impl ListTagsForResource for CloudWatchLogs { + async fn list_tags_for_resource( + &self, + resource_arn: &str, + ) -> Result>; + } + } + + mock! { + pub CloudWatchMetrics {} + + #[async_trait] + impl PutMetricData for CloudWatchMetrics { + async fn put_metric_data( + &self, + namespace: String, + metric_data: Vec, + ) -> Result>; + } + } +} diff --git a/src/bin/snapshots/global_retention_setter__tests__CWMetricCall.snap b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall.snap new file mode 100644 index 0000000..b12fde6 --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall.snap @@ -0,0 +1,81 @@ +--- +source: src/bin/global_retention_setter.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 3.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 2.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyHasRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyTaggedWithRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Errored", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_partial_success.snap b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_partial_success.snap new file mode 100644 index 0000000..be38859 --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_partial_success.snap @@ -0,0 +1,81 @@ +--- +source: src/bin/global_retention_setter.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 4.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyHasRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyTaggedWithRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Errored", + ), + dimensions: None, + timestamp: None, + value: Some( + 2.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_single_already_tagged_with_retention.snap b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_single_already_tagged_with_retention.snap new file mode 100644 index 0000000..e3574ff --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_single_already_tagged_with_retention.snap @@ -0,0 +1,81 @@ +--- +source: src/bin/global_retention_setter.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyHasRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyTaggedWithRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Errored", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_success.snap b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_success.snap new file mode 100644 index 0000000..b12fde6 --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__CWMetricCall_process_all_log_group_success.snap @@ -0,0 +1,81 @@ +--- +source: src/bin/global_retention_setter.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 3.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 2.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyHasRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "AlreadyTaggedWithRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Errored", + ), + dimensions: None, + timestamp: None, + value: Some( + 0.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_partial_success.snap b/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_partial_success.snap new file mode 100644 index 0000000..3236d3c --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_partial_success.snap @@ -0,0 +1,5 @@ +--- +source: src/bin/global_retention_setter.rs +expression: result +--- +Error occurred. Message: Failed to update some log group retentions: [TimeoutError(TimeoutError { source: Error { message: "Some error happened", severity: Error } }), TimeoutError(TimeoutError { source: Error { message: "Some error happened when getting tags", severity: Error } })]. Severity: Error diff --git a/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_single_already_tagged_with_retention.snap b/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_single_already_tagged_with_retention.snap new file mode 100644 index 0000000..7b21413 --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_single_already_tagged_with_retention.snap @@ -0,0 +1,5 @@ +--- +source: src/bin/global_retention_setter.rs +expression: result +--- +{"alreadyHasRetention":0,"alreadyTaggedWithRetention":1,"message":"Success","totalGroups":1,"updated":0} diff --git a/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_success.snap b/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_success.snap new file mode 100644 index 0000000..021b3fd --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__process_all_log_group_success.snap @@ -0,0 +1,5 @@ +--- +source: src/bin/global_retention_setter.rs +expression: result +--- +{"alreadyHasRetention":1,"alreadyTaggedWithRetention":0,"message":"Success","totalGroups":3,"updated":2} diff --git a/src/bin/snapshots/global_retention_setter__tests__process_log_group_fails.snap b/src/bin/snapshots/global_retention_setter__tests__process_log_group_fails.snap new file mode 100644 index 0000000..7aa5125 --- /dev/null +++ b/src/bin/snapshots/global_retention_setter__tests__process_log_group_fails.snap @@ -0,0 +1,12 @@ +--- +source: src/bin/global_retention_setter.rs +expression: result +--- +TimeoutError( + TimeoutError { + source: Error { + message: "Some error happened", + severity: Error, + }, + }, +) diff --git a/src/cloudwatch_logs_traits.rs b/src/cloudwatch_logs_traits.rs new file mode 100644 index 0000000..79a7fde --- /dev/null +++ b/src/cloudwatch_logs_traits.rs @@ -0,0 +1,127 @@ +// Traits defined for testing purposes -- see https://docs.aws.amazon.com/sdk-for-rust/latest/dg/testing.html + +/* + +This file contains a wrapper struct `CloudWatchLogs` which wraps the default CloudWatch Logs Client. +Traits are defined in the middle of the file to provide generic interfaces to each CW Logs operation. +At the bottom, default implementations are provided to invoke the main client. + +For testing, you can automock each of these traits; much easier than creating a fake AWS API HTTP server! + +Adding a new operation: +1. First add a new trait. Add the `#[cfg_attr(test, automock)]` annotation before it, which MUST appear before the `#[async_trait]` annotation (per automock documentation) +2. Add a default implementation in the bottom section that invokes the AWS CW Logs client. +3. Test examples are in `retention_setter.rs` + +*/ + +use std::{collections::HashMap, pin::Pin}; + +use async_trait::async_trait; +use aws_sdk_cloudwatchlogs::{ + error::{DescribeLogGroupsError, ListTagsForResourceError, PutRetentionPolicyError, TagResourceError}, + output::{DescribeLogGroupsOutput, ListTagsForResourceOutput, PutRetentionPolicyOutput, TagResourceOutput}, + types::SdkError, + Client as CloudWatchLogsClient, +}; +#[cfg(test)] +use mockall::automock; +use tokio_stream::Stream; + +/* Base Struct */ + +#[derive(Clone, Debug)] +pub struct CloudWatchLogs { + client: CloudWatchLogsClient, +} + +impl CloudWatchLogs { + pub fn new(client: CloudWatchLogsClient) -> Self { + Self { client } + } +} + +/* End Base Struct */ + +/* Traits */ + +#[cfg_attr(test, automock)] +#[async_trait] +pub trait DescribeLogGroups { + async fn describe_log_groups(&self, log_group_name_prefix: &str) -> Result>; +} + +#[async_trait] +pub trait DescribeLogGroupsPaginated { + fn describe_log_groups_paginated(&self) -> Pin>>>>; +} + +#[async_trait] +pub trait ListTagsForResource { + async fn list_tags_for_resource(&self, resource_arn: &str) -> Result>; +} + +#[async_trait] +pub trait PutRetentionPolicy { + async fn put_retention_policy(&self, log_group_name: &str, retention_in_days: i32) -> Result>; +} + +#[async_trait] +pub trait TagResource { + // Add retention tag to a log group + async fn tag_resource(&self, log_group_arn: &str, tags: HashMap) -> Result>; +} + +/* End Traits */ + +/* Implementations */ + +#[async_trait] +impl DescribeLogGroups for CloudWatchLogs { + async fn describe_log_groups(&self, log_group_name_prefix: &str) -> Result> { + Ok(self.client.describe_log_groups().log_group_name_prefix(log_group_name_prefix).send().await?) + } +} + +#[async_trait] +impl DescribeLogGroupsPaginated for CloudWatchLogs { + fn describe_log_groups_paginated(&self) -> Pin>>>> { + Box::pin(self.client.describe_log_groups().into_paginator().send()) + } +} + +#[async_trait] +impl ListTagsForResource for CloudWatchLogs { + async fn list_tags_for_resource(&self, resource_arn: &str) -> Result> { + Ok(self.client.list_tags_for_resource().resource_arn(resource_arn).send().await?) + } +} + +#[async_trait] +impl PutRetentionPolicy for CloudWatchLogs { + async fn put_retention_policy(&self, log_group_name: &str, retention_in_days: i32) -> Result> { + Ok(self + .client + .put_retention_policy() + .log_group_name(log_group_name) + .retention_in_days(retention_in_days) + .send() + .await?) + } +} + +#[async_trait] +impl TagResource for CloudWatchLogs { + async fn tag_resource(&self, log_group_arn: &str, tags: HashMap) -> Result> { + Ok(self + .client + .tag_resource() + .resource_arn(log_group_arn) + .set_tags(Some(tags)) + .tags("retention", "Set by AWS Default Log Retention project.") + .send() + .await?) + } +} + +/* End Implementations */ diff --git a/src/cloudwatch_metrics_traits.rs b/src/cloudwatch_metrics_traits.rs new file mode 100644 index 0000000..06a9834 --- /dev/null +++ b/src/cloudwatch_metrics_traits.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use aws_sdk_cloudwatch::{error::PutMetricDataError, model::MetricDatum, output::PutMetricDataOutput, types::SdkError, Client as CloudWatchMetricsClient}; + +#[cfg(test)] +use mockall::automock; + +/* Base Struct */ + +#[derive(Clone, Debug)] +pub struct CloudWatchMetrics { + client: CloudWatchMetricsClient, +} + +impl CloudWatchMetrics { + pub fn new(client: CloudWatchMetricsClient) -> Self { + Self { client } + } +} + +/* End Base Struct */ + +/* Traits */ + +#[cfg_attr(test, automock)] +#[async_trait] +pub trait PutMetricData { + async fn put_metric_data(&self, namespace: String, metric_data: Vec) -> Result>; +} + +/* End Traits */ + +/* Implementations */ + +#[async_trait] +impl PutMetricData for CloudWatchMetrics { + async fn put_metric_data(&self, namespace: String, metric_data: Vec) -> Result> { + Ok(self + .client + .put_metric_data() + .set_metric_data(Some(metric_data)) + .namespace(namespace) + .send() + .await?) + } +} + +/* End Implementations */ diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..73227fa --- /dev/null +++ b/src/error.rs @@ -0,0 +1,33 @@ +use std::fmt::Display; + +use aws_smithy_client::SdkError; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Error { + pub message: String, + pub severity: Severity, +} + +#[derive(Debug, Serialize, PartialEq, Eq)] +pub enum Severity { + Warning, + Error, +} + +impl std::error::Error for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("Error occurred. Message: {}. Severity: {:#?}", &self.message, &self.severity)) + } +} + +impl From> for Error { + fn from(e: SdkError) -> Self { + Self { + message: e.to_string(), + severity: Severity::Error, + } + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..2478d46 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct CloudTrailEvent { + pub detail: CloudTrailEventDetail, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CloudTrailEventDetail { + pub aws_region: String, + pub user_identity: CloudTrailEventUserIdentity, + pub request_parameters: CloudTrailEventRequestParameters, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CloudTrailEventRequestParameters { + pub log_group_name: String, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CloudTrailEventUserIdentity { + pub account_id: String, +} + +impl CloudTrailEvent { + pub fn new(account_id: impl Into, aws_region: impl Into, log_group_name: impl Into) -> Self { + Self { + detail: CloudTrailEventDetail { + request_parameters: CloudTrailEventRequestParameters { + log_group_name: log_group_name.into(), + }, + aws_region: aws_region.into(), + user_identity: CloudTrailEventUserIdentity { account_id: account_id.into() }, + }, + } + } +} diff --git a/src/global.rs b/src/global.rs new file mode 100644 index 0000000..2a5bc19 --- /dev/null +++ b/src/global.rs @@ -0,0 +1,144 @@ +use std::{collections::HashMap, time::Duration}; + +use aws_config::SdkConfig; +use aws_sdk_cloudwatch::Client as CloudWatchMetricsClient; +use aws_sdk_cloudwatchlogs::Client as CloudWatchLogsClient; +use aws_smithy_types::retry::{RetryConfig, RetryMode}; +use cached::proc_macro::cached; + +use crate::{cloudwatch_logs_traits::CloudWatchLogs, cloudwatch_metrics_traits::CloudWatchMetrics}; + +#[cached] +pub async fn cloudwatch_logs() -> CloudWatchLogs { + let sdk_config = sdk_config().await; + CloudWatchLogs::new(CloudWatchLogsClient::new(&sdk_config)) +} + +#[cached] +pub async fn cloudwatch_metrics() -> CloudWatchMetrics { + let sdk_config = sdk_config().await; + CloudWatchMetrics::new(CloudWatchMetricsClient::new(&sdk_config)) +} + +#[cached] +async fn sdk_config() -> SdkConfig { + let retry_config = RetryConfig::standard() + .with_initial_backoff(Duration::from_millis(500)) + .with_max_attempts(10) + .with_retry_mode(RetryMode::Adaptive); + + aws_config::from_env().retry_config(retry_config).load().await +} + +#[cfg_attr(not(test), cached)] // Disables caching for tests https://github.com/jaemk/cached/issues/130 +pub fn retention() -> i32 { + std::env::var("log_retention_in_days") + .unwrap_or_else(|_| "30".to_string()) + .parse() + .unwrap_or(30) +} + +#[cfg_attr(not(test), cached)] // Disables caching for tests https://github.com/jaemk/cached/issues/130 +pub fn log_group_tags() -> Option> { + let log_group_tags = std::env::var("log_group_tags").ok()?; + let log_group_tags = serde_json::from_str(&log_group_tags).ok()?; + Some(log_group_tags) +} + +#[cached] +pub fn metric_namespace() -> String { + std::env::var("metric_namespace").unwrap_or_else(|_| "LogRotation".to_string()) +} + +pub fn initialize_logger() { + env_logger::builder().format_timestamp(None).init(); +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::global::retention; + + use super::{cloudwatch_logs, cloudwatch_metrics, initialize_logger, log_group_tags}; + + #[tokio::test] + async fn test_cw_logs_client() { + std::env::set_var("AWS_ACCESS_KEY_ID", "AKIAASDASDQWEF"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "ASIAAFQWEFWEIFJ"); + + let client = cloudwatch_logs().await; + // Remove "base" value from the snapshot because it is a dynamic value + insta::with_settings!( + {filters => vec![(r"base: .*\n\s*", "")]}, + {insta::assert_debug_snapshot!(client)} + ) + } + + #[tokio::test] + async fn test_cw_metrics_client() { + std::env::set_var("AWS_ACCESS_KEY_ID", "AKIAASDASDQWEF"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "ASIAAFQWEFWEIFJ"); + + let client = cloudwatch_metrics().await; + // Remove "base" value from the snapshot because it is a dynamic value + insta::with_settings!( + {filters => vec![(r"base: .*\n\s*", "")]}, + {insta::assert_debug_snapshot!(client)} + ) + } + + #[test] + fn test_retention() { + std::env::set_var("log_retention_in_days", "1"); + assert_eq!(1, retention()); + } + + #[test] + fn test_retention_invalid() { + std::env::set_var("log_retention_in_days", "asdasdasd"); + assert_eq!(30, retention()); + } + + #[test] + fn test_retention_not_set() { + std::env::remove_var("log_retention_in_days"); + assert_eq!(30, retention()); + } + + #[test] + fn test_log_group_tags_none() { + std::env::remove_var("log_group_tags"); + assert_eq!(log_group_tags(), None); + } + + #[test] + fn test_log_group_tags_valid() { + let tags_json = "{\"a\": \"b\", \"c\": \"d\"}"; + std::env::set_var("log_group_tags", tags_json); + + let mut expected = HashMap::new(); + expected.insert("a".to_string(), "b".to_string()); + expected.insert("c".to_string(), "d".to_string()); + + assert_eq!(log_group_tags(), Some(expected)); + } + + #[test] + fn test_log_group_tags_invalid_none() { + std::env::set_var("log_group_tags", "true"); + assert_eq!(log_group_tags(), None); + } + + #[test] + fn test_log_group_tags_empty() { + std::env::set_var("log_group_tags", "{}"); + assert_eq!(log_group_tags(), Some(HashMap::new())); + } + + #[test] + fn test_initialize_logger() { + // Not much to test here...... + initialize_logger(); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..95baee0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +pub mod cloudwatch_logs_traits; +pub mod cloudwatch_metrics_traits; +pub mod error; +pub mod event; +pub mod global; +pub mod metric_publisher; +pub mod retention_setter; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6f70173 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,505 @@ +use lambda_runtime::{Context, Error as LambdaRuntimeError, LambdaEvent}; + +use log::{debug, error, info, trace, warn}; +use serde_json::{json, Value as JsonValue}; +use terraform_aws_default_log_retention::{ + cloudwatch_logs_traits::{DescribeLogGroups, ListTagsForResource, PutRetentionPolicy, TagResource}, + cloudwatch_metrics_traits::PutMetricData, + error::{Error, Severity}, + event::CloudTrailEvent, + global::{cloudwatch_logs, cloudwatch_metrics, initialize_logger, log_group_tags, retention}, + metric_publisher::{self, Metric, MetricName}, + retention_setter::get_existing_retention, +}; + +// TODO: Main and func are identical for main.rs and global_retention_setter.rs. How to genericize? +#[tokio::main] +// Ignore for code coverage +#[cfg(not(tarpaulin_include))] +async fn main() -> Result<(), LambdaRuntimeError> { + trace!("Initializing logger..."); + initialize_logger(); + + trace!("Initializing service function..."); + let func = lambda_runtime::service_fn(func); + + trace!("Getting runtime result..."); + let result = lambda_runtime::run(func).await; + + match result { + Ok(message) => { + trace!("Received OK message: {:#?}", message); + Ok(message) + } + Err(error) => { + error!("ERROR in Lambda main: {}", error); + Err(error) + } + } +} + +// Ignore for code coverage +#[cfg(not(tarpaulin_include))] +async fn func(event: LambdaEvent) -> Result { + debug!("Received payload: {}. Context: {:?}", event.payload, event.context); + let cloudwatch_logs = cloudwatch_logs().await; + let cloudwatch_metrics = cloudwatch_metrics().await; + let cloud_trail_event = parse_event(event.payload, Some(event.context)); + if let Err(error) = cloud_trail_event { + return process_error(error); + } + let cloud_trail_event = cloud_trail_event.expect("Should be Ok() based on above code."); + + let result = process_event(cloud_trail_event, cloudwatch_logs, cloudwatch_metrics).await; + + match result { + Ok(message) => Ok(message), + Err(error) => process_error(error), + } +} + +/// Returns Ok if error is just a warning +fn process_error(error: Error) -> Result { + match error.severity { + Severity::Warning => { + warn!("WARN in Lambda function: {}", error); + Ok(json!(error)) + } + Severity::Error => { + error!("ERROR in Lambda function: {}", error); + Err(error.into()) + } + } +} + +async fn process_event( + event: CloudTrailEvent, + cloudwatch_logs: impl DescribeLogGroups + ListTagsForResource + PutRetentionPolicy + TagResource, + cloudwatch_metrics: impl PutMetricData, +) -> Result { + let log_group_name = event.detail.request_parameters.log_group_name; + + let existing_retention = get_existing_retention(&log_group_name, &cloudwatch_logs).await?; + + if existing_retention != 0 { + info!( + "Not setting retention for {} because it is set to {} days already.", + log_group_name, existing_retention + ); + metric_publisher::publish_metric(cloudwatch_metrics, Metric::new(MetricName::AlreadyHasRetention, 1.0)).await; + return Ok(json!({ + "message": + format!( + "Not setting retention for {} because it is set to {} days already.", + log_group_name, existing_retention + ) + })); + } + + let log_group_arn = format!( + "arn:aws:logs:{}:{}:log-group:{}", + event.detail.aws_region, event.detail.user_identity.account_id, log_group_name + ); + let tags = cloudwatch_logs.list_tags_for_resource(&log_group_arn).await?; + if let Some(retention) = tags.tags().and_then(|tags| tags.get("retention")) { + info!( + "Not setting retention for {} because tag `retention`=`{}` exists on it.", + log_group_name, retention + ); + metric_publisher::publish_metric(cloudwatch_metrics, Metric::new(MetricName::AlreadyTaggedWithRetention, 1.0)).await; + return Ok(json!({ + "message": + format!( + "Not setting retention for {} because tag `retention`=`{}` exists on it.", + log_group_name, retention + ) + })); + } + + cloudwatch_logs.put_retention_policy(&log_group_name, retention()).await?; + + metric_publisher::publish_metric(cloudwatch_metrics, Metric::new(MetricName::Updated, 1.0)).await; + + if let Some(tags) = log_group_tags() { + cloudwatch_logs.tag_resource(&log_group_arn, tags).await?; + } + + info!("Retention set successfully for {}", log_group_name); + Ok(json!({"message": "Retention set successfully"})) +} + +/// Parses a JsonValue into a CloudTrailEvent +/// Normally we could allow our func to parse the event for us, but it doesn't handle errors gracefully or with enough information. +/// +/// # Arguments +/// +/// * `payload` the original payload given by Lambda runtime +/// * `context` Optionally, provide the Context object given by the Lambda runtime. It isn't needed for execution; only to enhance the returned error if the payload fails to parse +fn parse_event(payload: JsonValue, context: Option) -> Result { + // Must clone payload so we can optionally use it in the error message + let cloud_trail_event = serde_json::from_value(payload.clone()); + if let Err(error) = cloud_trail_event { + // Known instances are: + // * When someone tried to make a group but they don't have access + return Err(Error { + severity: Severity::Warning, + message: format!( + "Error deserializing input payload. Payload: `{}`. Context: `{:?}`. Error: `{}`.", + payload, context, error + ), + }); + } + let cloud_trail_event = cloud_trail_event.expect("Cannot be Err based on code above"); + + Ok(cloud_trail_event) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use async_trait::async_trait; + use aws_sdk_cloudwatch::{error::PutMetricDataError, model::MetricDatum, output::PutMetricDataOutput}; + use aws_sdk_cloudwatchlogs::{ + error::{DescribeLogGroupsError, ListTagsForResourceError, PutRetentionPolicyError, TagResourceError}, + model::LogGroup, + output::{DescribeLogGroupsOutput, ListTagsForResourceOutput, PutRetentionPolicyOutput, TagResourceOutput}, + types::SdkError, + }; + use lambda_runtime::{Context, LambdaEvent}; + use mockall::{mock, predicate}; + use serde_json::json; + use terraform_aws_default_log_retention::{ + cloudwatch_logs_traits::{DescribeLogGroups, ListTagsForResource, PutRetentionPolicy, TagResource}, + error::{Error, Severity}, + }; + use terraform_aws_default_log_retention::{cloudwatch_metrics_traits::PutMetricData, event::CloudTrailEvent}; + + use crate::{func, parse_event, process_error, process_event}; + + #[ctor::ctor] + fn init() { + std::env::set_var("log_group_tags", "{}"); + } + + #[tokio::test] + async fn test_process_event_success_no_tags() { + let event = CloudTrailEvent::new("123456789", "us-east-1", "MyLogGroupWasCreated"); + let log_group_arn = "arn:aws:logs:us-east-1:123456789:log-group:MyLogGroupWasCreated"; + + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client + .expect_describe_log_groups() + .with(predicate::eq("MyLogGroupWasCreated")) + .once() + .returning(|_| mock_describe_log_groups_response("MyLogGroupWasCreated", 0)); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq(log_group_arn)) + .once() + .returning(|_| mock_list_tags_for_resource_response(None)); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("MyLogGroupWasCreated"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with(predicate::eq(log_group_arn), predicate::eq(HashMap::new())) + .once() + .returning(|_, _| Ok(TagResourceOutput::builder().build())); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_event_success_no_tags", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let result = process_event(event, mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect("Should not fail"); + + insta::assert_debug_snapshot!(result); + } + + #[tokio::test] + async fn test_process_event_fails_when_put_retention_policy_fails() { + let event = CloudTrailEvent::new("123456789", "us-east-1", "MyLogGroupWasCreated"); + + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client + .expect_describe_log_groups() + .with(predicate::eq("MyLogGroupWasCreated")) + .once() + .returning(|_| mock_describe_log_groups_response("MyLogGroupWasCreated", 0)); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq("arn:aws:logs:us-east-1:123456789:log-group:MyLogGroupWasCreated")) + .once() + .returning(|_| mock_list_tags_for_resource_response(None)); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("MyLogGroupWasCreated"), predicate::eq(30)) + .once() + .returning(|_, _| { + // This type of error would never happen because it is "my" error type rather than an AWS error type. Luckily it doesn't matter -- we only care that an error happened. + Err(SdkError::timeout_error(Box::new(Error { + message: "Some error happened".to_string(), + severity: Severity::Error, + }))) + }); + + let error = process_event(event, mock_cloud_watch_logs_client, MockCloudWatchMetrics::new()) + .await + .expect_err("Should fail"); + + insta::assert_debug_snapshot!(error); + } + + #[tokio::test] + async fn test_process_event_fails_when_tag_log_group_fails() { + let event = CloudTrailEvent::new("123456789", "us-east-1", "MyLogGroupWasCreated"); + + let log_group_arn = "arn:aws:logs:us-east-1:123456789:log-group:MyLogGroupWasCreated"; + + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client + .expect_describe_log_groups() + .with(predicate::eq("MyLogGroupWasCreated")) + .once() + .returning(|_| mock_describe_log_groups_response("MyLogGroupWasCreated", 0)); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq(log_group_arn)) + .once() + .returning(|_| mock_list_tags_for_resource_response(None)); + + mock_cloud_watch_logs_client + .expect_put_retention_policy() + .with(predicate::eq("MyLogGroupWasCreated"), predicate::eq(30)) + .once() + .returning(|_, _| Ok(PutRetentionPolicyOutput::builder().build())); + + mock_cloud_watch_logs_client + .expect_tag_resource() + .with(predicate::eq(log_group_arn), predicate::eq(HashMap::new())) + .once() + .returning(|_, _| { + // This type of error would never happen because it is "my" error type rather than an AWS error type. Luckily it doesn't matter -- we only care that an error happened. + Err(SdkError::timeout_error(Box::new(Error { + message: "Some error happened".to_string(), + severity: Severity::Error, + }))) + }); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_event_fails_when_tag_log_group_fails", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let error = process_event(event, mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect_err("Should fail"); + + insta::assert_debug_snapshot!(error); + } + + #[tokio::test] + async fn test_process_event_retention_already_set() { + let event = CloudTrailEvent::new("123456789", "us-east-1", "MyLogGroupWasCreated"); + + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client + .expect_describe_log_groups() + .with(predicate::eq("MyLogGroupWasCreated")) + .once() + .returning(|_| mock_describe_log_groups_response("MyLogGroupWasCreated", 30)); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_event_retention_already_set", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let result = process_event(event, mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect("Should not fail"); + + insta::assert_debug_snapshot!(result); + } + + #[tokio::test] + async fn test_process_event_do_not_overwrite_when_retention_tag_set() { + let event = CloudTrailEvent::new("123456789", "us-east-1", "MyLogGroupWasCreated"); + + let mut mock_cloud_watch_logs_client = MockCloudWatchLogs::new(); + mock_cloud_watch_logs_client + .expect_describe_log_groups() + .with(predicate::eq("MyLogGroupWasCreated")) + .once() + .returning(|_| mock_describe_log_groups_response("MyLogGroupWasCreated", 0)); + + mock_cloud_watch_logs_client + .expect_list_tags_for_resource() + .with(predicate::eq("arn:aws:logs:us-east-1:123456789:log-group:MyLogGroupWasCreated")) + .once() + .returning(|_| mock_list_tags_for_resource_response(Some("Do not override please"))); + + let mut mock_cloud_watch_metrics_client = MockCloudWatchMetrics::new(); + mock_cloud_watch_metrics_client + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_process_event_do_not_overwrite_when_retention_tag_set", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let result = process_event(event, mock_cloud_watch_logs_client, mock_cloud_watch_metrics_client) + .await + .expect("Should not fail"); + + insta::assert_debug_snapshot!(result); + } + + #[test] + fn test_parse_event_success() { + let expected = CloudTrailEvent::new("123", "us-east-77", "SomeLogGroup"); + let input = json!(expected); + + assert_eq!(expected, parse_event(input, None).expect("Should succeed")); + } + + #[test] + fn test_parse_event_fail() { + let input = json!({"invalid": "input"}); + let mut context = Context::default(); + context.request_id = "1231231233123123123".to_string(); + context.invoked_function_arn = "arn:aws:whatever:my-awesome-stuff".to_string(); + + let result = parse_event(input, Some(context)).expect_err("Should be an error deserializing the structure"); + + insta::assert_debug_snapshot!(result); + } + + #[test] + fn test_process_error_severity_error() { + process_error(Error { + severity: Severity::Error, + message: "".to_string(), + }) + .expect_err("Should be an error"); + } + + #[test] + fn test_process_error_severity_warning() { + process_error(Error { + severity: Severity::Warning, + message: "".to_string(), + }) + .expect("Should be successful"); + } + + #[tokio::test] + async fn test_process_event_bad_input() { + let input = json!({"invalid": "input"}); + let event = LambdaEvent::new(input, Context::default()); + let result = func(event).await.expect("Should be OK with error message (warning)."); + + insta::assert_debug_snapshot!(result); + } + + // Required to mock multiple traits at a time + // See https://docs.rs/mockall/latest/mockall/#multiple-and-inherited-traits + mock! { + // Creates MockCloudWatchLogs for use in tests + // Add more trait impls below if needed in tests + pub CloudWatchLogs {} + + #[async_trait] + impl DescribeLogGroups for CloudWatchLogs { + async fn describe_log_groups( + &self, + log_group_name_prefix: &str, + ) -> Result>; + } + + #[async_trait] + impl ListTagsForResource for CloudWatchLogs { + async fn list_tags_for_resource( + &self, + resource_arn: &str, + ) -> Result>; + } + + #[async_trait] + impl PutRetentionPolicy for CloudWatchLogs { + async fn put_retention_policy( + &self, + log_group_name: &str, + retention_in_days: i32, + ) -> Result>; + } + + #[async_trait] + impl TagResource for CloudWatchLogs { + async fn tag_resource( + &self, + log_group_arn: &str, + tags: HashMap + ) -> Result>; + } + } + + mock! { + pub CloudWatchMetrics {} + + #[async_trait] + impl PutMetricData for CloudWatchMetrics { + async fn put_metric_data( + &self, + namespace: String, + metric_data: Vec, + ) -> Result>; + } + } + + fn mock_describe_log_groups_response(log_group_name: &str, retention: i32) -> Result> { + let log_group = LogGroup::builder().log_group_name(log_group_name).retention_in_days(retention).build(); + let response = DescribeLogGroupsOutput::builder().log_groups(log_group).build(); + Ok(response) + } + + fn mock_list_tags_for_resource_response(retention_tag_value: Option<&str>) -> Result> { + if let Some(retention_tag_value) = retention_tag_value { + let mut tags: HashMap = HashMap::new(); + tags.insert("retention".to_string(), retention_tag_value.to_string()); + Ok(ListTagsForResourceOutput::builder().set_tags(Some(tags)).build()) + } else { + Ok(ListTagsForResourceOutput::builder().build()) + } + } +} diff --git a/src/metric_publisher.rs b/src/metric_publisher.rs new file mode 100644 index 0000000..536f16b --- /dev/null +++ b/src/metric_publisher.rs @@ -0,0 +1,130 @@ +use aws_sdk_cloudwatch::model::MetricDatum; +use log::warn; + +use crate::{cloudwatch_metrics_traits::PutMetricData, global::metric_namespace}; + +// Enumerates the titles of metric names +// to ensure consistency between Lambdas +#[derive(Debug, Clone)] +pub enum MetricName { + Total, + Updated, + AlreadyHasRetention, + AlreadyTaggedWithRetention, + Errored, +} + +#[derive(Debug, Clone)] +pub struct Metric { + pub name: MetricName, + pub value: f64, +} + +impl From for MetricDatum { + fn from(metric: Metric) -> Self { + MetricDatum::builder().metric_name(metric.name.to_string()).value(metric.value).build() + } +} + +impl Metric { + pub fn new(name: MetricName, value: f64) -> Self { + Self { name, value } + } +} + +impl std::fmt::Display for MetricName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn publish_metrics(client: impl PutMetricData, metrics: Vec) { + let metrics = metrics.iter().map(|metric| metric.clone().into()).collect(); + + let result = client.put_metric_data(metric_namespace(), metrics).await; + + if let Err(error) = result { + warn!("Metric publish failed. Error: {:?}", error) + } +} + +pub async fn publish_metric(client: impl PutMetricData, metric: Metric) { + let metrics = vec![metric]; + publish_metrics(client, metrics).await +} + +#[cfg(test)] +mod tests { + use crate::error::{Error, Severity}; + + use super::*; + use async_trait::async_trait; + use aws_sdk_cloudwatch::{error::PutMetricDataError, output::PutMetricDataOutput}; + use aws_smithy_client::SdkError; + use mockall::mock; + + #[test] + fn test_metric_into_metric_datum() { + let metric = Metric::new(MetricName::Total, 75.0); + let actual: MetricDatum = metric.into(); + + let expected = MetricDatum::builder().metric_name("Total").value(75.0).build(); + + assert_eq!(expected, actual); + } + + #[tokio::test] + async fn test_publish_metrics_success() { + let mut cw_metrics_mock = MockCloudWatchMetrics::new(); + cw_metrics_mock + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_publish_metrics_success", metrics); + true + }) + .returning(|_, _| Ok(PutMetricDataOutput::builder().build())); + + let metrics = vec![Metric::new(MetricName::Updated, 7.0), Metric::new(MetricName::Total, 9.0)]; + + publish_metrics(cw_metrics_mock, metrics).await; + } + + #[tokio::test] + async fn test_publish_metrics_failed() { + let mut cw_metrics_mock = MockCloudWatchMetrics::new(); + cw_metrics_mock + .expect_put_metric_data() + .once() + .withf(|namespace, metrics| { + assert_eq!("LogRotation", namespace); + insta::assert_debug_snapshot!("CWMetricCall_publish_metrics_failed", metrics); + true + }) + // This type of error would never happen because it is "my" error type rather than an AWS error type. Luckily it doesn't matter -- we only care that an error happened. + .returning(|_, _| { + Err(SdkError::timeout_error(Box::new(Error { + message: "Some error happened".to_string(), + severity: Severity::Error, + }))) + }); + + let metrics = vec![Metric::new(MetricName::Updated, 7.0), Metric::new(MetricName::Total, 9.0)]; + + publish_metrics(cw_metrics_mock, metrics).await; + } + + mock! { + pub CloudWatchMetrics {} + + #[async_trait] + impl PutMetricData for CloudWatchMetrics { + async fn put_metric_data( + &self, + namespace: String, + metric_data: Vec, + ) -> Result>; + } + } +} diff --git a/src/retention_setter.rs b/src/retention_setter.rs new file mode 100644 index 0000000..f769a3b --- /dev/null +++ b/src/retention_setter.rs @@ -0,0 +1,99 @@ +use crate::{ + cloudwatch_logs_traits::DescribeLogGroups, + error::{Error, Severity}, +}; + +pub async fn get_existing_retention(log_group_name: &str, client: &impl DescribeLogGroups) -> Result { + let describe_log_groups_response = client.describe_log_groups(log_group_name).await?; + + let log_group = describe_log_groups_response + .log_groups() + .unwrap_or_default() + .iter() + .find(|log_group| log_group.log_group_name().unwrap_or_default() == log_group_name); + + match log_group { + Some(log_group) => Ok(log_group.retention_in_days().unwrap_or(0)), + None => Err(Error { + message: format!( + "Did not find log group named {}. Maybe it was deleted immediately after creation?", + log_group_name + ), + severity: Severity::Warning, + }), + } +} + +#[cfg(test)] +mod tests { + use aws_sdk_cloudwatchlogs::{error::DescribeLogGroupsError, model::LogGroup, output::DescribeLogGroupsOutput, types::SdkError}; + use mockall::predicate; + + use crate::{cloudwatch_logs_traits::MockDescribeLogGroups, error::Severity}; + + use super::get_existing_retention; + + #[tokio::test] + async fn test_get_existing_retention() { + let group = "MyLogGroup"; + let retention = 0; + + let mut mock_describe_log_groups = MockDescribeLogGroups::new(); + mock_describe_log_groups + .expect_describe_log_groups() + .with(predicate::eq(group)) + .returning(move |_| mock_describe_log_groups_response(group, retention)) + .once(); + + assert_eq!(retention, get_existing_retention(group, &mock_describe_log_groups).await.unwrap()); + + let retention = 30; + mock_describe_log_groups + .expect_describe_log_groups() + .with(predicate::eq(group)) + .returning(move |_| mock_describe_log_groups_response(group, retention)) + .once(); + + assert_eq!(retention, get_existing_retention(group, &mock_describe_log_groups).await.unwrap()); + } + + #[tokio::test] + async fn test_no_log_groups_found_results_in_warning() { + let group = "MyLogGroup"; + + let mut mock_describe_log_groups = MockDescribeLogGroups::new(); + mock_describe_log_groups + .expect_describe_log_groups() + .with(predicate::eq(group)) + .returning(|_| Ok(DescribeLogGroupsOutput::builder().build())) + .once(); + + let err = get_existing_retention(group, &mock_describe_log_groups).await.unwrap_err(); + + assert_eq!(Severity::Warning, err.severity); + assert!(err.message.contains(group)); + } + + #[tokio::test] + async fn test_wrong_log_groups_found_results_in_warning() { + let group = "MyLogGroup"; + + let mut mock_describe_log_groups = MockDescribeLogGroups::new(); + mock_describe_log_groups + .expect_describe_log_groups() + .with(predicate::eq(group)) + .returning(|_| mock_describe_log_groups_response("SomeRandomOtherLogGroupThatIDidNotAskFor", 0)) + .once(); + + let err = get_existing_retention(group, &mock_describe_log_groups).await.unwrap_err(); + + assert_eq!(Severity::Warning, err.severity); + assert!(err.message.contains(group)); + } + + fn mock_describe_log_groups_response(log_group_name: &str, retention: i32) -> Result> { + let log_group = LogGroup::builder().log_group_name(log_group_name).retention_in_days(retention).build(); + let response = DescribeLogGroupsOutput::builder().log_groups(log_group).build(); + Ok(response) + } +} diff --git a/src/snapshots/terraform_aws_default_log_retention__global__tests__cw_logs_client.snap b/src/snapshots/terraform_aws_default_log_retention__global__tests__cw_logs_client.snap new file mode 100644 index 0000000..59809b8 --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__global__tests__cw_logs_client.snap @@ -0,0 +1,40 @@ +--- +source: src/global.rs +expression: client +--- +CloudWatchLogs { + client: Client { + handle: Handle { + client: Client { + connector: DynConnector, + middleware: DynMiddleware, + retry_policy: Standard { + config: Config { + initial_retry_tokens: 500, + retry_cost: 5, + no_retry_increment: 1, + timeout_retry_cost: 10, + max_attempts: 10, + initial_backoff: 500ms, + max_backoff: 20s, + }, + shared_state: CrossRequestRetryState { + quota_available: Mutex { + data: 500, + poisoned: false, + .. + }, + }, + }, + operation_timeout_config: OperationTimeoutConfig { + operation_timeout: None, + operation_attempt_timeout: None, + }, + sleep_impl: Some( + TokioSleep, + ), + }, + conf: Config, + }, + }, +} diff --git a/src/snapshots/terraform_aws_default_log_retention__global__tests__cw_metrics_client.snap b/src/snapshots/terraform_aws_default_log_retention__global__tests__cw_metrics_client.snap new file mode 100644 index 0000000..ddf2fde --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__global__tests__cw_metrics_client.snap @@ -0,0 +1,40 @@ +--- +source: src/global.rs +expression: client +--- +CloudWatchMetrics { + client: Client { + handle: Handle { + client: Client { + connector: DynConnector, + middleware: DynMiddleware, + retry_policy: Standard { + config: Config { + initial_retry_tokens: 500, + retry_cost: 5, + no_retry_increment: 1, + timeout_retry_cost: 10, + max_attempts: 10, + initial_backoff: 500ms, + max_backoff: 20s, + }, + shared_state: CrossRequestRetryState { + quota_available: Mutex { + data: 500, + poisoned: false, + .. + }, + }, + }, + operation_timeout_config: OperationTimeoutConfig { + operation_timeout: None, + operation_attempt_timeout: None, + }, + sleep_impl: Some( + TokioSleep, + ), + }, + conf: Config, + }, + }, +} diff --git a/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall.snap b/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall.snap new file mode 100644 index 0000000..7a9e5bc --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall.snap @@ -0,0 +1,36 @@ +--- +source: src/metric_publisher.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 7.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 9.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_failed.snap b/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_failed.snap new file mode 100644 index 0000000..7a9e5bc --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_failed.snap @@ -0,0 +1,36 @@ +--- +source: src/metric_publisher.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 7.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 9.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_success.snap b/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_success.snap new file mode 100644 index 0000000..7a9e5bc --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__metric_publisher__tests__CWMetricCall_publish_metrics_success.snap @@ -0,0 +1,36 @@ +--- +source: src/metric_publisher.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 7.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, + MetricDatum { + metric_name: Some( + "Total", + ), + dimensions: None, + timestamp: None, + value: Some( + 9.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_do_not_overwrite_when_retention_tag_set.snap b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_do_not_overwrite_when_retention_tag_set.snap new file mode 100644 index 0000000..9f0d60e --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_do_not_overwrite_when_retention_tag_set.snap @@ -0,0 +1,21 @@ +--- +source: src/main.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "AlreadyTaggedWithRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_fails_when_tag_log_group_fails.snap b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_fails_when_tag_log_group_fails.snap new file mode 100644 index 0000000..28d2ee3 --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_fails_when_tag_log_group_fails.snap @@ -0,0 +1,21 @@ +--- +source: src/main.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_retention_already_set.snap b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_retention_already_set.snap new file mode 100644 index 0000000..135a33f --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_retention_already_set.snap @@ -0,0 +1,21 @@ +--- +source: src/main.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "AlreadyHasRetention", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_success_no_tags.snap b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_success_no_tags.snap new file mode 100644 index 0000000..28d2ee3 --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__CWMetricCall_process_event_success_no_tags.snap @@ -0,0 +1,21 @@ +--- +source: src/main.rs +expression: metrics +--- +[ + MetricDatum { + metric_name: Some( + "Updated", + ), + dimensions: None, + timestamp: None, + value: Some( + 1.0, + ), + statistic_values: None, + values: None, + counts: None, + unit: None, + storage_resolution: None, + }, +] diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__parse_event_fail.snap b/src/snapshots/terraform_aws_default_log_retention__tests__parse_event_fail.snap new file mode 100644 index 0000000..23d237e --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__parse_event_fail.snap @@ -0,0 +1,8 @@ +--- +source: src/main.rs +expression: result +--- +Error { + message: "Error deserializing input payload. Payload: `{\"invalid\":\"input\"}`. Context: `Some(Context { request_id: \"1231231233123123123\", deadline: 0, invoked_function_arn: \"arn:aws:whatever:my-awesome-stuff\", xray_trace_id: None, client_context: None, identity: None, env_config: Config { function_name: \"\", memory: 0, version: \"\", log_stream: \"\", log_group: \"\" } })`. Error: `missing field `detail``.", + severity: Warning, +} diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__process_event_bad_input.snap b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_bad_input.snap new file mode 100644 index 0000000..8fed23a --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_bad_input.snap @@ -0,0 +1,8 @@ +--- +source: src/main.rs +expression: result +--- +Object { + "message": String("Error deserializing input payload. Payload: `{\"invalid\":\"input\"}`. Context: `Some(Context { request_id: \"\", deadline: 0, invoked_function_arn: \"\", xray_trace_id: None, client_context: None, identity: None, env_config: Config { function_name: \"\", memory: 0, version: \"\", log_stream: \"\", log_group: \"\" } })`. Error: `missing field `detail``."), + "severity": String("Warning"), +} diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__process_event_do_not_overwrite_when_retention_tag_set.snap b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_do_not_overwrite_when_retention_tag_set.snap new file mode 100644 index 0000000..735dd7a --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_do_not_overwrite_when_retention_tag_set.snap @@ -0,0 +1,7 @@ +--- +source: src/main.rs +expression: result +--- +Object { + "message": String("Not setting retention for MyLogGroupWasCreated because tag `retention`=`Do not override please` exists on it."), +} diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_put_retention_policy_fails.snap b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_put_retention_policy_fails.snap new file mode 100644 index 0000000..bb4635f --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_put_retention_policy_fails.snap @@ -0,0 +1,8 @@ +--- +source: src/main.rs +expression: error +--- +Error { + message: "request has timed out", + severity: Error, +} diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_tag_log_group_fails.snap b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_tag_log_group_fails.snap new file mode 100644 index 0000000..bb4635f --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_fails_when_tag_log_group_fails.snap @@ -0,0 +1,8 @@ +--- +source: src/main.rs +expression: error +--- +Error { + message: "request has timed out", + severity: Error, +} diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__process_event_retention_already_set.snap b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_retention_already_set.snap new file mode 100644 index 0000000..f39fb91 --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_retention_already_set.snap @@ -0,0 +1,7 @@ +--- +source: src/main.rs +expression: result +--- +Object { + "message": String("Not setting retention for MyLogGroupWasCreated because it is set to 30 days already."), +} diff --git a/src/snapshots/terraform_aws_default_log_retention__tests__process_event_success_no_tags.snap b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_success_no_tags.snap new file mode 100644 index 0000000..ae39519 --- /dev/null +++ b/src/snapshots/terraform_aws_default_log_retention__tests__process_event_success_no_tags.snap @@ -0,0 +1,7 @@ +--- +source: src/main.rs +expression: result +--- +Object { + "message": String("Retention set successfully"), +} diff --git a/tf-alarm-topic.tf b/tf-alarm-topic.tf new file mode 100644 index 0000000..fee2086 --- /dev/null +++ b/tf-alarm-topic.tf @@ -0,0 +1,66 @@ +locals { + create_sns_topic = can([var.alarm_configuration.email_notification_list]) + sns_topic_arn = local.create_sns_topic ? aws_sns_topic.alarms[0].arn : var.alarm_configuration.sns_topic_arn +} + +resource "aws_sns_topic" "alarms" { + count = local.create_sns_topic ? 1 : 0 + name = var.name + + kms_master_key_id = data.aws_kms_key.master.id + tags = var.tags +} + +resource "aws_sns_topic_subscription" "alarms" { + for_each = local.create_sns_topic ? toset(var.alarm_configuration.email_notification_list) : toset([]) + topic_arn = aws_sns_topic.alarms[0].arn + protocol = "email" + endpoint = each.value +} + +resource "aws_sns_topic_policy" "alarms" { + count = local.create_sns_topic ? 1 : 0 + arn = aws_sns_topic.alarms[count.index].arn + policy = data.aws_iam_policy_document.alarms[count.index].json +} + +data "aws_iam_policy_document" "alarms" { + count = local.create_sns_topic ? 1 : 0 + statement { + sid = "CloudWatchAlarm" + principals { + type = "Service" + identifiers = ["cloudwatch.amazonaws.com"] + } + effect = "Allow" + actions = ["sns:Publish"] + resources = [aws_sns_topic.alarms[count.index].arn] + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [data.aws_caller_identity.current.account_id] + } + } + + statement { + sid = "Owners" + principals { + type = "AWS" + identifiers = [data.aws_iam_session_context.current.issuer_arn] + } + effect = "Allow" + # cannot do sns:* for SNS access policy + # https://docs.aws.amazon.com/sns/latest/dg/sns-access-policy-language-api-permissions-reference.html + actions = [ + "sns:GetTopicAttributes", + "sns:SetTopicAttributes", + "sns:AddPermission", + "sns:RemovePermission", + "sns:DeleteTopic", + "sns:Subscribe", + "sns:ListSubscriptionsByTopic", + "sns:Publish" + ] + resources = [aws_sns_topic.alarms[count.index].arn] + } +} diff --git a/tf-alarms.tf b/tf-alarms.tf new file mode 100644 index 0000000..50feba0 --- /dev/null +++ b/tf-alarms.tf @@ -0,0 +1,58 @@ +locals { + alarms = { + retention-errors = { + description = "Errors occurred invoking the ${aws_lambda_function.log_retention.function_name} function! Log groups are not getting default retention.", + comparison_operator = "GreaterThanThreshold" + threshold = aws_lambda_function_event_invoke_config.log_retention.maximum_retry_attempts # Set to value of max retry attempts (2) because if all attempts fail we will see `retry attempts + 1 primary attempt` failures (3). Alarm only when all fail. + period = (aws_lambda_function.log_retention.timeout * (aws_lambda_function_event_invoke_config.log_retention.maximum_retry_attempts + 1)) + 60 + metric_name = "Errors" + function_name = aws_lambda_function.log_retention.function_name + } + retention-throttles = { + description = "The ${aws_lambda_function.log_retention.function_name} function was throttled! Log groups are not getting default retention.", + comparison_operator = "GreaterThanThreshold" + threshold = 0 + period = 300 + metric_name = "Throttles" + function_name = aws_lambda_function.log_retention.function_name + } + global-retention-errors = { + description = "Errors occurred invoking the ${aws_lambda_function.global_log_retention.function_name} function! Log groups are not getting default retention.", + comparison_operator = "GreaterThanThreshold" + threshold = aws_lambda_function_event_invoke_config.global_log_retention.maximum_retry_attempts # Set to value of max retry attempts (2) because if all attempts fail we will see `retry attempts + 1 primary attempt` failures (3). Alarm only when all fail. + period = (aws_lambda_function.global_log_retention.timeout * (aws_lambda_function_event_invoke_config.global_log_retention.maximum_retry_attempts + 1)) + 60 + metric_name = "Errors" + function_name = aws_lambda_function.global_log_retention.function_name + } + global-retention-throttles = { + description = "The ${aws_lambda_function.global_log_retention.function_name} function was throttled! Log groups are not getting default retention.", + comparison_operator = "GreaterThanThreshold" + threshold = 0 + period = 300 + metric_name = "Throttles" + function_name = aws_lambda_function.global_log_retention.function_name + } + } +} + +resource "aws_cloudwatch_metric_alarm" "alarm" { + for_each = local.alarms + alarm_name = "${var.name}-${each.key}" + alarm_description = each.value.description + comparison_operator = each.value.comparison_operator + threshold = each.value.threshold + evaluation_periods = 1 + metric_name = each.value.metric_name + namespace = "AWS/Lambda" + period = each.value.period + statistic = "Sum" + actions_enabled = true + + alarm_actions = [local.sns_topic_arn] + ok_actions = [] + insufficient_data_actions = [] + + dimensions = { FunctionName = each.value.function_name } + + tags = var.tags +} diff --git a/tf-cloudwatch-logs.tf b/tf-cloudwatch-logs.tf new file mode 100644 index 0000000..532c97d --- /dev/null +++ b/tf-cloudwatch-logs.tf @@ -0,0 +1,31 @@ +data "aws_lambda_function" "datadog" { + function_name = "sf-datadog-lambda-forwarder" +} + +resource "aws_cloudwatch_log_group" "log_retention_lambda" { + name = "/aws/lambda/${local.log_retention_lambda_name}" + retention_in_days = 30 + tags = var.tags +} + +resource "aws_cloudwatch_log_subscription_filter" "log_retention_lambda_datadog" { + count = var.enable_datadog_log_subscription ? 1 : 0 + name = "default" + destination_arn = data.aws_lambda_function.datadog.arn + log_group_name = aws_cloudwatch_log_group.log_retention_lambda.name + filter_pattern = "" +} + +resource "aws_cloudwatch_log_group" "global_log_retention_lambda" { + name = "/aws/lambda/${local.global_log_retention_lambda_name}" + retention_in_days = 30 + tags = var.tags +} + +resource "aws_cloudwatch_log_subscription_filter" "global_log_retention_lambda_datadog" { + count = var.enable_datadog_log_subscription ? 1 : 0 + name = "default" + destination_arn = data.aws_lambda_function.datadog.arn + log_group_name = aws_cloudwatch_log_group.global_log_retention_lambda.name + filter_pattern = "" +} diff --git a/tf-global-retention-setter.tf b/tf-global-retention-setter.tf new file mode 100644 index 0000000..aeae2d2 --- /dev/null +++ b/tf-global-retention-setter.tf @@ -0,0 +1,73 @@ +data "archive_file" "global_log_retention" { + type = "zip" + source_file = "${path.module}/dist/global_retention_setter/bootstrap" + output_path = "${path.module}/dist/global_retention_setter/bootstrap.zip" +} + +resource "aws_lambda_function" "global_log_retention" { + depends_on = [aws_cloudwatch_log_group.global_log_retention_lambda] + function_name = local.global_log_retention_lambda_name + filename = data.archive_file.global_log_retention.output_path + source_code_hash = data.archive_file.global_log_retention.output_base64sha256 + runtime = "provided.al2" + architectures = ["arm64"] + handler = "bootstrap" + role = aws_iam_role.log_retention.arn + timeout = 900 + kms_key_arn = var.kms_key_arn + memory_size = 128 + description = "Sets default CloudWatch Logs retention settings for all existing log groups." + + environment { + variables = { + log_retention_in_days = var.log_retention_in_days + log_group_tags = local.log_group_tags_json + metric_namespace = var.metric_namespace + RUST_BACKTRACE = 1 + RUST_LOG = "warn,global_retention_setter=${var.log_level}" # https://docs.rs/env_logger/latest/env_logger/ + } + } + + vpc_config { + subnet_ids = data.aws_subnets.subnets.ids + security_group_ids = [local.https_security_group_id] + } + + tags = var.tags +} + +resource "aws_lambda_invocation" "run_on_existing_groups" { + count = var.set_on_all_existing_groups ? 1 : 0 + function_name = aws_lambda_function.global_log_retention.function_name + input = "{}" +} + +resource "aws_lambda_function_event_invoke_config" "global_log_retention" { + function_name = aws_lambda_function.global_log_retention.function_name + maximum_retry_attempts = 2 # This is default, but setting it to ensure that is the case. +} + + +resource "aws_cloudwatch_event_rule" "global_log_retention" { + count = var.global_log_retention_run_period == null ? 0 : 1 + name = aws_lambda_function.global_log_retention.function_name + description = "Sets default retention for all log groups in the region every ${var.global_log_retention_run_period} minutes" + schedule_expression = "rate(${var.global_log_retention_run_period} minutes)" + tags = var.tags +} + +resource "aws_cloudwatch_event_target" "global_log_retention" { + count = var.global_log_retention_run_period == null ? 0 : 1 + rule = aws_cloudwatch_event_rule.global_log_retention[0].name + target_id = "lambda" + arn = aws_lambda_function.global_log_retention.arn +} + +resource "aws_lambda_permission" "global_log_retention" { + count = var.global_log_retention_run_period == null ? 0 : 1 + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.global_log_retention.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.global_log_retention[0].arn +} diff --git a/tf-inputs.tf b/tf-inputs.tf new file mode 100644 index 0000000..f19e69a --- /dev/null +++ b/tf-inputs.tf @@ -0,0 +1,96 @@ +variable "name" { + type = string + description = "Base name for all resources. E.x. ." +} + +variable "vpc_id" { + type = string + description = "Pass in the ID of the VPC to override. Defaults to the first VPC found in the account." + default = null +} + +variable "https_egress_security_group_name" { + type = string + description = "Pass in the name of a security group to override. Name of a security group which provides egress on port 443 to CloudWatch Logs." + default = null +} + +variable "kms_key_arn" { + type = string + default = null + description = "Provide a KMS key to override usage of the default master KMS key." +} + +variable "permissions_boundary_arn" { + type = string + default = null + description = "Provide a permissions boundary ARN if you are bound by one." +} + +variable "alarm_configuration" { + type = any + description = "Provide either `sns_topic_arn` to an existing SNS topic, or a list of email users `email_notification_list` to subscribe for notifications. Alarm creation is REQUIRED for this module. Note that retention setting is retried automatically, so an alarm may mean that it failed the first time and succeeded the second time. Investigating logs for each failure is recommended." + + validation { + condition = ( + can([var.alarm_configuration.sns_topic_arn]) + || + can([var.alarm_configuration.email_notification_list]) + ) + error_message = "Must pass either a SNS topic ARN or an email notification list." + } +} + +variable "log_level" { + type = string + default = "info" + description = "Override Lambda log level (trace/debug/info/warn/error)" +} + +variable "enable_datadog_log_subscription" { + type = bool + default = true + description = "Subscribes the logs from the Lambda to Datadog" +} + +variable "log_retention_in_days" { + type = number + default = 90 + description = "Default number of days to set on new log groups. Must be a valid option that CloudWatch Logs support: https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutRetentionPolicy.html#API_PutRetentionPolicy_RequestParameters" +} + +variable "log_group_tags" { + type = map(string) + default = null + description = "Set of tags to put on all log groups when retention is set. If not set, no tags will be added. If set, a `retention` tag will automatically be added to this list." +} + +variable "set_on_all_existing_groups" { + type = bool + default = true + description = "Set to false to disable running a bit of code which will set retention on all existing groups." +} + +variable "global_log_retention_run_period" { + type = number + default = 60 * 6 + description = "Set to a number of minutes to invoke the global log retention Lambda on a schedule. Note that running it may cause perpetual diffs in other people's Terraform if they are creating a log group and not setting retention." +} + +variable "metric_namespace" { + type = string + default = "LogRetention" + description = "CloudWatch Metric namespace for custom metrics emitted by these Lambdas." +} + +variable "iam_role_suffix" { + type = string + default = "" + description = "Due to Terraform limitations, this module always creates an IAM role. Pass in a suffix for the IAM role name so that it does not conflict between regions." +} + +variable "tags" { + type = map(string) + default = null + description = "Adds tags to all created resources. It is highly recommended to use the AWS Provider's default tags instead of this variable. See: https://www.hashicorp.com/blog/default-tags-in-the-terraform-aws-provider. You can also use this input to add additional tags above and beyond the tags that are added by default_tags." +} diff --git a/tf-lambda-iam-role.tf b/tf-lambda-iam-role.tf new file mode 100644 index 0000000..855aef2 --- /dev/null +++ b/tf-lambda-iam-role.tf @@ -0,0 +1,63 @@ +resource "aws_iam_role" "log_retention" { + name = local.iam_role_name + assume_role_policy = data.aws_iam_policy_document.lambda_assume.json + permissions_boundary = var.permissions_boundary_arn + + inline_policy { + name = "log_retention" + policy = data.aws_iam_policy_document.log_retention.json + } + + tags = var.tags +} + +data "aws_iam_policy_document" "log_retention" { + statement { + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:ListTagsForResource", + "logs:TagResource", + "logs:PutRetentionPolicy", + "logs:DescribeLogGroups" + ] + resources = ["arn:aws:logs:*:*:*"] + } + + statement { + actions = ["tag:GetResources"] + resources = ["*"] + } + + statement { + actions = ["cloudwatch:PutMetricData"] + resources = ["*"] + condition { + test = "StringEquals" + variable = "cloudwatch:namespace" + values = [var.metric_namespace] + } + } + + statement { + actions = ["kms:GenerateDataKey"] + resources = [local.kms_key_arn] + } + + statement { + actions = ["ec2:*NetworkInterface*"] + resources = ["*"] + } +} + +data "aws_iam_policy_document" "lambda_assume" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} diff --git a/tf-locals.tf b/tf-locals.tf new file mode 100644 index 0000000..d7cd7df --- /dev/null +++ b/tf-locals.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} + +locals { + log_retention_lambda_name = "${var.name}-log-retention-setter" + global_log_retention_lambda_name = "${var.name}-global-log-retention-setter" + iam_role_name = "${local.log_retention_lambda_name}${var.iam_role_suffix}" + log_group_tags_json = var.log_group_tags == null ? "" : jsonencode(var.log_group_tags) # Null causes JSON parse error in Lambda +} diff --git a/tf-log-retention-lambda.tf b/tf-log-retention-lambda.tf new file mode 100644 index 0000000..0f9e113 --- /dev/null +++ b/tf-log-retention-lambda.tf @@ -0,0 +1,80 @@ +data "archive_file" "log_retention" { + type = "zip" + source_file = "${path.module}/dist/log_retention_setter/bootstrap" + output_path = "${path.module}/dist/log_retention_setter/bootstrap.zip" +} + +resource "aws_lambda_function" "log_retention" { + depends_on = [aws_cloudwatch_log_group.log_retention_lambda] + function_name = local.log_retention_lambda_name + filename = data.archive_file.log_retention.output_path + source_code_hash = data.archive_file.log_retention.output_base64sha256 + runtime = "provided.al2" + architectures = ["arm64"] + handler = "bootstrap" + role = aws_iam_role.log_retention.arn + timeout = 60 + kms_key_arn = var.kms_key_arn + memory_size = 128 + description = "Sets default CloudWatch Logs retention settings for new log groups." + + environment { + variables = { + log_retention_in_days = var.log_retention_in_days + log_group_tags = local.log_group_tags_json + metric_namespace = var.metric_namespace + RUST_BACKTRACE = 1 + RUST_LOG = "warn,terraform_aws_default_log_retention=${var.log_level}" # https://docs.rs/env_logger/latest/env_logger/ + } + } + + vpc_config { + subnet_ids = data.aws_subnets.subnets.ids + security_group_ids = [local.https_security_group_id] + } + + tags = var.tags +} + +resource "aws_cloudwatch_event_rule" "log_group_creation" { + name = "${var.name}-log-group-creation" + + event_pattern = <