diff --git a/Makefile.toml b/Makefile.toml index 0d63dc37e3e..5620ddc509a 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -388,6 +388,9 @@ case "${BUILDSYS_ARCH,,}" in arch="${BUILDSYS_ARCH}" ;; esac +ami_output="${BUILDSYS_OUTPUT_DIR}/${BUILDSYS_NAME_FULL}-amis.json" +ami_output_latest="${BUILDSYS_OUTPUT_DIR}/latest/${BUILDSYS_NAME_VARIANT}-amis.json" + pubsys \ --infra-config-path "${PUBLISH_INFRA_CONFIG_PATH}" \ \ @@ -401,8 +404,71 @@ pubsys \ --arch "${arch}" \ --name "${PUBLISH_AMI_NAME}" \ --description "${PUBLISH_AMI_DESCRIPTION:-${PUBLISH_AMI_NAME}}" \ + \ + --ami-output "${ami_output}" \ + \ ${NO_PROGRESS:+--no-progress} \ ${PUBLISH_REGIONS:+--regions "${PUBLISH_REGIONS}"} + +ln -snf "../${ami_output##*/}" "${ami_output_latest}" +''' +] + +[tasks.ami-public] +# Rather than depend on "build", which currently rebuilds images each run, we +# depend on publish-tools and check for the input file below to save time. +# This does mean that `cargo make ami` must be run before `cargo make ami-public`. +dependencies = ["publish-tools"] +script_runner = "bash" +script = [ +''' +set -e + +export PATH="${BUILDSYS_TOOLS_DIR}/bin:${PATH}" + +ami_input="${BUILDSYS_OUTPUT_DIR}/${BUILDSYS_NAME_FULL}-amis.json" +if [ ! -s "${ami_input}" ]; then + echo "AMI input file doesn't exist for the current version/commit - ${BUILDSYS_VERSION_FULL} - please run 'cargo make ami'" >&2 + exit 1 +fi + +pubsys \ + --infra-config-path "${PUBLISH_INFRA_CONFIG_PATH}" \ + \ + publish-ami \ + --make-public \ + \ + --ami-input "${ami_input}" \ + ${PUBLISH_REGIONS:+--regions "${PUBLISH_REGIONS}"} +''' +] + +[tasks.ami-private] +# Rather than depend on "build", which currently rebuilds images each run, we +# depend on publish-tools and check for the input file below to save time. +# This does mean that `cargo make ami` must be run before `cargo make ami-private`. +dependencies = ["publish-tools"] +script_runner = "bash" +script = [ +''' +set -e + +export PATH="${BUILDSYS_TOOLS_DIR}/bin:${PATH}" + +ami_input="${BUILDSYS_OUTPUT_DIR}/${BUILDSYS_NAME_FULL}-amis.json" +if [ ! -s "${ami_input}" ]; then + echo "AMI input file doesn't exist for the current version/commit - ${BUILDSYS_VERSION_FULL} - please run 'cargo make ami'" >&2 + exit 1 +fi + +pubsys \ + --infra-config-path "${PUBLISH_INFRA_CONFIG_PATH}" \ + \ + publish-ami \ + --make-private \ + \ + --ami-input "${ami_input}" \ + ${PUBLISH_REGIONS:+--regions "${PUBLISH_REGIONS}"} ''' ] diff --git a/tools/pubsys/src/aws/ami/mod.rs b/tools/pubsys/src/aws/ami/mod.rs index d7911734a91..1e67734e327 100644 --- a/tools/pubsys/src/aws/ami/mod.rs +++ b/tools/pubsys/src/aws/ami/mod.rs @@ -5,8 +5,8 @@ mod register; mod snapshot; mod wait; -use crate::aws::client::build_client; -use crate::config::{AwsConfig, InfraConfig}; +use crate::aws::{client::build_client, region_from_string}; +use crate::config::InfraConfig; use crate::Args; use futures::future::{join, lazy, ready, FutureExt}; use futures::stream::{self, StreamExt}; @@ -17,6 +17,7 @@ use rusoto_ebs::EbsClient; use rusoto_ec2::{CopyImageError, CopyImageRequest, CopyImageResult, Ec2, Ec2Client}; use snafu::{ensure, OptionExt, ResultExt}; use std::collections::{HashMap, VecDeque}; +use std::fs::File; use std::path::PathBuf; use structopt::StructOpt; use wait::wait_for_ami; @@ -60,10 +61,31 @@ pub(crate) struct AmiArgs { /// Regions where you want the AMI, the first will be used as the base for copying #[structopt(long, use_delimiter = true)] regions: Vec, + + /// If specified, save created regional AMI IDs in JSON at this path. + #[structopt(long)] + ami_output: Option, } /// Common entrypoint from main() pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { + match _run(args, ami_args).await { + Ok(ami_ids) => { + // Write the AMI IDs to file if requested + if let Some(ref path) = ami_args.ami_output { + let file = File::create(path).context(error::FileCreate { path })?; + serde_json::to_writer_pretty(file, &ami_ids).context(error::Serialize { path })?; + info!("Wrote AMI data to {}", path.display()); + } + Ok(()) + } + Err(e) => Err(e), + } +} + +async fn _run(args: &Args, ami_args: &AmiArgs) -> Result> { + let mut ami_ids = HashMap::new(); + info!( "Using infra config from path: {}", args.infra_config_path.display() @@ -80,7 +102,7 @@ pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { aws.regions.clone() } .into_iter() - .map(|name| region_from_string(&name, &aws)) + .map(|name| region_from_string(&name, &aws).context(error::ParseRegion)) .collect::>>()?; // We register in this base region first, then copy from there to any other regions. @@ -138,9 +160,11 @@ pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { (new_id, false) }; + ami_ids.insert(base_region.name().to_string(), image_id.clone()); + // If we don't need to copy AMIs, we're done. if regions.is_empty() { - return Ok(()); + return Ok(ami_ids); } // Wait for AMI to be available so it can be copied @@ -187,6 +211,7 @@ pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { region.name(), id ); + ami_ids.insert(region.name().to_string(), id.clone()); continue; } let request = CopyImageRequest { @@ -214,7 +239,7 @@ pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { // If all target regions already have the AMI, we're done. if copy_requests.is_empty() { - return Ok(()); + return Ok(ami_ids); } // Start requests; they return almost immediately and the copying work is done by the service @@ -234,12 +259,24 @@ pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { let mut saw_error = false; for (region, copy_response) in copy_responses { match copy_response { - Ok(success) => info!( - "Registered AMI '{}' in region {}: {}", - ami_args.name, - region.name(), - success.image_id.unwrap_or_else(|| "".to_string()) - ), + Ok(success) => { + if let Some(image_id) = success.image_id { + info!( + "Registered AMI '{}' in {}: {}", + ami_args.name, + region.name(), + image_id, + ); + ami_ids.insert(region.name().to_string(), image_id); + } else { + saw_error = true; + error!( + "Registered AMI '{}' in {} but didn't receive an AMI ID!", + ami_args.name, + region.name(), + ); + } + } Err(e) => { saw_error = true; error!("Copy to {} failed: {}", region.name(), e); @@ -249,25 +286,13 @@ pub(crate) async fn run(args: &Args, ami_args: &AmiArgs) -> Result<()> { ensure!(!saw_error, error::AmiCopy); - Ok(()) -} - -/// Builds a Region from the given region name, and uses the custom endpoint from the AWS config, -/// if specified in aws.region.REGION.endpoint. -fn region_from_string(name: &str, aws: &AwsConfig) -> Result { - let maybe_endpoint = aws.region.get(name).and_then(|r| r.endpoint.clone()); - Ok(match maybe_endpoint { - Some(endpoint) => Region::Custom { - name: name.to_string(), - endpoint, - }, - None => name.parse().context(error::ParseRegion { name })?, - }) + Ok(ami_ids) } mod error { use crate::aws::{self, ami}; use snafu::Snafu; + use std::path::PathBuf; #[derive(Debug, Snafu)] #[snafu(visibility = "pub(super)")] @@ -283,7 +308,15 @@ mod error { }, #[snafu(display("Error reading config: {}", source))] - Config { source: crate::config::Error }, + Config { + source: crate::config::Error, + }, + + #[snafu(display("Failed to create file '{}': {}", path.display(), source))] + FileCreate { + path: PathBuf, + source: std::io::Error, + }, #[snafu(display("Error getting AMI ID for {} {} in {}: {}", arch, name, region, source))] GetAmiId { @@ -294,12 +327,12 @@ mod error { }, #[snafu(display("Infra.toml is missing {}", missing))] - MissingConfig { missing: String }, + MissingConfig { + missing: String, + }, - #[snafu(display("Failed to parse region '{}': {}", name, source))] ParseRegion { - name: String, - source: rusoto_signature::region::ParseRegionError, + source: crate::aws::Error, }, #[snafu(display("Error registering {} {} in {}: {}", arch, name, region, source))] @@ -310,6 +343,12 @@ mod error { source: ami::register::Error, }, + #[snafu(display("Failed to serialize output to '{}': {}", path.display(), source))] + Serialize { + path: PathBuf, + source: serde_json::Error, + }, + #[snafu(display("AMI '{}' in {} did not become available: {}", id, region, source))] WaitAmi { id: String, diff --git a/tools/pubsys/src/aws/client.rs b/tools/pubsys/src/aws/client.rs index 304790375b3..489806219a0 100644 --- a/tools/pubsys/src/aws/client.rs +++ b/tools/pubsys/src/aws/client.rs @@ -1,5 +1,5 @@ -use async_trait::async_trait; use crate::config::AwsConfig; +use async_trait::async_trait; use rusoto_core::{request::DispatchSignedRequest, HttpClient, Region}; use rusoto_credential::{ AutoRefreshingProvider, AwsCredentials, CredentialsError, DefaultCredentialsProvider, @@ -22,9 +22,9 @@ impl NewWith for EbsClient { where P: ProvideAwsCredentials + Send + Sync + 'static, D: DispatchSignedRequest + Send + Sync + 'static, - { - Self::new_with(request_dispatcher, credentials_provider, region) - } + { + Self::new_with(request_dispatcher, credentials_provider, region) + } } impl NewWith for Ec2Client { @@ -32,9 +32,9 @@ impl NewWith for Ec2Client { where P: ProvideAwsCredentials + Send + Sync + 'static, D: DispatchSignedRequest + Send + Sync + 'static, - { - Self::new_with(request_dispatcher, credentials_provider, region) - } + { + Self::new_with(request_dispatcher, credentials_provider, region) + } } /// Create a rusoto client of the given type using the given region and configuration. diff --git a/tools/pubsys/src/aws/mod.rs b/tools/pubsys/src/aws/mod.rs index ebd757288eb..794d622e020 100644 --- a/tools/pubsys/src/aws/mod.rs +++ b/tools/pubsys/src/aws/mod.rs @@ -1,4 +1,38 @@ +use crate::config::AwsConfig; +use rusoto_core::Region; +use snafu::ResultExt; + #[macro_use] pub(crate) mod client; pub(crate) mod ami; +pub(crate) mod publish_ami; + +/// Builds a Region from the given region name, and uses the custom endpoint from the AWS config, +/// if specified in aws.region.REGION.endpoint. +fn region_from_string(name: &str, aws: &AwsConfig) -> Result { + let maybe_endpoint = aws.region.get(name).and_then(|r| r.endpoint.clone()); + Ok(match maybe_endpoint { + Some(endpoint) => Region::Custom { + name: name.to_string(), + endpoint, + }, + None => name.parse().context(error::ParseRegion { name })?, + }) +} + +mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility = "pub(super)")] + pub(crate) enum Error { + #[snafu(display("Failed to parse region '{}': {}", name, source))] + ParseRegion { + name: String, + source: rusoto_signature::region::ParseRegionError, + }, + } +} +pub(crate) use error::Error; +type Result = std::result::Result; diff --git a/tools/pubsys/src/aws/publish_ami/mod.rs b/tools/pubsys/src/aws/publish_ami/mod.rs new file mode 100644 index 00000000000..7b92224a2a3 --- /dev/null +++ b/tools/pubsys/src/aws/publish_ami/mod.rs @@ -0,0 +1,459 @@ +//! The publish_ami module owns the 'publish-ami' subcommand and controls the process of granting +//! and revoking public access to EC2 AMIs. + +use crate::aws::{client::build_client, region_from_string}; +use crate::config::InfraConfig; +use crate::Args; +use futures::future::{join, ready}; +use futures::stream::{self, StreamExt}; +use log::{debug, error, info, trace}; +use rusoto_core::{Region, RusotoError}; +use rusoto_ec2::{ + DescribeImagesError, DescribeImagesRequest, DescribeImagesResult, Ec2, Ec2Client, + ModifyImageAttributeError, ModifyImageAttributeRequest, ModifySnapshotAttributeError, + ModifySnapshotAttributeRequest, +}; +use snafu::{ensure, OptionExt, ResultExt}; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::iter::FromIterator; +use std::path::PathBuf; +use structopt::StructOpt; + +/// Grants or revokes permissions to Bottlerocket AMIs +#[derive(Debug, StructOpt)] +#[structopt(setting = clap::AppSettings::DeriveDisplayOrder)] +#[structopt(group = clap::ArgGroup::with_name("mode").required(true).multiple(false))] +pub(crate) struct PublishArgs { + /// Path to the JSON file containing regional AMI IDs to modify + #[structopt(long)] + ami_input: PathBuf, + + /// Comma-separated list of regions to publish in, overriding Infra.toml; given regions must be + /// in the --ami-input file + #[structopt(long, use_delimiter = true)] + regions: Vec, + + /// Make the AMIs public + #[structopt(long, group = "mode")] + make_public: bool, + /// Make the AMIs private + #[structopt(long, group = "mode")] + make_private: bool, +} + +/// Common entrypoint from main() +pub(crate) async fn run(args: &Args, publish_args: &PublishArgs) -> Result<()> { + let (operation, mode) = if publish_args.make_public { + ("add".to_string(), "public") + } else if publish_args.make_private { + ("remove".to_string(), "private") + } else { + unreachable!("developer error: make-public and make-private not required/exclusive"); + }; + + info!( + "Using AMI data from path: {}", + publish_args.ami_input.display() + ); + let file = File::open(&publish_args.ami_input).context(error::File { + op: "open", + path: &publish_args.ami_input, + })?; + let mut ami_input: HashMap = + serde_json::from_reader(file).context(error::Deserialize { + path: &publish_args.ami_input, + })?; + trace!("Parsed AMI input: {:?}", ami_input); + + // pubsys will not create a file if it did not create AMIs, so we should only have an empty + // file if a user created one manually, and they shouldn't be creating an empty file. + ensure!( + !ami_input.is_empty(), + error::Input { + path: &publish_args.ami_input + } + ); + + info!( + "Using infra config from path: {}", + args.infra_config_path.display() + ); + let infra_config = InfraConfig::from_path(&args.infra_config_path).context(error::Config)?; + trace!("Parsed infra config: {:?}", infra_config); + + let aws = infra_config.aws.unwrap_or_else(Default::default); + + // If the user gave an override list of regions, use that, otherwise use what's in the config. + let regions = if !publish_args.regions.is_empty() { + publish_args.regions.clone() + } else { + aws.regions.clone().into() + }; + // Check that the requested regions are a subset of the regions we *could* publish from the AMI + // input JSON. + let requested_regions = HashSet::from_iter(regions.iter()); + let known_regions = HashSet::<&String>::from_iter(ami_input.keys()); + ensure!( + requested_regions.is_subset(&known_regions), + error::UnknownRegions { + regions: requested_regions + .difference(&known_regions) + .map(|s| s.to_string()) + .collect::>(), + } + ); + + // Parse region names, adding endpoints from InfraConfig if specified + let mut amis = HashMap::with_capacity(regions.len()); + for name in regions { + let ami_id = ami_input + .remove(&name) + // This could only happen if someone removes the check above... + .with_context(|| error::UnknownRegions { + regions: vec![name.clone()], + })?; + let region = region_from_string(&name, &aws).context(error::ParseRegion)?; + amis.insert(region, ami_id); + } + + // We make a map storing our regional clients because they're used in a future and need to + // live until the future is resolved. + let mut ec2_clients = HashMap::with_capacity(amis.len()); + for region in amis.keys() { + let ec2_client = build_client::(®ion, &aws).context(error::Client { + client_type: "EC2", + region: region.name(), + })?; + ec2_clients.insert(region.clone(), ec2_client); + } + + let snapshots = get_snapshots(&amis, &ec2_clients).await?; + trace!("Found snapshots: {:?}", snapshots); + + info!("Updating snapshot permissions - making {}", mode); + modify_snapshots(&snapshots, &ec2_clients, operation.clone()).await?; + info!("Updating image permissions - making {}", mode); + modify_images(&amis, &ec2_clients, operation.clone()).await?; + + Ok(()) +} + +/// Returns a regional mapping of snapshot IDs associated with the given AMIs. +async fn get_snapshots( + amis: &HashMap, + clients: &HashMap, +) -> Result>> { + // Build requests for image information. + let mut describe_requests = Vec::with_capacity(amis.len()); + for (region, image_id) in amis { + let ec2_client = &clients[region]; + let describe_request = DescribeImagesRequest { + image_ids: Some(vec![image_id.to_string()]), + ..Default::default() + }; + let describe_future = ec2_client.describe_images(describe_request); + + // Store the region and image ID so we can include it in errors + let info_future = ready((region.clone(), image_id.clone())); + describe_requests.push(join(info_future, describe_future)); + } + + // Send requests in parallel and wait for responses, collecting results into a list. + let request_stream = stream::iter(describe_requests).buffer_unordered(4); + let describe_responses: Vec<( + (Region, String), + std::result::Result>, + )> = request_stream.collect().await; + + // For each described image, get the snapshot IDs from the block device mappings. + let mut snapshots = HashMap::with_capacity(amis.len()); + for ((region, image_id), describe_response) in describe_responses { + // Get the image description, ensuring we only have one. + let describe_response = describe_response.context(error::DescribeImages { + region: region.name(), + })?; + let mut images = describe_response.images.context(error::MissingInResponse { + request_type: "DescribeImages", + missing: "images", + })?; + ensure!( + !images.is_empty(), + error::MissingImage { + region: region.name(), + image_id, + } + ); + ensure!( + images.len() == 1, + error::MultipleImages { + region: region.name(), + images: images + .into_iter() + .map(|i| i.image_id.unwrap_or_else(|| "".to_string())) + .collect::>() + } + ); + let image = images.remove(0); + + // Look into the block device mappings for snapshots. + let bdms = image + .block_device_mappings + .context(error::MissingInResponse { + request_type: "DescribeImages", + missing: "block_device_mappings", + })?; + ensure!( + !bdms.is_empty(), + error::MissingInResponse { + request_type: "DescribeImages", + missing: "non-empty block_device_mappings" + } + ); + let mut snapshot_ids = Vec::with_capacity(bdms.len()); + for bdm in bdms { + let ebs = bdm.ebs.context(error::MissingInResponse { + request_type: "DescribeImages", + missing: "ebs in block_device_mappings", + })?; + let snapshot_id = ebs.snapshot_id.context(error::MissingInResponse { + request_type: "DescribeImages", + missing: "snapshot_id in block_device_mappings.ebs", + })?; + snapshot_ids.push(snapshot_id); + } + snapshots.insert(region, snapshot_ids); + } + + Ok(snapshots) +} + +/// Modify snapshot attributes to make them public/private as requested. +async fn modify_snapshots( + snapshots: &HashMap>, + clients: &HashMap, + operation: String, +) -> Result<()> { + // Build requests to modify snapshot attributes. + let mut modify_snapshot_requests = Vec::new(); + for (region, snapshot_ids) in snapshots { + for snapshot_id in snapshot_ids { + let ec2_client = &clients[region]; + let modify_snapshot_request = ModifySnapshotAttributeRequest { + attribute: Some("createVolumePermission".to_string()), + group_names: Some(vec!["all".to_string()]), + operation_type: Some(operation.clone()), + snapshot_id: snapshot_id.clone(), + ..Default::default() + }; + let modify_snapshot_future = + ec2_client.modify_snapshot_attribute(modify_snapshot_request); + + // Store the region and snapshot ID so we can include it in errors + let info_future = ready((region.name().to_string(), snapshot_id.clone())); + modify_snapshot_requests.push(join(info_future, modify_snapshot_future)); + } + } + + // Send requests in parallel and wait for responses, collecting results into a list. + let request_stream = stream::iter(modify_snapshot_requests).buffer_unordered(4); + let modify_snapshot_responses: Vec<( + (String, String), + std::result::Result<(), RusotoError>, + )> = request_stream.collect().await; + + // Count up successes and failures so we can give a clear total in the final error message. + let mut error_count = 0u16; + let mut success_count = 0u16; + for ((region, snapshot_id), modify_snapshot_response) in modify_snapshot_responses { + match modify_snapshot_response { + Ok(()) => { + success_count += 1; + debug!( + "Modified permissions of snapshot {} in {}", + snapshot_id, region, + ); + } + Err(e) => { + error_count += 1; + error!( + "Modifying permissions of {} in {} failed: {}", + snapshot_id, region, e + ); + } + } + } + + ensure!( + error_count == 0, + error::ModifySnapshotAttribute { + error_count, + success_count, + } + ); + + Ok(()) +} + +/// Modify image attributes to make them public/private as requested. +async fn modify_images( + images: &HashMap, + clients: &HashMap, + operation: String, +) -> Result<()> { + // Build requests to modify image attributes. + let mut modify_image_requests = Vec::new(); + for (region, image_id) in images { + let ec2_client = &clients[region]; + let modify_image_request = ModifyImageAttributeRequest { + attribute: Some("launchPermission".to_string()), + user_groups: Some(vec!["all".to_string()]), + operation_type: Some(operation.clone()), + image_id: image_id.clone(), + ..Default::default() + }; + let modify_image_future = ec2_client.modify_image_attribute(modify_image_request); + + // Store the region and image ID so we can include it in errors + let info_future = ready((region.name().to_string(), image_id.clone())); + modify_image_requests.push(join(info_future, modify_image_future)); + } + + // Send requests in parallel and wait for responses, collecting results into a list. + let request_stream = stream::iter(modify_image_requests).buffer_unordered(4); + let modify_image_responses: Vec<( + (String, String), + std::result::Result<(), RusotoError>, + )> = request_stream.collect().await; + + // Count up successes and failures so we can give a clear total in the final error message. + let mut error_count = 0u16; + let mut success_count = 0u16; + for ((region, image_id), modify_image_response) in modify_image_responses { + match modify_image_response { + Ok(()) => { + success_count += 1; + info!("Modified permissions of image {} in {}", image_id, region,); + } + Err(e) => { + error_count += 1; + error!( + "Modifying permissions of {} in {} failed: {}", + image_id, region, e + ); + } + } + } + + ensure!( + error_count == 0, + error::ModifyImageAttribute { + error_count, + success_count, + } + ); + + Ok(()) +} + +mod error { + use crate::aws; + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility = "pub(super)")] + pub(crate) enum Error { + #[snafu(display("Error creating {} client in {}: {}", client_type, region, source))] + Client { + client_type: String, + region: String, + source: aws::client::Error, + }, + + #[snafu(display("Error reading config: {}", source))] + Config { + source: crate::config::Error, + }, + + #[snafu(display("Failed to describe images in {}: {}", region, source))] + DescribeImages { + region: String, + source: rusoto_core::RusotoError, + }, + + #[snafu(display("Failed to deserialize input from '{}': {}", path.display(), source))] + Deserialize { + path: PathBuf, + source: serde_json::Error, + }, + + #[snafu(display("Failed to {} '{}': {}", op, path.display(), source))] + File { + op: String, + path: PathBuf, + source: io::Error, + }, + + #[snafu(display("Input '{}' is empty", path.display()))] + Input { + path: PathBuf, + }, + + #[snafu(display("Infra.toml is missing {}", missing))] + MissingConfig { + missing: String, + }, + + #[snafu(display("Failed to find given AMI ID {} in {}", image_id, region))] + MissingImage { + region: String, + image_id: String, + }, + + #[snafu(display("Response to {} was missing {}", request_type, missing))] + MissingInResponse { + request_type: String, + missing: String, + }, + + #[snafu(display( + "Failed to modify permissions of {} of {} images", + error_count, error_count + success_count, + ))] + ModifyImageAttribute { + error_count: u16, + success_count: u16, + }, + + #[snafu(display( + "Failed to modify permissions of {} of {} snapshots", + error_count, error_count + success_count, + ))] + ModifySnapshotAttribute { + error_count: u16, + success_count: u16, + }, + + #[snafu(display("DescribeImages in {} with unique filters returned multiple results: {}", region, images.join(", ")))] + MultipleImages { + region: String, + images: Vec, + }, + + ParseRegion { + source: crate::aws::Error, + }, + + #[snafu(display( + "Given region(s) in Infra.toml / regions argument that are not in --ami-input file: {}", + regions.join(", ") + ))] + UnknownRegions { + regions: Vec, + }, + } +} +pub(crate) use error::Error; +type Result = std::result::Result; diff --git a/tools/pubsys/src/main.rs b/tools/pubsys/src/main.rs index 1f42b69eb27..da8292cbe64 100644 --- a/tools/pubsys/src/main.rs +++ b/tools/pubsys/src/main.rs @@ -4,9 +4,9 @@ Currently implemented: * building repos, whether starting from an existing repo or from scratch * registering and copying EC2 AMIs +* Marking EC2 AMIs public (or private again) To be implemented: -* machine-readable output describing AMI registrations * updating SSM parameters * high-level document describing pubsys usage with examples @@ -45,11 +45,17 @@ fn run() -> Result<()> { match args.subcommand { SubCommand::Repo(ref repo_args) => repo::run(&args, &repo_args).context(error::Repo), SubCommand::Ami(ref ami_args) => { + let mut rt = Runtime::new().context(error::Runtime)?; + rt.block_on(async { aws::ami::run(&args, &ami_args).await.context(error::Ami) }) + } + SubCommand::PublishAmi(ref publish_args) => { let mut rt = Runtime::new().context(error::Runtime)?; rt.block_on(async { - aws::ami::run(&args, &ami_args).await.context(error::Ami) + aws::publish_ami::run(&args, &publish_args) + .await + .context(error::PublishAmi) }) - }, + } } } @@ -80,6 +86,7 @@ struct Args { enum SubCommand { Repo(repo::RepoArgs), Ami(aws::ami::AmiArgs), + PublishAmi(aws::publish_ami::PublishArgs), } /// Parses a SemVer, stripping a leading 'v' if present @@ -114,6 +121,11 @@ mod error { #[snafu(display("Logger setup error: {}", source))] Logger { source: simplelog::TermLogError }, + #[snafu(display("Failed to publish AMI: {}", source))] + PublishAmi { + source: crate::aws::publish_ami::Error, + }, + #[snafu(display("Failed to build repo: {}", source))] Repo { source: crate::repo::Error },