diff --git a/Cargo.lock b/Cargo.lock index 76dc5ebc1..db56152da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,28 @@ dependencies = [ "syn 2.0.72", ] +[[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.72", +] + [[package]] name = "async-task" version = "4.7.1" @@ -2428,6 +2450,7 @@ dependencies = [ name = "pubsys" version = "0.1.0" dependencies = [ + "async-stream", "aws-config", "aws-credential-types", "aws-sdk-ebs", @@ -2463,6 +2486,7 @@ dependencies = [ "tempfile", "tinytemplate", "tokio", + "tokio-retry", "tokio-stream", "toml", "tough", @@ -3474,6 +3498,17 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" diff --git a/Cargo.toml b/Cargo.toml index a0a22017f..ab57470ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,95 @@ installers = [] # Target platforms to build apps for (Rust target-triple syntax) targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"] +[workspace.dependencies] +bottlerocket-types = { version = "0.0.14", git = "https://github.com/bottlerocket-os/bottlerocket-test-system", tag = "v0.0.14" } +bottlerocket-variant = { version = "0.1", path = "tools/bottlerocket-variant" } +buildsys = { version = "0.1", path = "tools/buildsys", lib = true, artifact = [ "bin:buildsys", "bin:bottlerocket-variant" ] } +buildsys-config = { version = "0.1", path = "tools/buildsys-config" } +oci-cli-wrapper = { version = "0.1", path = "tools/oci-cli-wrapper" } +parse-datetime = { version = "0.1", path = "tools/parse-datetime" } +pipesys = { version = "0.1", path = "tools/pipesys", lib = true, artifact = [ "bin:pipesys" ] } +pubsys = { version = "0.1", path = "tools/pubsys", artifact = [ "bin:pubsys" ] } +pubsys-config = { version = "0.1", path = "tools/pubsys-config" } +pubsys-setup = { version = "0.1", path = "tools/pubsys-setup", artifact = [ "bin:pubsys-setup" ] } +testsys = { version = "0.1", path = "tools/testsys", artifact = [ "bin:testsys" ] } +testsys-config = { version = "0.1", path = "tools/testsys-config" } +testsys-model = { version = "0.0.14", git = "https://github.com/bottlerocket-os/bottlerocket-test-system", tag = "v0.0.14" } +twoliter = { version = "0", path = "twoliter", artifact = [ "bin:twoliter" ] } +unplug = { version = "0.1", path = "tools/unplug", artifact = [ "bin:unplug" ] } +update-metadata = { version = "0.1", path = "tools/update-metadata" } + +anyhow = "1" +async-recursion = "1" +async-stream = "0.3" +async-trait = "0.1" +async-walkdir = "1" +aws-config = "1" +aws-credential-types = "1" +aws-sdk-ebs = "1" +aws-sdk-ec2 = "1" +aws-sdk-kms = "1" +aws-sdk-ssm = "1" +aws-sdk-sts = "1" +aws-smithy-types = "1" +aws-types = "1" +base64 = "0.22" +bytes = "1" +chrono = { version = "0.4", default-features = false } +clap = "4" +coldsnap = { version = "0.6", default-features = false } +daemonize = "0.5" +duct = "0.13" +env_logger = "0.11" +fastrand = "2" +filetime = "0.2" +flate2 = "1" +futures = "0.3" +governor = "0.6" +guppy = "0.17" +handlebars = "5" +hex = "0.4" +home = "0.5" +indicatif = "0.17" +lazy_static = "1" +log = "0.4" +maplit = "1" +nix = "0.28" +nonzero_ext = "0.3" +num_cpus = "1" +olpc-cjson = "0.1" +rand = { version = "0.8", default-features = false } +regex = "1" +reqwest = { version = "0.11", default-features = false } +semver = "1" +serde = "1" +serde_json = "1" +serde_plain = "1" +serde_yaml = "0.9" +sha2 = "0.10" +shell-words = "1" +simplelog = "0.12" +snafu = "0.8" +tabled = "0.10" +tar = "0.4" +tempfile = "3" +term_size = "0.3" +tinytemplate = "1" +tokio = "1" +tokio-stream = "0.1" +tokio-retry = "0.3" +toml = "0.8" +tough = "0.17" +tough-kms = "0.9" +tough-ssm = "0.12" +tracing = "0.1" +tuftool = { version = "0.10", artifact = [ "bin:tuftool" ] } +unescape = "0.1" +url = "2" +uuid = "1" +walkdir = "2" +which = "6" + # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" diff --git a/deny.toml b/deny.toml index dd7481edf..f3f32e57e 100644 --- a/deny.toml +++ b/deny.toml @@ -74,16 +74,8 @@ skip = [ { name = "http", version = "=0.2" }, # dependencies are using different versions of http-body { name = "http-body", version = "=0.4" }, - # kube-client uses an older version of pem - { name = "pem", version = "=1" }, # dependencies are using different versions of redox_syscall { name = "redox_syscall", version = "=0.4" }, - # several dependencies are using an old version of serde_yaml - { name = "serde_yaml", version = "=0.8" }, - # dependencies are using different versions of snafu - { name = "snafu", version = "=0.7" }, - # dependencies are using different versions of snafu-derive - { name = "snafu-derive", version = "=0.7" }, # dependencies are using different versions of strsim { name = "strsim", version = "=0.10" }, # multiple deps are using an older version of syn diff --git a/tests/integration-tests/Cargo.toml b/tests/integration-tests/Cargo.toml index 869c25288..7597899f4 100644 --- a/tests/integration-tests/Cargo.toml +++ b/tests/integration-tests/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" license = "MIT OR Apache-2.0" [dev-dependencies] -tokio = { version = "1", default-features = false, features = ["process", "fs", "rt-multi-thread"] } -twoliter = { version = "0", path = "../../twoliter", artifact = [ "bin:twoliter" ] } +tokio = { workspace = true, features = ["fs", "process", "rt-multi-thread"] } +twoliter = { workspace = true } diff --git a/tools/bottlerocket-variant/Cargo.toml b/tools/bottlerocket-variant/Cargo.toml index 6224deec2..701befc46 100644 --- a/tools/bottlerocket-variant/Cargo.toml +++ b/tools/bottlerocket-variant/Cargo.toml @@ -9,6 +9,6 @@ publish = false exclude = ["README.md"] [dependencies] -serde = "1" -snafu = "0.8" +serde.workspace = true +snafu.workspace = true diff --git a/tools/buildsys/Cargo.toml b/tools/buildsys/Cargo.toml index 392310bcd..1c64ecb52 100644 --- a/tools/buildsys/Cargo.toml +++ b/tools/buildsys/Cargo.toml @@ -9,27 +9,27 @@ publish = false exclude = ["README.md"] [dependencies] -bottlerocket-variant = { version = "0.1", path = "../bottlerocket-variant" } -buildsys-config = { version = "0.1", path = "../buildsys-config" } -clap = { version = "4", features = ["derive", "env"] } -duct = "0.13" -guppy = "0.17" -hex = "0.4" -lazy_static = "1" -pipesys = { version = "0.1", path = "../pipesys" } -rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } -regex = "1" -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] } -serde = { version = "1", features = ["derive"] } -serde_plain = "1" -serde_json = "1" -sha2 = "0.10" -snafu = "0.8" -tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] } -toml = "0.8" -url = { version = "2", features = ["serde"] } -walkdir = "2" -nonzero_ext = "0.3" +bottlerocket-variant.workspace = true +buildsys-config.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +duct.workspace = true +guppy.workspace = true +hex.workspace = true +lazy_static.workspace = true +pipesys.workspace = true +rand = { workspace = true, features = ["std", "std_rng"] } +regex.workspace = true +reqwest = { workspace = true, features = ["blocking", "rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_plain.workspace = true +serde_json.workspace = true +sha2.workspace = true +snafu.workspace = true +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread"] } +toml.workspace = true +url = { workspace = true, features = ["serde"] } +walkdir.workspace = true +nonzero_ext.workspace = true [dev-dependencies] -tempfile = "3" +tempfile.workspace = true diff --git a/tools/oci-cli-wrapper/Cargo.toml b/tools/oci-cli-wrapper/Cargo.toml index e39cea7bf..dcbd2f833 100644 --- a/tools/oci-cli-wrapper/Cargo.toml +++ b/tools/oci-cli-wrapper/Cargo.toml @@ -7,14 +7,14 @@ edition = "2021" publish = false [dependencies] -async-trait = "0.1" -log = "0.4" -olpc-cjson = "0.1" -regex = "1" -serde = { version = "1", features = ["derive"]} -serde_json = "1" -snafu = "0.8" -tar = "0.4" -tempfile = "3" -tokio = { version = "1.32", features = ["process"] } -which = "6" +async-trait.workspace = true +log.workspace = true +olpc-cjson.workspace = true +regex.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +snafu.workspace = true +tar.workspace = true +tempfile.workspace = true +tokio = { workspace = true, features = ["process"] } +which.workspace = true diff --git a/tools/parse-datetime/Cargo.toml b/tools/parse-datetime/Cargo.toml index 538eb5af1..3346a0e57 100644 --- a/tools/parse-datetime/Cargo.toml +++ b/tools/parse-datetime/Cargo.toml @@ -9,5 +9,5 @@ publish = false exclude = ["README.md"] [dependencies] -chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } -snafu = { version = "0.8", features = ["backtraces-impl-backtrace-crate"] } +chrono = { workspace = true, features = ["clock", "std"] } +snafu = { workspace = true, features = ["backtraces-impl-backtrace-crate"] } diff --git a/tools/pipesys/Cargo.toml b/tools/pipesys/Cargo.toml index 3c06b43d9..230173781 100644 --- a/tools/pipesys/Cargo.toml +++ b/tools/pipesys/Cargo.toml @@ -7,14 +7,14 @@ edition = "2021" publish = false [dependencies] -anyhow = "1.0.75" -clap = { version = "4", features = ["derive"] } -daemonize = "0.5.0" -env_logger = "0.11.3" -futures = "0.3.28" -log = "0.4.20" -nix = { version = "0.28.0", features = ["fs"] } -tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] } +anyhow.workspace = true +clap = { workspace = true, features = ["derive"] } +daemonize.workspace = true +env_logger.workspace = true +futures.workspace = true +log.workspace = true +nix = { workspace = true, features = ["fs"] } +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread"] } [target.'cfg(target_os = "linux")'.dependencies] inotify = "0.10.2" diff --git a/tools/pubsys-config/Cargo.toml b/tools/pubsys-config/Cargo.toml index afd12eab6..a0f4830b4 100644 --- a/tools/pubsys-config/Cargo.toml +++ b/tools/pubsys-config/Cargo.toml @@ -7,13 +7,13 @@ edition = "2021" publish = false [dependencies] -chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } -home = "0.5" -lazy_static = "1" -log = "0.4" -parse-datetime = { path = "../parse-datetime", version = "0.1" } -serde = { version = "1", features = ["derive"] } -serde_yaml = "0.9" -snafu = "0.8" -toml = "0.8" -url = { version = "2", features = ["serde"] } +chrono = { workspace = true, features = ["clock", "std"] } +home.workspace = true +lazy_static.workspace = true +log.workspace = true +parse-datetime.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_yaml.workspace = true +snafu.workspace = true +toml.workspace = true +url = { workspace = true, features = ["serde"] } diff --git a/tools/pubsys-setup/Cargo.toml b/tools/pubsys-setup/Cargo.toml index 176649455..6cf16655b 100644 --- a/tools/pubsys-setup/Cargo.toml +++ b/tools/pubsys-setup/Cargo.toml @@ -7,14 +7,14 @@ edition = "2021" publish = false [dependencies] -clap = { version = "4", features = ["derive"] } -hex = "0.4" -log = "0.4" -pubsys-config = { path = "../pubsys-config/", version = "0.1" } -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] } -sha2 = "0.10" -shell-words = "1" -simplelog = "0.12" -snafu = "0.8" -tempfile = "3" -url = { version = "2", features = ["serde"] } +clap = { workspace = true, features = ["derive"] } +hex.workspace = true +log.workspace = true +pubsys-config.workspace = true +reqwest = { workspace = true, features = ["blocking", "rustls-tls"] } +sha2.workspace = true +shell-words.workspace = true +simplelog.workspace = true +snafu.workspace = true +tempfile.workspace = true +url = { workspace = true, features = ["serde"] } diff --git a/tools/pubsys/Cargo.toml b/tools/pubsys/Cargo.toml index 2e5c6a4cb..0f265526e 100644 --- a/tools/pubsys/Cargo.toml +++ b/tools/pubsys/Cargo.toml @@ -7,45 +7,47 @@ edition = "2021" publish = false [dependencies] -aws-config = "1" -aws-credential-types = "1" -aws-sdk-ebs = "1" -aws-sdk-ec2 = "1" -aws-sdk-kms = "1" -aws-sdk-ssm = "1" -aws-sdk-sts = "1" -aws-smithy-types = "1" -aws-types = "1" -buildsys = { path = "../buildsys", version = "0.1" } -bytes = "1" -chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } -clap = { version = "4", features = ["derive"] } -coldsnap = { version = "0.6", default-features = false, features = ["aws-sdk-rust-rustls"] } -duct = "0.13" -futures = "0.3" -governor = "0.6" -indicatif = "0.17" -lazy_static = "1" -log = "0.4" -nonzero_ext = "0.3" -num_cpus = "1" -oci-cli-wrapper = { path = "../oci-cli-wrapper", version = "0.1" } -parse-datetime = { path = "../parse-datetime", version = "0.1" } -pubsys-config = { path = "../pubsys-config/", version = "0.1" } -semver = "1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_plain = "1" -simplelog = "0.12" -snafu = "0.8" -tabled = "0.10" -tempfile = "3" -tinytemplate = "1" -tokio = { version = "1", features = ["full"] } -tokio-stream = { version = "0.1", features = ["time"] } -toml = "0.8" -tough = { version = "0.17", features = ["http"] } -tough-kms = "0.9" -tough-ssm = "0.12" -update-metadata = { path = "../update-metadata/", version = "0.1" } -url = { version = "2", features = ["serde"] } +async-stream.workspace = true +aws-config.workspace = true +aws-credential-types.workspace = true +aws-sdk-ebs.workspace = true +aws-sdk-ec2.workspace = true +aws-sdk-kms.workspace = true +aws-sdk-ssm.workspace = true +aws-sdk-sts.workspace = true +aws-smithy-types.workspace = true +aws-types.workspace = true +buildsys.workspace = true +bytes.workspace = true +chrono = { workspace = true, features = ["clock", "std"] } +clap = { workspace = true, features = ["derive"] } +coldsnap = { workspace = true, features = ["aws-sdk-rust-rustls"] } +duct.workspace = true +futures.workspace = true +governor.workspace = true +indicatif.workspace = true +lazy_static.workspace = true +log.workspace = true +nonzero_ext.workspace = true +num_cpus.workspace = true +oci-cli-wrapper.workspace = true +parse-datetime.workspace = true +pubsys-config.workspace = true +semver.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +serde_plain.workspace = true +simplelog.workspace = true +snafu.workspace = true +tabled.workspace = true +tempfile.workspace = true +tinytemplate.workspace = true +tokio = { workspace = true, features = ["full"] } +tokio-retry.workspace = true +tokio-stream = { workspace = true, features = ["time"] } +toml.workspace = true +tough = { workspace = true, features = ["http"] } +tough-kms.workspace = true +tough-ssm.workspace = true +update-metadata.workspace = true +url = { workspace = true, features = ["serde"] } diff --git a/tools/pubsys/src/aws/promote_ssm/mod.rs b/tools/pubsys/src/aws/promote_ssm/mod.rs index 91db607d0..36c0b7a9f 100644 --- a/tools/pubsys/src/aws/promote_ssm/mod.rs +++ b/tools/pubsys/src/aws/promote_ssm/mod.rs @@ -307,43 +307,28 @@ mod error { #[snafu(visibility(pub(super)))] pub(crate) enum Error { #[snafu(display("Error reading config: {}", source))] - Config { - source: pubsys_config::Error, - }, + Config { source: pubsys_config::Error }, #[snafu(display("Found no parameters in source version {}", version))] - EmptySource { - version: String, - }, + EmptySource { version: String }, #[snafu(display("Failed to fetch parameters from SSM: {}", source))] - FetchSsm { - source: ssm::Error, - }, + FetchSsm { source: ssm::Error }, #[snafu(display("Failed to find templates: {}", source))] - FindTemplates { - source: template::Error, - }, + FindTemplates { source: template::Error }, #[snafu(display("Infra.toml is missing {}", missing))] - MissingConfig { - missing: String, - }, + MissingConfig { missing: String }, #[snafu(display("Failed to render templates: {}", source))] - RenderTemplates { - source: template::Error, - }, + RenderTemplates { source: template::Error }, #[snafu(display("Failed to set SSM parameters: {}", source))] - SetSsm { - source: ssm::Error, - }, + SetSsm { source: ssm::Error }, - ValidateSsm { - source: ssm::Error, - }, + #[snafu(display("Failed to validate SSM parameters: {}", source))] + ValidateSsm { source: ssm::Error }, #[snafu(display( "Failed to parse existing SSM parameters at path {:?}: {}", diff --git a/tools/pubsys/src/aws/ssm/mod.rs b/tools/pubsys/src/aws/ssm/mod.rs index c093e1f08..f21824571 100644 --- a/tools/pubsys/src/aws/ssm/mod.rs +++ b/tools/pubsys/src/aws/ssm/mod.rs @@ -443,9 +443,7 @@ mod error { #[snafu(visibility(pub(super)))] pub(crate) enum Error { #[snafu(display("Error reading config: {}", source))] - Config { - source: pubsys_config::Error, - }, + Config { source: pubsys_config::Error }, #[snafu(display( "Failed to check whether AMI {} in {} was public: {}", @@ -466,9 +464,7 @@ mod error { }, #[snafu(display("Failed to fetch parameters from SSM: {}", source))] - FetchSsm { - source: ssm::Error, - }, + FetchSsm { source: ssm::Error }, #[snafu(display("Failed to {} '{}': {}", op, path.display(), source))] File { @@ -478,19 +474,13 @@ mod error { }, #[snafu(display("Failed to find templates: {}", source))] - FindTemplates { - source: template::Error, - }, + FindTemplates { source: template::Error }, #[snafu(display("Input '{}' is empty", path.display()))] - Input { - path: PathBuf, - }, + Input { path: PathBuf }, #[snafu(display("Infra.toml is missing {}", missing))] - MissingConfig { - missing: String, - }, + MissingConfig { missing: String }, #[snafu(display("Cowardly refusing to overwrite parameters without ALLOW_CLOBBER"))] NoClobber, @@ -499,31 +489,22 @@ mod error { NoPrivateImages, #[snafu(display("Failed to render templates: {}", source))] - RenderTemplates { - source: template::Error, - }, + RenderTemplates { source: template::Error }, #[snafu(display("Failed to set SSM parameters: {}", source))] - SetSsm { - source: ssm::Error, - }, + SetSsm { source: ssm::Error }, #[snafu(display( "Given region(s) in Infra.toml / regions argument that are not in --ami-input file: {}", regions.join(", ") ))] - UnknownRegions { - regions: Vec, - }, + UnknownRegions { regions: Vec }, - ValidateSsm { - source: ssm::Error, - }, + #[snafu(display("Failed to validate SSM parameters: {}", source))] + ValidateSsm { source: ssm::Error }, #[snafu(display("Failed to parse rendered SSM parameters to JSON: {}", source))] - ParseRenderedSsmParameters { - source: serde_json::Error, - }, + ParseRenderedSsmParameters { source: serde_json::Error }, #[snafu(display("Failed to write rendered SSM parameters to {:#?}: {}", path, source))] WriteRenderedSsmParameters { diff --git a/tools/pubsys/src/aws/ssm/ssm.rs b/tools/pubsys/src/aws/ssm/ssm.rs index 37fd8b4d1..a1bd5c4d8 100644 --- a/tools/pubsys/src/aws/ssm/ssm.rs +++ b/tools/pubsys/src/aws/ssm/ssm.rs @@ -1,6 +1,7 @@ //! The ssm module owns the getting and setting of parameters in SSM. use super::{SsmKey, SsmParameters}; +use async_stream::stream; use aws_sdk_ssm::error::{ProvideErrorMetadata, SdkError}; use aws_sdk_ssm::operation::{ get_parameters::{GetParametersError, GetParametersOutput}, @@ -8,16 +9,66 @@ use aws_sdk_ssm::operation::{ }; use aws_sdk_ssm::{config::Region, types::ParameterType, Client as SsmClient}; use futures::future::{join, ready}; +use futures::pin_mut; use futures::stream::{self, FuturesUnordered, StreamExt}; +use governor::Jitter; +use governor::{ + clock::DefaultClock, middleware::NoOpMiddleware, state::keyed::DefaultKeyedStateStore, Quota, + RateLimiter, +}; +use lazy_static::lazy_static; use log::{debug, error, info, trace, warn}; +use nonzero_ext::nonzero; use snafu::{ensure, OptionExt, ResultExt}; use std::collections::{HashMap, HashSet}; use std::time::Duration; +use tokio_retry::{ + strategy::{jitter, ExponentialBackoff}, + RetryIf, +}; + +// SSM validation may retry if it fails for any reason. +// These parameters control the exponential backoff and number of retries. +const SSM_VALIDATION_RETRY_EXP_BASE_MILLIS: u64 = 2_000; +const SSM_VALIDATION_NUM_RETRIES: usize = 3; + +// Configures the rate limit used for SSM parameter fetching. +// SSM service quotas are provided on https://docs.aws.amazon.com/general/latest/gr/ssm.html +// This rate limiter applies to SSM: +// * GetParameter +// * GetParameters +// * GetParametersByPath +const GET_PARAMETERS_RATE_LIMIT_PER_SEC: u32 = 40; +// Configures the maximum "token bucket" size for the SSM rate limiter. +const GET_PARAMETERS_BURST_LIMIT: u32 = 20; +type RegionKeyRateLimiter = + RateLimiter, DefaultClock, NoOpMiddleware>; + +lazy_static! { + static ref GET_PARAMETERS_MAX_JITTER: Jitter = Jitter::up_to(Duration::from_millis(20)); + static ref GET_PARAMETERS_RATE_LIMITER: RegionKeyRateLimiter = RateLimiter::keyed( + Quota::per_second(nonzero!(GET_PARAMETERS_RATE_LIMIT_PER_SEC)) + .allow_burst(nonzero!(GET_PARAMETERS_BURST_LIMIT)) + ); +} + +/// Async throttling function to be called before calling SSM GetParameter* functions. +/// +/// Returns when the rate limit is satisfied. +pub async fn rate_limit_ssm_get_parameters(region: &Region) { + if let Err(e) = GET_PARAMETERS_RATE_LIMITER.check_key(region) { + debug!( + "SSM GetParameters in '{}' rate-limited until {:?}", + region, + e.earliest_possible() + ); + GET_PARAMETERS_RATE_LIMITER + .until_key_ready_with_jitter(region, *GET_PARAMETERS_MAX_JITTER) + .await; + } +} /// Fetches the values of the given SSM keys using the given clients -// TODO: We can batch GET requests so throttling is less likely here, but if we need to handle -// hundreds of parameters for a given build, we could use the throttling logic from -// `set_parameters` pub(crate) async fn get_parameters( requested: &[K], clients: &HashMap, @@ -39,13 +90,22 @@ where for (region, names) in regional_names { // At most 10 parameters can be requested at a time. for names_chunk in names.chunks(10) { - trace!("Requesting {:?} in {}", names_chunk, region); let ssm_client = &clients[®ion]; let len = names_chunk.len(); - let get_future = ssm_client - .get_parameters() - .set_names((!names_chunk.is_empty()).then_some(names_chunk.to_vec().clone())) - .send(); + + let get_future = { + let names_chunk = names_chunk.to_vec(); + let region = region.clone(); + async move { + rate_limit_ssm_get_parameters(®ion).await; + trace!("Requesting {:?} in {}", names_chunk, region); + ssm_client + .get_parameters() + .set_names((!names_chunk.is_empty()).then_some(names_chunk)) + .send() + .await + } + }; // Store the region so we can include it in errors and the output map let info_future = ready((region.clone(), len)); @@ -169,16 +229,24 @@ pub(crate) async fn get_parameters_by_prefix_in_region( info!("Retrieving SSM parameters in {}", region.to_string()); let mut parameters = HashMap::new(); - // Send the request - let mut get_future = client - .get_parameters_by_path() - .path(ssm_prefix) - .recursive(true) - .into_paginator() - .send(); + let paginated_response_stream = stream! { + let mut paginated_request = client + .get_parameters_by_path() + .path(ssm_prefix) + .recursive(true) + .into_paginator() + .send(); + while let Some(page) = { + rate_limit_ssm_get_parameters(region).await; + paginated_request.next().await + } { + yield page; + } + }; + pin_mut!(paginated_response_stream); // Iterate over the retrieved parameters - while let Some(page) = get_future.next().await { + while let Some(page) = paginated_response_stream.next().await { let retrieved_parameters = page .context(error::GetParametersByPathSnafu { path: ssm_prefix, @@ -371,9 +439,38 @@ pub(crate) async fn set_parameters( } /// Fetch the given parameters, and ensure the live values match the given values +/// +/// Retries validation up to 3 times on any failure, using exponential backoff. pub(crate) async fn validate_parameters( expected_parameters: &SsmParameters, ssm_clients: &HashMap, +) -> Result<()> { + let retry_strategy = ExponentialBackoff::from_millis(SSM_VALIDATION_RETRY_EXP_BASE_MILLIS) + .map(jitter) + .enumerate() + .map(|(attempt, d)| { + if attempt > 0 { + error!("Retrying: attempt = {}", attempt + 1,); + } + d + }) + .take(SSM_VALIDATION_NUM_RETRIES); + + RetryIf::spawn( + retry_strategy, + || async { validate_parameters_inner(expected_parameters, ssm_clients).await }, + |e: &'_ Error| { + error!("Failed to validate SSM parameters: {}", e); + true + }, + ) + .await +} + +/// Fetch the given parameters, and ensure the live values match the given values +async fn validate_parameters_inner( + expected_parameters: &SsmParameters, + ssm_clients: &HashMap, ) -> Result<()> { // Fetch the given parameter names let expected_parameter_names: Vec<&SsmKey> = expected_parameters.keys().collect(); diff --git a/tools/testsys-config/Cargo.toml b/tools/testsys-config/Cargo.toml index 4e7da6337..2189c1df8 100644 --- a/tools/testsys-config/Cargo.toml +++ b/tools/testsys-config/Cargo.toml @@ -7,14 +7,14 @@ edition = "2021" publish = false [dependencies] -bottlerocket-types = { git = "https://github.com/bottlerocket-os/bottlerocket-test-system", version = "0.0.14", tag = "v0.0.14" } -bottlerocket-variant = { version = "0.1", path = "../bottlerocket-variant" } -handlebars = "5" -log = "0.4" -maplit = "1" -testsys-model = { git = "https://github.com/bottlerocket-os/bottlerocket-test-system", version = "0.0.14", tag = "v0.0.14" } -serde = { version = "1", features = ["derive"] } -serde_plain = "1" -serde_yaml = "0.9" -snafu = "0.8" -toml = "0.8" +bottlerocket-types.workspace = true +bottlerocket-variant.workspace = true +handlebars.workspace = true +log.workspace = true +maplit.workspace = true +testsys-model.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_plain.workspace = true +serde_yaml.workspace = true +snafu.workspace = true +toml.workspace = true diff --git a/tools/testsys/Cargo.toml b/tools/testsys/Cargo.toml index 897662c4b..51834b590 100644 --- a/tools/testsys/Cargo.toml +++ b/tools/testsys/Cargo.toml @@ -10,28 +10,28 @@ edition = "2021" publish = false [dependencies] -async-trait = "0.1" -aws-config = "1" -aws-sdk-ec2 = "1" -base64 = "0.22" -bottlerocket-types = { git = "https://github.com/bottlerocket-os/bottlerocket-test-system", version = "0.0.14", tag = "v0.0.14" } -bottlerocket-variant = { version = "0.1", path = "../bottlerocket-variant" } -clap = { version = "4", features = ["derive", "env"] } -env_logger = "0.11" -futures = "0.3" -handlebars = "5" -log = "0.4" -maplit = "1" -testsys-model = { git = "https://github.com/bottlerocket-os/bottlerocket-test-system", version = "0.0.14", tag = "v0.0.14" } -pubsys-config = { path = "../pubsys-config/", version = "0.1.0" } -fastrand = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_plain = "1" -serde_yaml = "0.9" -snafu = "0.8" -term_size = "0.3" -testsys-config = { path = "../testsys-config/", version = "0.1" } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } -unescape = "0.1" -url = "2" +async-trait.workspace = true +aws-config.workspace = true +aws-sdk-ec2.workspace = true +base64.workspace = true +bottlerocket-types.workspace = true +bottlerocket-variant.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +env_logger.workspace = true +futures.workspace = true +handlebars.workspace = true +log.workspace = true +maplit.workspace = true +testsys-model.workspace = true +pubsys-config.workspace = true +fastrand.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +serde_plain.workspace = true +serde_yaml.workspace = true +snafu.workspace = true +term_size.workspace = true +testsys-config.workspace = true +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread"] } +unescape.workspace = true +url.workspace = true diff --git a/tools/unplug/Cargo.toml b/tools/unplug/Cargo.toml index 2870ba87e..b07cd7d7f 100644 --- a/tools/unplug/Cargo.toml +++ b/tools/unplug/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" publish = false [dependencies] -anyhow = "1.0.75" +anyhow.workspace = true [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.148" diff --git a/tools/update-metadata/Cargo.toml b/tools/update-metadata/Cargo.toml index 1874049cc..edadcb214 100644 --- a/tools/update-metadata/Cargo.toml +++ b/tools/update-metadata/Cargo.toml @@ -9,13 +9,13 @@ publish = false exclude = ["README.md"] [dependencies] -chrono = { version = "0.4", default-features = false, features = ["std", "serde", "clock"] } -parse-datetime = { path = "../parse-datetime", version = "0.1" } -regex = "1" -semver = { version = "1", features = ["serde"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_plain = "1" -snafu = "0.8" -toml = "0.8" +chrono = { workspace = true, features = ["clock", "serde", "std"] } +parse-datetime.workspace = true +regex.workspace = true +semver = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +serde_plain.workspace = true +snafu.workspace = true +toml.workspace = true diff --git a/twoliter/Cargo.toml b/twoliter/Cargo.toml index 7a19e42f7..387dc1668 100644 --- a/twoliter/Cargo.toml +++ b/twoliter/Cargo.toml @@ -10,43 +10,43 @@ keywords = ["twoliter", "bottlerocket"] exclude = ["/design", "/target", "/dockerfiles", "/scripts"] [dependencies] -anyhow = "1" -async-recursion = "1" -async-walkdir = "1" -base64 = "0.22" -buildsys-config = { version = "0.1", path = "../tools/buildsys-config" } -clap = { version = "4", features = ["derive", "env", "std"] } -env_logger = "0.11" -filetime = "0.2" -flate2 = "1" -futures= "0.3" -log = "0.4" -oci-cli-wrapper = { version = "0.1", path = "../tools/oci-cli-wrapper" } -olpc-cjson = "0.1" -semver = { version = "1", features = ["serde"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -sha2 = "0.10" -tar = "0.4" -tempfile = "3" -tokio = { version = "1", default-features = false, features = ["fs", "macros", "process", "rt-multi-thread"] } -toml = "0.8" -tracing = { version = "0.1", features = ["log"] } -uuid = { version = "1", features = [ "v4" ] } +anyhow.workspace = true +async-recursion.workspace = true +async-walkdir.workspace = true +base64.workspace = true +buildsys-config.workspace = true +clap = { workspace = true, features = ["derive", "env", "std"] } +env_logger.workspace = true +filetime.workspace = true +flate2.workspace = true +futures.workspace = true +log.workspace = true +oci-cli-wrapper.workspace = true +olpc-cjson.workspace = true +semver = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sha2.workspace = true +tar.workspace = true +tempfile.workspace = true +tokio = { workspace = true, features = ["fs", "macros", "process", "rt-multi-thread"] } +toml.workspace = true +tracing = { workspace = true, features = ["log"] } +uuid = { workspace = true, features = ["v4"] } -# Binary dependencies. These are binaries that we want to embed in the Twoliter binary. -buildsys = { version = "0.1.0", artifact = [ "bin:buildsys", "bin:bottlerocket-variant" ], path = "../tools/buildsys" } -pipesys = { version = "0.1.0", artifact = [ "bin:pipesys" ], path = "../tools/pipesys" } -pubsys = { version = "0.1.0", artifact = [ "bin:pubsys" ], path = "../tools/pubsys" } -pubsys-setup = { version = "0.1.0", artifact = [ "bin:pubsys-setup" ], path = "../tools/pubsys-setup" } -testsys = { version = "0.1.0", artifact = [ "bin:testsys" ], path = "../tools/testsys" } -tuftool = { version = "0.10", artifact = [ "bin:tuftool" ] } -unplug = { version = "0.1.0", artifact = [ "bin:unplug" ], path = "../tools/unplug" } +# Binary dependencies. These are binaries that we want to embed in the Twoliter binary +buildsys = { workspace = true } +pipesys = { workspace = true } +pubsys = { workspace = true } +pubsys-setup = { workspace = true } +testsys = { workspace = true } +tuftool = { workspace = true } +unplug = { workspace = true } [build-dependencies] -bytes = "1" -flate2 = "1" -tar = "0.4" +bytes.workspace = true +flate2.workspace = true +tar.workspace = true [features] default = ["integ-tests"]