Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ resolver = "2"
members = [
"tools/amispec",
"tools/bottlerocket-variant",
"tools/error-utils",
"tools/buildsys",
"tools/buildsys-config",
"tools/include-env-compressed",
Expand Down Expand Up @@ -58,6 +59,7 @@ bottlerocket-types = { version = "0.0.16", git = "https://github.com/bottlerocke
bottlerocket-variant = { version = "0.1", path = "tools/bottlerocket-variant" }
buildsys = { version = "0.1", path = "tools/buildsys", lib = true, artifact = [ "bin:buildsys" ] }
buildsys-config = { version = "0.1", path = "tools/buildsys-config" }
error-utils = { version = "0.1", path = "tools/error-utils" }
include-env-compressed = { version = "0.1", path = "tools/include-env-compressed" }
include-env-compressed-macro = { version = "0.1", path = "tools/include-env-compressed/include-env-compressed-macro" }
krane-bundle = { version = "0.1", path = "tools/krane" }
Expand Down Expand Up @@ -99,6 +101,7 @@ aws-sdk-ec2 = { version = "1", default-features = false, features = ["default-ht
aws-sdk-kms = { version = "1", default-features = false, features = ["default-https-client", "rt-tokio"] }
aws-sdk-ssm = { version = "1", default-features = false, features = ["default-https-client", "rt-tokio"] }
aws-sdk-sts = { version = "1", default-features = false, features = ["default-https-client", "rt-tokio"] }
aws-smithy-runtime-api = { version = "1", features = ["client"] }
aws-smithy-types = "1"
aws-types = "1"
base64 = "0.22"
Expand Down
24 changes: 24 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,27 @@ reason = "Twoliter usually does not want to resolve symlinks. Use path_absolutiz
path = "tokio::fs::canonicalize"
reason = "Twoliter usually does not want to resolve symlinks. Use path_absolutize instead"

[[disallowed-types]]
path = "aws_sdk_ec2::error::SdkError"
reason = "Use error_utils::AwsSdkError wrapper instead to ensure consistent error formatting"

[[disallowed-types]]
path = "aws_sdk_ssm::error::SdkError"
reason = "Use error_utils::AwsSdkError wrapper instead to ensure consistent error formatting"

[[disallowed-types]]
path = "aws_smithy_runtime_api::client::result::SdkError"
reason = "Use error_utils::AwsSdkError wrapper instead to ensure consistent error formatting"

[[disallowed-types]]
path = "aws_sdk_ebs::error::SdkError"
reason = "Use error_utils::AwsSdkError wrapper instead to ensure consistent error formatting"

[[disallowed-types]]
path = "aws_sdk_kms::error::SdkError"
reason = "Use error_utils::AwsSdkError wrapper instead to ensure consistent error formatting"

[[disallowed-types]]
path = "aws_sdk_sts::error::SdkError"
reason = "Use error_utils::AwsSdkError wrapper instead to ensure consistent error formatting"

14 changes: 14 additions & 0 deletions tools/error-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "error-utils"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0 OR MIT"
publish = false

[dependencies]
aws-smithy-runtime-api.workspace = true
aws-smithy-types.workspace = true

[dev-dependencies]
aws-sdk-ssm.workspace = true
aws-smithy-types.workspace = true
105 changes: 105 additions & 0 deletions tools/error-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#![allow(clippy::disallowed_types)]

use aws_smithy_types::error::display::DisplayErrorContext;
use aws_smithy_types::error::metadata::ProvideErrorMetadata;
use std::error::Error as StdError;
use std::fmt;

/// Shadow the AWS SDK error type.
pub type SdkError<E, R = ::aws_smithy_runtime_api::client::orchestrator::HttpResponse> =
::aws_smithy_runtime_api::client::result::SdkError<E, R>;

/// Wrapper around AWS SDK errors that ensures consistent formatting
pub struct AwsSdkError<E, R = ::aws_smithy_runtime_api::client::orchestrator::HttpResponse>(
pub SdkError<E, R>,
)
where
E: ProvideErrorMetadata + StdError + 'static;

impl<E> fmt::Display for AwsSdkError<E>
where
E: ProvideErrorMetadata + StdError + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", format_aws_sdk_error(&self.0))
}
}

impl<E> fmt::Debug for AwsSdkError<E>
where
E: ProvideErrorMetadata + StdError + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}

impl<E> StdError for AwsSdkError<E>
where
E: ProvideErrorMetadata + StdError + 'static,
{
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.0)
}
}

impl<E> From<SdkError<E>> for AwsSdkError<E>
where
E: ProvideErrorMetadata + StdError + 'static,
{
fn from(error: SdkError<E>) -> Self {
AwsSdkError(error)
}
}

