From a28baa95e57a1524a27dc0e17a69088502279f71 Mon Sep 17 00:00:00 2001 From: "Sean P. Kelly" Date: Fri, 16 Dec 2022 05:35:47 +0000 Subject: [PATCH 1/3] pubsys-config: fix clippy warnings --- tools/pubsys-config/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/pubsys-config/src/lib.rs b/tools/pubsys-config/src/lib.rs index 7feccd31e6e..751e2b58daa 100644 --- a/tools/pubsys-config/src/lib.rs +++ b/tools/pubsys-config/src/lib.rs @@ -65,15 +65,15 @@ impl InfraConfig { /// Deserializes an InfraConfig from Infra.lock, if it exists, otherwise uses Infra.toml /// If the default flag is true, will create a default config if Infra.toml doesn't exist pub fn from_path_or_lock(path: &Path, default: bool) -> Result { - let lock_path = Self::compute_lock_path(&path)?; + let lock_path = Self::compute_lock_path(path)?; if lock_path.exists() { info!("Found infra config at path: {}", lock_path.display()); Self::from_lock_path(lock_path) } else if default { - Self::from_path_or_default(&path) + Self::from_path_or_default(path) } else { info!("Found infra config at path: {}", path.display()); - Self::from_path(&path) + Self::from_path(path) } } From 0e50626a926d8a3260a30bbce2d7b034f1e77a0a Mon Sep 17 00:00:00 2001 From: "Sean P. Kelly" Date: Fri, 16 Dec 2022 05:39:37 +0000 Subject: [PATCH 2/3] pubsys: fix clippy warnings --- tools/pubsys/src/aws/ssm/template.rs | 2 +- tools/pubsys/src/repo.rs | 2 +- tools/pubsys/src/repo/refresh_repo/mod.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/pubsys/src/aws/ssm/template.rs b/tools/pubsys/src/aws/ssm/template.rs index 17a0e08ba2e..d469bae9e58 100644 --- a/tools/pubsys/src/aws/ssm/template.rs +++ b/tools/pubsys/src/aws/ssm/template.rs @@ -40,7 +40,7 @@ pub(crate) fn get_parameters( template_path: &Path, build_context: &BuildContext<'_>, ) -> Result { - let templates_str = fs::read_to_string(&template_path).context(error::FileSnafu { + let templates_str = fs::read_to_string(template_path).context(error::FileSnafu { op: "read", path: &template_path, })?; diff --git a/tools/pubsys/src/repo.rs b/tools/pubsys/src/repo.rs index 730ecdf235e..efadccf4ed4 100644 --- a/tools/pubsys/src/repo.rs +++ b/tools/pubsys/src/repo.rs @@ -244,7 +244,7 @@ where for target_path in targets { debug!("Adding target from path: {}", target_path.display()); editor - .add_target_path(&target_path) + .add_target_path(target_path) .context(error::AddTargetSnafu { path: &target_path })?; } diff --git a/tools/pubsys/src/repo/refresh_repo/mod.rs b/tools/pubsys/src/repo/refresh_repo/mod.rs index be0ebbd52c1..70024a23d14 100644 --- a/tools/pubsys/src/repo/refresh_repo/mod.rs +++ b/tools/pubsys/src/repo/refresh_repo/mod.rs @@ -97,7 +97,7 @@ fn refresh_repo( .context(repo_error::RepoLoadSnafu { metadata_base_url: metadata_url.clone(), })?; - let mut repo_editor = RepositoryEditor::from_repo(&root_role_path, repo) + let mut repo_editor = RepositoryEditor::from_repo(root_role_path, repo) .context(repo_error::EditorFromRepoSnafu)?; info!("Loaded TUF repo: {}", metadata_url); @@ -114,11 +114,11 @@ fn refresh_repo( // Write out the metadata files for the repository info!("Writing repo metadata to: {}", metadata_out_dir.display()); - fs::create_dir_all(&metadata_out_dir).context(repo_error::CreateDirSnafu { + fs::create_dir_all(metadata_out_dir).context(repo_error::CreateDirSnafu { path: &metadata_out_dir, })?; signed_repo - .write(&metadata_out_dir) + .write(metadata_out_dir) .context(repo_error::RepoWriteSnafu { path: &metadata_out_dir, })?; From a89fb03eeac6163d4a0cf538fc53901b3c163489 Mon Sep 17 00:00:00 2001 From: "Sean P. Kelly" Date: Wed, 21 Dec 2022 03:39:37 +0000 Subject: [PATCH 3/3] pubsys: disallow private AMIs in public SSM params --- tools/Cargo.lock | 79 +++++++++++++ tools/deny.toml | 2 +- tools/pubsys-config/src/lib.rs | 6 +- tools/pubsys/Cargo.toml | 2 + tools/pubsys/src/aws/ami/mod.rs | 3 +- tools/pubsys/src/aws/ami/public.rs | 64 +++++++++++ tools/pubsys/src/aws/ssm/mod.rs | 165 ++++++++++++++++++++++++--- tools/pubsys/src/aws/ssm/template.rs | 31 ++++- 8 files changed, 325 insertions(+), 27 deletions(-) create mode 100644 tools/pubsys/src/aws/ami/public.rs diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 62518f1675b..46f782633c9 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -976,6 +976,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "digest" version = "0.10.5" @@ -1175,6 +1188,12 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.24" @@ -1241,6 +1260,24 @@ dependencies = [ "regex", ] +[[package]] +name = "governor" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "quanta", + "rand", + "smallvec", +] + [[package]] name = "h2" version = "0.3.14" @@ -1735,6 +1772,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "maplit" version = "1.0.2" @@ -1852,6 +1898,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nonzero_ext" version = "0.3.0" @@ -2212,10 +2264,12 @@ dependencies = [ "coldsnap", "duct", "futures", + "governor", "http", "indicatif", "lazy_static", "log", + "nonzero_ext", "num_cpus", "parse-datetime", "pubsys-config", @@ -2273,6 +2327,22 @@ dependencies = [ "url", ] +[[package]] +name = "quanta" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.21" @@ -2312,6 +2382,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "10.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb" +dependencies = [ + "bitflags", +] + [[package]] name = "rayon" version = "1.5.3" diff --git a/tools/deny.toml b/tools/deny.toml index e8551a4df39..1e95313db43 100644 --- a/tools/deny.toml +++ b/tools/deny.toml @@ -12,7 +12,7 @@ confidence-threshold = 0.93 # Commented license types are allowed but not currently used allow = [ "Apache-2.0", - # "BSD-2-Clause", + "BSD-2-Clause", "BSD-3-Clause", "BSL-1.0", # "CC0-1.0", diff --git a/tools/pubsys-config/src/lib.rs b/tools/pubsys-config/src/lib.rs index 751e2b58daa..8b244977e3e 100644 --- a/tools/pubsys-config/src/lib.rs +++ b/tools/pubsys-config/src/lib.rs @@ -105,7 +105,7 @@ impl InfraConfig { } /// S3-specific TUF infrastructure configuration -#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct S3Config { pub region: Option, #[serde(default)] @@ -116,7 +116,7 @@ pub struct S3Config { } /// AWS-specific infrastructure configuration -#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct AwsConfig { #[serde(default)] @@ -130,7 +130,7 @@ pub struct AwsConfig { } /// AWS region-specific configuration -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct AwsRegionConfig { pub role: Option, diff --git a/tools/pubsys/Cargo.toml b/tools/pubsys/Cargo.toml index 05392b55d92..b44d5308c7b 100644 --- a/tools/pubsys/Cargo.toml +++ b/tools/pubsys/Cargo.toml @@ -22,10 +22,12 @@ clap = "3.1" coldsnap = { version = "0.4", default-features = false, features = ["aws-sdk-rust-rustls"] } duct = "0.13.0" futures = "0.3.5" +governor = "0.5" http = "0.2.8" indicatif = "0.17.1" lazy_static = "1.4" log = "0.4" +nonzero_ext = "0.3" num_cpus = "1" parse-datetime = { path = "../../sources/parse-datetime", version = "0.1.0" } pubsys-config = { path = "../pubsys-config/", version = "0.1.0" } diff --git a/tools/pubsys/src/aws/ami/mod.rs b/tools/pubsys/src/aws/ami/mod.rs index b6099d00461..23a26792a3a 100644 --- a/tools/pubsys/src/aws/ami/mod.rs +++ b/tools/pubsys/src/aws/ami/mod.rs @@ -1,6 +1,7 @@ //! The ami module owns the 'ami' subcommand and controls the process of registering and copying //! EC2 AMIs. +pub(crate) mod public; mod register; mod snapshot; pub(crate) mod wait; @@ -370,7 +371,7 @@ async fn _run(args: &Args, ami_args: &AmiArgs) -> Result> /// If JSON output was requested, we serialize out a mapping of region to AMI information; this /// struct holds the information we save about each AMI. The `ssm` subcommand uses this /// information to populate templates representing SSM parameter names and values. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)] pub(crate) struct Image { pub(crate) id: String, pub(crate) name: String, diff --git a/tools/pubsys/src/aws/ami/public.rs b/tools/pubsys/src/aws/ami/public.rs new file mode 100644 index 00000000000..a29af11d108 --- /dev/null +++ b/tools/pubsys/src/aws/ami/public.rs @@ -0,0 +1,64 @@ +use aws_sdk_ec2::Client as Ec2Client; +use snafu::{ensure, OptionExt, ResultExt}; + +/// Returns whether or not the given AMI ID refers to a public AMI. +pub(crate) async fn ami_is_public( + ec2_client: &Ec2Client, + region: &str, + ami_id: &str, +) -> Result { + let ec2_response = ec2_client + .describe_images() + .image_ids(ami_id.to_string()) + .send() + .await + .context(error::DescribeImagesSnafu { + ami_id: ami_id.to_string(), + region: region.to_string(), + })?; + + let returned_images = ec2_response.images().unwrap_or_default(); + + ensure!( + returned_images.len() <= 1, + error::TooManyImagesSnafu { + ami_id: ami_id.to_string(), + region: region.to_string(), + } + ); + + Ok(returned_images + .first() + .context(error::NoSuchImageSnafu { + ami_id: ami_id.to_string(), + region: region.to_string(), + })? + .public() + .unwrap_or(false)) +} + +mod error { + use aws_sdk_ec2::error::DescribeImagesError; + use aws_sdk_ec2::types::SdkError; + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(crate) enum Error { + #[snafu(display("Error describing AMI {} in {}: {}", ami_id, region, source))] + DescribeImages { + ami_id: String, + region: String, + #[snafu(source(from(SdkError, Box::new)))] + source: Box>, + }, + + #[snafu(display("AMI {} not found in {}", ami_id, region))] + NoSuchImage { ami_id: String, region: String }, + + #[snafu(display("Multiples AMIs with ID {} found in {}", ami_id, region))] + TooManyImages { ami_id: String, region: String }, + } +} +pub(crate) use error::Error; +type Result = std::result::Result; diff --git a/tools/pubsys/src/aws/ssm/mod.rs b/tools/pubsys/src/aws/ssm/mod.rs index 8c5afd6803f..6f3d5e2ac5c 100644 --- a/tools/pubsys/src/aws/ssm/mod.rs +++ b/tools/pubsys/src/aws/ssm/mod.rs @@ -5,18 +5,28 @@ pub(crate) mod ssm; pub(crate) mod template; -use crate::aws::{ami::Image, client::build_client_config, parse_arch, region_from_string}; +use self::template::RenderedParameter; +use crate::aws::{ + ami::public::ami_is_public, ami::Image, client::build_client_config, parse_arch, + region_from_string, +}; use crate::Args; -use aws_sdk_ec2::model::ArchitectureValues; +use aws_config::SdkConfig; +use aws_sdk_ec2::{model::ArchitectureValues, Client as Ec2Client}; use aws_sdk_ssm::{Client as SsmClient, Region}; -use log::{info, trace}; +use futures::stream::{StreamExt, TryStreamExt}; +use governor::{prelude::*, Quota, RateLimiter}; +use log::{error, info, trace}; +use nonzero_ext::nonzero; use pubsys_config::InfraConfig; use serde::Serialize; use snafu::{ensure, OptionExt, ResultExt}; -use std::collections::{HashMap, HashSet}; -use std::fs::File; use std::iter::FromIterator; use std::path::PathBuf; +use std::{ + collections::{HashMap, HashSet}, + fs::File, +}; use structopt::{clap, StructOpt}; /// Sets SSM parameters based on current build information @@ -51,6 +61,17 @@ pub(crate) struct SsmArgs { /// Allows overwrite of existing parameters #[structopt(long)] allow_clobber: bool, + + /// Allows publishing non-public images to the `/aws/` namespace + #[structopt(long)] + allow_private_images: bool, +} + +/// Wrapper struct over parameter update and AWS clients needed to execute on it. +#[derive(Debug, Clone)] +struct SsmParamUpdateOp { + parameter: RenderedParameter, + ec2_client: Ec2Client, } /// Common entrypoint from main() @@ -80,13 +101,6 @@ pub(crate) async fn run(args: &Args, ssm_args: &SsmArgs) -> Result<()> { let amis = parse_ami_input(®ions, ssm_args)?; - let mut ssm_clients = HashMap::with_capacity(amis.len()); - for region in amis.keys() { - let client_config = build_client_config(region, &base_region, &aws).await; - let ssm_client = SsmClient::new(&client_config); - ssm_clients.insert(region.clone(), ssm_client); - } - // Template setup =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= // Non-image-specific context for building and rendering templates @@ -112,21 +126,64 @@ pub(crate) async fn run(args: &Args, ssm_args: &SsmArgs) -> Result<()> { } let new_parameters = - template::render_parameters(template_parameters, amis, ssm_prefix, &build_context) + template::render_parameters(template_parameters, &amis, ssm_prefix, &build_context) .context(error::RenderTemplatesSnafu)?; trace!("Generated templated parameters: {:#?}", new_parameters); + // Generate AWS Clients to use for the updates. + let mut param_update_ops: Vec = Vec::with_capacity(new_parameters.len()); + let mut aws_sdk_configs: HashMap = HashMap::with_capacity(regions.len()); + let mut ssm_clients = HashMap::with_capacity(amis.len()); + + for parameter in new_parameters.iter() { + let region = ¶meter.ssm_key.region; + // Store client configs so that we only have to create them once. + // The HashMap `entry` API doesn't play well with `async`, so we use a match here instead. + let client_config = match aws_sdk_configs.get(region) { + Some(client_config) => client_config.clone(), + None => { + let client_config = build_client_config(region, &base_region, &aws).await; + aws_sdk_configs.insert(region.clone(), client_config.clone()); + client_config + } + }; + + let ssm_client = SsmClient::new(&client_config); + if ssm_clients.get(region).is_none() { + ssm_clients.insert(region.clone(), ssm_client); + } + + let ec2_client = Ec2Client::new(&client_config); + param_update_ops.push(SsmParamUpdateOp { + parameter: parameter.clone(), + ec2_client, + }); + } + + // Unless overridden, only allow public images to be published to public parameters. + if !ssm_args.allow_private_images { + info!("Ensuring that only public images are published to public parameters."); + ensure!( + check_public_namespace_amis_are_public(param_update_ops.iter()).await?, + error::NoPrivateImagesSnafu + ); + } + // SSM get/compare =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= info!("Getting current SSM parameters"); - let new_parameter_names: Vec<&SsmKey> = new_parameters.keys().collect(); + let new_parameter_names: Vec<&SsmKey> = + new_parameters.iter().map(|param| ¶m.ssm_key).collect(); let current_parameters = ssm::get_parameters(&new_parameter_names, &ssm_clients) .await .context(error::FetchSsmSnafu)?; trace!("Current SSM parameters: {:#?}", current_parameters); // Show the difference between source and target parameters in SSM. - let parameters_to_set = key_difference(&new_parameters, ¤t_parameters); + let parameters_to_set = key_difference( + &RenderedParameter::as_ssm_parameters(&new_parameters), + ¤t_parameters, + ); if parameters_to_set.is_empty() { info!("No changes necessary."); return Ok(()); @@ -156,8 +213,60 @@ pub(crate) async fn run(args: &Args, ssm_args: &SsmArgs) -> Result<()> { Ok(()) } +// Rate limits on the EC2 side use the TokenBucket method, and buckets refill at a rate of 20 tokens per second. +// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/throttling.html#throttling-rate-based for more details. +const DESCRIBE_IMAGES_RATE_LIMIT: Quota = Quota::per_second(nonzero!(20u32)); +const MAX_CONCURRENT_AMI_CHECKS: usize = 8; + +/// Given a set of SSM parameter updates, ensures all parameters in the public namespace refer to public AMIs. +async fn check_public_namespace_amis_are_public( + parameter_updates: impl Iterator, +) -> Result { + let public_namespace_updates = parameter_updates + .filter(|update| update.parameter.ssm_key.is_in_public_namespace()) + .cloned(); + + // Wrap `crate::aws::ami::public::ami_is_public()` in a future that returns the correct error type. + let check_ami_public = |update: SsmParamUpdateOp| async move { + let region = &update.parameter.ssm_key.region; + let ami_id = &update.parameter.ami.id; + let is_public = ami_is_public(&update.ec2_client, region.as_ref(), ami_id) + .await + .context(error::CheckAmiPublicSnafu { + ami_id: ami_id.to_string(), + region: region.to_string(), + }); + + if let Ok(false) = is_public { + error!( + "Attempted to set parameter '{}' in {} to '{}', based on AMI {}. That AMI is not marked public!", + update.parameter.ssm_key.name, region, update.parameter.value, ami_id + ); + } + + is_public + }; + + // Concurrently check our input parameter updates... + let rate_limiter = RateLimiter::direct(DESCRIBE_IMAGES_RATE_LIMIT); + let results: Vec> = futures::stream::iter(public_namespace_updates) + .ratelimit_stream(&rate_limiter) + .then(|update| async move { Ok(check_ami_public(update)) }) + .try_buffer_unordered(usize::min(num_cpus::get(), MAX_CONCURRENT_AMI_CHECKS)) + .collect() + .await; + + // `collect()` on `TryStreams` doesn't seem to happily invert a `Vec>` to a `Result>`, + // so we use the usual `Iterator` methods to do it here. + Ok(results + .into_iter() + .collect::>>()? + .into_iter() + .all(|is_public| is_public)) +} + /// The key to a unique SSM parameter -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Debug, Eq, Hash, PartialEq, Clone)] pub(crate) struct SsmKey { pub(crate) region: Region, pub(crate) name: String, @@ -167,6 +276,10 @@ impl SsmKey { pub(crate) fn new(region: Region, name: String) -> Self { Self { region, name } } + + pub(crate) fn is_in_public_namespace(&self) -> bool { + self.name.starts_with("/aws/") + } } impl AsRef for SsmKey { @@ -296,6 +409,23 @@ mod error { source: pubsys_config::Error, }, + #[snafu(display( + "Failed to check whether AMI {} in {} was public: {}", + ami_id, + region, + source + ))] + CheckAmiPublic { + ami_id: String, + region: String, + source: crate::aws::ami::public::Error, + }, + + #[snafu(display("Failed to create EC2 client for region {}", region))] + CreateEc2Client { + region: String, + }, + #[snafu(display("Failed to deserialize input from '{}': {}", path.display(), source))] Deserialize { path: PathBuf, @@ -332,6 +462,9 @@ mod error { #[snafu(display("Cowardly refusing to overwrite parameters without ALLOW_CLOBBER"))] NoClobber, + #[snafu(display("Cowardly refusing to publish private image to public namespace without ALLOW_PRIVATE_IMAGES"))] + NoPrivateImages, + #[snafu(display("Failed to render templates: {}", source))] RenderTemplates { source: template::Error, diff --git a/tools/pubsys/src/aws/ssm/template.rs b/tools/pubsys/src/aws/ssm/template.rs index d469bae9e58..981e469cd24 100644 --- a/tools/pubsys/src/aws/ssm/template.rs +++ b/tools/pubsys/src/aws/ssm/template.rs @@ -70,13 +70,31 @@ pub(crate) fn get_parameters( Ok(template_parameters) } +/// A value which stores rendered SSM parameters alongside metadata used to render their templates +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct RenderedParameter { + pub(crate) ami: Image, + pub(crate) ssm_key: SsmKey, + pub(crate) value: String, +} + +impl RenderedParameter { + /// Creates an `SsmParameters` HashMap from a list of `RenderedParameter` + pub(crate) fn as_ssm_parameters(rendered_parameters: &[RenderedParameter]) -> SsmParameters { + rendered_parameters + .iter() + .map(|param| (param.ssm_key.clone(), param.value.clone())) + .collect() + } +} + /// Render the given template parameters using the data from the given AMIs pub(crate) fn render_parameters( template_parameters: TemplateParameters, - amis: HashMap, + amis: &HashMap, ssm_prefix: &str, build_context: &BuildContext<'_>, -) -> Result { +) -> Result> { /// Values that we allow as template variables #[derive(Debug, Serialize)] struct TemplateContext<'a> { @@ -87,7 +105,7 @@ pub(crate) fn render_parameters( image_version: &'a str, region: &'a str, } - let mut new_parameters = HashMap::new(); + let mut new_parameters = Vec::new(); for (region, image) in amis { let context = TemplateContext { variant: build_context.variant, @@ -115,10 +133,11 @@ pub(crate) fn render_parameters( template: &tp.value, })?; - new_parameters.insert( - SsmKey::new(region.clone(), join_name(ssm_prefix, &name_suffix)), + new_parameters.push(RenderedParameter { + ami: image.clone(), + ssm_key: SsmKey::new(region.clone(), join_name(ssm_prefix, &name_suffix)), value, - ); + }); } }