/// Helper function to format AWS SDK errors as "code: message" or fall back to full context
///
/// # Examples
///
/// With code only: "AccessDenied"
/// With message only: "Request timeout occurred"
/// With neither: Falls back to DisplayErrorContext for full error details
pub fn format_aws_sdk_error<E>(sdk_error: &SdkError<E>) -> String
where
E: ProvideErrorMetadata + StdError + 'static,
{
match (sdk_error.code(), sdk_error.message()) {
(Some(code), Some(message)) => format!("{code}: {message}"),
(Some(code), None) => code.to_string(),
(None, Some(message)) => message.to_string(),
(None, None) => format!("{}", DisplayErrorContext(sdk_error)),
}
}

#[cfg(test)]
mod tests {
use super::*;
use aws_sdk_ssm::operation::get_parameters::GetParametersError;
use aws_smithy_types::error::metadata::ErrorMetadata;

#[test]
fn test_error_metadata_extraction() {
// Test that our logic works with error metadata
let error_metadata = ErrorMetadata::builder()
.code("InvalidParameter")
.message("Parameter is invalid")
.build();

let service_error = GetParametersError::generic(error_metadata);

// Verify the error has the expected code and message
assert_eq!(service_error.code(), Some("InvalidParameter"));
assert_eq!(service_error.message(), Some("Parameter is invalid"));
}

#[test]
fn test_aws_sdk_error_wrapper_display() {
let sdk_error: SdkError<GetParametersError> = SdkError::timeout_error("request timeout");
let wrapper = AwsSdkError(sdk_error);

// The wrapper should use our format_aws_sdk_error function
let result = wrapper.to_string();
assert!(result.len() > 0);
assert!(result.contains("timeout") || result.contains("Timeout"));
}
}
1 change: 1 addition & 0 deletions tools/pubsys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ async-stream.workspace = true
async-trait.workspace = true
aws-config.workspace = true
aws-credential-types.workspace = true
error-utils = { workspace = true }
aws-sdk-ebs.workspace = true
aws-sdk-ec2.workspace = true
aws-sdk-kms.workspace = true
Expand Down
8 changes: 5 additions & 3 deletions tools/pubsys/src/aws/ami/launch_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use aws_sdk_ec2::{
types::{ImageAttributeName, LaunchPermission},
Client as Ec2Client,
};
use error_utils::AwsSdkError;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;

Expand All @@ -17,6 +18,7 @@ pub(crate) async fn get_launch_permissions(
.attribute(ImageAttributeName::LaunchPermission)
.send()
.await
.map_err(AwsSdkError::from)
.context(error::DescribeImageAttributeSnafu {
ami_id,
region: region.to_string(),
Expand Down Expand Up @@ -75,9 +77,9 @@ impl TryFrom<LaunchPermission> for LaunchPermissionDef {
}

mod error {
use aws_sdk_ec2::error::SdkError;
use aws_sdk_ec2::operation::describe_image_attribute::DescribeImageAttributeError;
use aws_sdk_ec2::types::LaunchPermission;
use error_utils::AwsSdkError;
use snafu::Snafu;

#[derive(Debug, Snafu)]
Expand All @@ -87,8 +89,8 @@ mod error {
DescribeImageAttribute {
ami_id: String,
region: String,
#[snafu(source(from(SdkError<DescribeImageAttributeError>, Box::new)))]
source: Box<SdkError<DescribeImageAttributeError>>,
#[snafu(source(from(AwsSdkError<DescribeImageAttributeError>, Box::new)))]
source: Box<AwsSdkError<DescribeImageAttributeError>>,
},

#[snafu(display("Invalid launch permission: {:?}", launch_permission))]
Expand Down
39 changes: 24 additions & 15 deletions tools/pubsys/src/aws/ami/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::aws::{client::build_client_config, region_from_string};
use crate::frompath::FromPath;
use crate::Args;
use aws_sdk_ebs::Client as EbsClient;
use aws_sdk_ec2::error::{ProvideErrorMetadata, SdkError};
use aws_sdk_ec2::error::ProvideErrorMetadata;
use aws_sdk_ec2::operation::copy_image::{CopyImageError, CopyImageOutput};
use aws_sdk_ec2::types::OperationType;
use aws_sdk_ec2::{config::Region, Client as Ec2Client};
Expand All @@ -25,8 +25,10 @@ use aws_sdk_sts::operation::get_caller_identity::{
use aws_sdk_sts::Client as StsClient;
use buildsys::manifest::ManifestInfo;
use clap::Parser;
use error_utils::AwsSdkError;
use futures::future::{join, lazy, ready, FutureExt};
use futures::stream::{self, StreamExt};
use futures::TryFutureExt;
use log::{error, info, trace, warn};
use pubsys_config::{AwsConfig as PubsysAwsConfig, InfraConfig};
use register::{get_ami_id, register_image, RegisteredIds};
Expand Down Expand Up @@ -271,11 +273,14 @@ async fn _run(args: &Args, ami_args: &AmiArgs) -> Result<HashMap<String, Image>>
let client_config = build_client_config(&base_region, &base_region, &aws).await;
let base_sts_client = StsClient::new(&client_config);

let response = base_sts_client.get_caller_identity().send().await.context(
error::GetCallerIdentitySnafu {
let response = base_sts_client
.get_caller_identity()
.send()
.await
.map_err(AwsSdkError::from)
.context(error::GetCallerIdentitySnafu {
region: base_region.as_ref(),
},
)?;
})?;
let base_account_id = response.account.context(error::MissingInResponseSnafu {
request_type: "GetCallerIdentity",
missing: "account",
Expand Down Expand Up @@ -391,7 +396,8 @@ async fn _run(args: &Args, ami_args: &AmiArgs) -> Result<HashMap<String, Image>>
.set_source_image_id(Some(ids_of_image.image_id.clone()))
.set_source_region(Some(base_region.as_ref().to_string()))
.set_copy_image_tags(Some(true))
.send();
.send()
.map_err(AwsSdkError::from);

// Store the region so we can output it to the user
let region_future = ready(region.clone());
Expand All @@ -414,7 +420,7 @@ async fn _run(args: &Args, ami_args: &AmiArgs) -> Result<HashMap<String, Image>>
// Run through the stream and collect results into a list.
let copy_responses: Vec<(
Region,
std::result::Result<CopyImageOutput, SdkError<CopyImageError>>,
std::result::Result<CopyImageOutput, AwsSdkError<CopyImageError>>,
)> = request_stream.collect().await;

// Report on successes and errors; don't fail immediately if we see an error so we can report
Expand Down Expand Up @@ -450,7 +456,7 @@ async fn _run(args: &Args, ami_args: &AmiArgs) -> Result<HashMap<String, Image>>
error!(
"Copy to {} failed: {}",
region,
e.into_service_error().code().unwrap_or("unknown")
e.0.into_service_error().code().unwrap_or("unknown")
);
}
}
Expand Down Expand Up @@ -508,7 +514,10 @@ async fn get_account_ids(
let mut requests = Vec::with_capacity(regions.len());
for region in regions.iter() {
let sts_client = &sts_clients[region];
let response_future = sts_client.get_caller_identity().send();
let response_future = sts_client
.get_caller_identity()
.send()
.map_err(AwsSdkError::from);

// Store the region so we can include it in any errors
let region_future = ready(region.clone());
Expand All @@ -519,7 +528,7 @@ async fn get_account_ids(
// Run through the stream and collect results into a list.
let responses: Vec<(
Region,
std::result::Result<GetCallerIdentityOutput, SdkError<GetCallerIdentityError>>,
std::result::Result<GetCallerIdentityOutput, AwsSdkError<GetCallerIdentityError>>,
)> = request_stream.collect().await;

for (region, response) in responses {
Expand Down Expand Up @@ -567,10 +576,10 @@ pub(crate) fn parse_uefi_data(filepath: &str) -> Result<FromPath<String>> {
mod error {
use super::register::mk_amispec;
use crate::aws::{ami, publish_ami};
use aws_sdk_ec2::error::SdkError;
use aws_sdk_ec2::operation::modify_image_attribute::ModifyImageAttributeError;
use aws_sdk_sts::operation::get_caller_identity::GetCallerIdentityError;
use buildsys::manifest;
use error_utils::AwsSdkError;
use snafu::Snafu;
use std::path::PathBuf;

Expand Down Expand Up @@ -613,8 +622,8 @@ mod error {
#[snafu(display("Error getting account ID in {}: {}", region, source))]
GetCallerIdentity {
region: String,
#[snafu(source(from(SdkError<GetCallerIdentityError>, Box::new)))]
source: Box<SdkError<GetCallerIdentityError>>,
#[snafu(source(from(AwsSdkError<GetCallerIdentityError>, Box::new)))]
source: Box<AwsSdkError<GetCallerIdentityError>>,
},

#[snafu(display(
Expand Down Expand Up @@ -642,8 +651,8 @@ mod error {
GrantImageAccess {
thing: String,
region: String,
#[snafu(source(from(SdkError<ModifyImageAttributeError>, Box::new)))]
source: Box<SdkError<ModifyImageAttributeError>>,
#[snafu(source(from(AwsSdkError<ModifyImageAttributeError>, Box::new)))]
source: Box<AwsSdkError<ModifyImageAttributeError>>,
},

#[snafu(display(
Expand Down
Loading
Loading