-
Notifications
You must be signed in to change notification settings - Fork 521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pubsys: added ami validation #3020
pubsys: added ami validation #3020
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Most of these are just nitpicks or tips. The thing that I care the most to see changed is to add a test for the AMI file parsing code. The function is a pure function and the logic inside is complex enough to warrant it.
write_results_path: Option<PathBuf>, | ||
|
||
#[structopt(long, requires = "write-results-path")] | ||
/// Optional filter to only write validation results with these statuses to the above path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, but the help text looks funky without this.
/// Optional filter to only write validation results with these statuses to the above path | |
/// Optional filter to only write validation results with these statuses to the above path. |
|
||
pub(crate) async fn describe_images<'a>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use a docstring here to explain why we need this.
region: &Region, | ||
client: &Ec2Client, | ||
image_ids: Vec<String>, | ||
expected_image_public: &HashMap<String, bool>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd document what the purpose of this parameter is in the docstring, since it's not obvious from the function signature.
correct: i32, | ||
incorrect: i32, | ||
missing: i32, | ||
accessible: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have strong feelings about rewriting this, but I'm realizing that because accessible
removes meaning from the other values here, I think a more maintainable way to write this would be to:
- Change
AmiValidationRegionSummary
to an enum of something like
enum AmiValiationRegionSummary {
Results {
correct: u64, // etc
},
Unreachable,
- Modify
display()
to render this by copying it to a private implementation which uses the marker values, orn/a
or something similar in the table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's currently implemented the same way in the ssm-validation so I will address those together in a different PR.
}), | ||
) | ||
}) | ||
.collect::<HashMap<Region, ami::Result<HashSet<AmiValidationResult>>>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bit of a gnarly turbofish that probably isn't needed, since there's a type specifier on the variable binding.
.collect::<HashMap<Region, ami::Result<HashSet<AmiValidationResult>>>>(); | |
.collect(); |
}) | ||
.collect::<HashMap<Region, ami::Result<HashSet<AmiValidationResult>>>>(); | ||
|
||
let validation_results = AmiValidationResults::new(results); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, but usually new
isn't appropriate when you're initializing something that's computation heavy like that. A better name would be from_result_map
or evaluate_results()
or something.
let validation_results = AmiValidationResults::new(results); | |
let validation_results = AmiValidationResults::new(results); |
// If the value for each Region is a JSON object instead of an array, wrap it in an array | ||
let vectored_images = expected_amis | ||
.into_iter() | ||
.map(|(region, value)| { | ||
let image_values = match value.as_array() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we write a test for this behavior? As an alternative tip, you can get Serde to parse this for you "for free" via #[serde(untagged)]
on an enum.
enum ImageDef {
Image(Image),
ImageList(Vec<Image>),
}
impl ImageDef {
fn images(&self) -> Vec<Image> {
match self {
// etc
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to check validate_ami/results.rs
let mut requests = Vec::with_capacity(clients.len()); | ||
for region in clients.keys() { | ||
trace!("Requesting images in {}", region); | ||
let ec2_client: &Ec2Client = &clients[region]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let ec2_client: &Ec2Client = &clients[region]; | |
let ec2_client: &Ec2Client = &clients.get(region).context()...; |
Indexing with []
can panic.
) -> HashMap<&'a Region, Result<HashMap<String, ImageDef>>> { | ||
// Build requests for images; we have to request with a regional client so we split them by | ||
// region | ||
let mut requests = Vec::with_capacity(clients.len()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using an iterator for this function could simplify some of the logic below.
region.as_ref() | ||
); | ||
let mut image_def = ImageDef::from(image.to_owned()); | ||
if !*expected_public { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this go above 141 and then make ImageDef::from()
take in the bool so image_def doesn't need to be mut?
let infra_config = InfraConfig::from_path_or_lock(&args.infra_config_path, false) | ||
.context(error::ConfigSnafu)?; | ||
|
||
let aws = infra_config.aws.clone().unwrap_or_default(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need .clone()
info!("Parsed expected ami file"); | ||
|
||
// Create a HashMap of AmiClients, one for each region where validation should happen | ||
let base_region = &Region::new(aws.regions[0].clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let base_region = &Region::new(aws.regions[0].clone()); | |
let base_region = &Region::new(aws.regions.get(0).context()...); |
[]
can panic
/// Validates EC2 images in a single region, based on a Vec (ImageDef) of expected images | ||
/// and a HashMap (AmiId, ImageDef) of actual retrieved images. Returns a HashSet of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super nit
/// Validates EC2 images in a single region, based on a Vec (ImageDef) of expected images | |
/// and a HashMap (AmiId, ImageDef) of actual retrieved images. Returns a HashSet of | |
/// Validates EC2 images in a single region, based on a `Vec<ImageDef>` of expected images | |
/// and a `HashMap<AmiId, ImageDef>` of actual retrieved images. Returns a `HashSet<AmiValidationResult>` containing ... |
expected_images: Vec<ImageDef>, | ||
actual_images: &HashMap<AmiId, ImageDef>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason 1 is a reference and the other is taking ownership?
if image.public { | ||
image.launch_permissions = None; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't image.launch_permissions
set to None when the ImageDef
is created? If that's not possible can launch_permissions
be a function?
image.id.clone(), | ||
image.clone(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to duplicate the image.id
?
) | ||
.context(error::ParseExpectedImagesFileSnafu)?; | ||
|
||
// If the value for each Region is a JSON object instead of an array, wrap it in an array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would cause the value to be an object instead of an array? This is a file that another pubsys command is writing right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, this takes a file that is written by pubsys ami
where each region has a single AMI object. However, I wanted to add functionality where the command can take a file containing an array of AMI objects per region as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to the suggested AmiValidationRegionSummary
refactor. Otherwise just a few small things.
impl Display for AmiValidationResultStatus { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
match self { | ||
AmiValidationResultStatus::Correct => write!(f, "Correct"), | ||
AmiValidationResultStatus::Incorrect => write!(f, "Incorrect"), | ||
AmiValidationResultStatus::Missing => write!(f, "Missing"), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impl Display for AmiValidationResultStatus { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
match self { | |
AmiValidationResultStatus::Correct => write!(f, "Correct"), | |
AmiValidationResultStatus::Incorrect => write!(f, "Incorrect"), | |
AmiValidationResultStatus::Missing => write!(f, "Missing"), | |
} | |
} | |
} | |
derive_display_from_serialize!(AmiValidationResultStatus); |
That will have the same effect. And expand if new variants of the enum are added.
impl FromStr for AmiValidationResultStatus { | ||
type Err = super::Error; | ||
|
||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { | ||
match s { | ||
"Correct" => Ok(Self::Correct), | ||
"Incorrect" => Ok(Self::Incorrect), | ||
"Missing" => Ok(Self::Missing), | ||
filter => Err(Self::Err::InvalidStatusFilter { | ||
filter: filter.to_string(), | ||
}), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impl FromStr for AmiValidationResultStatus { | |
type Err = super::Error; | |
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { | |
match s { | |
"Correct" => Ok(Self::Correct), | |
"Incorrect" => Ok(Self::Incorrect), | |
"Missing" => Ok(Self::Missing), | |
filter => Err(Self::Err::InvalidStatusFilter { | |
filter: filter.to_string(), | |
}), | |
} | |
} | |
} | |
derive_fromstr_from_deserialize!(AmiValidationResultStatus) |
pub(crate) region: Region, | ||
|
||
/// The validation status of the image | ||
pub(crate) status: AmiValidationResultStatus, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should just be a function since it is described by other fields of this enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I suggested this be added as a field in a prior revision, since it's otherwise not really a Result
being computed, but rather a validation computation being created which is later used to compute the result.
I guess if we renamed this AmiValidation
with a method AmiValidation::get_result()
or something, I'd be ok with that.
But aren't these serialized? Do we want to keep the status so that the computation result is specifically kept?
expected_images | ||
.get(region) | ||
.map(|e| e.to_owned()) | ||
.unwrap_or(vec![]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
.unwrap_or(vec![]), | |
.unwrap_or_default();, |
|
||
#[derive(Debug, Snafu)] | ||
#[snafu(visibility(pub(super)))] | ||
pub(crate) enum Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the error enum variants and ensure they're being used. I see a few that aren't being used anywhere. e.g. ReadValidationConfig
, ParseValidationConfig
, MissingField
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the error enum variants and ensure they're being used. I see a few that aren't being used anywhere. e.g. ReadValidationConfig, ParseValidationConfig, MissingField etc.
As an aside, the reason you have to check is because the code that Snafu generates "uses" each enum variant so the compiler can't warn you about unused variants.
/// Represents a single EC2 image validation result | ||
#[derive(Debug, Eq, Hash, PartialEq, Serialize)] | ||
pub(crate) struct AmiValidationResult { | ||
/// The id of the image |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: proper capitalization
/// The id of the image | |
/// The ID of the image |
and where ever else this may apply
/// The id of the image | ||
pub(crate) id: String, | ||
|
||
/// ImageDef containing expected values for the image |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
/// ImageDef containing expected values for the image | |
/// `ImageDef` containing expected values for the image |
and where ever else this may apply.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love the testing.
use std::path::PathBuf; | ||
use structopt::{clap, StructOpt}; | ||
|
||
/// Validates EC2 images |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Describe this in more detail. What's the input, what's the output, etc.
|
||
let validation_results = AmiValidationResults::new(results); | ||
|
||
// If a path was given to write the results to, write the results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing nit
// If a path was given to write the results to, write the results | |
// If a path was given, write the results |
|
||
#[derive(Debug, Snafu)] | ||
#[snafu(visibility(pub(super)))] | ||
pub(crate) enum Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the error enum variants and ensure they're being used. I see a few that aren't being used anywhere. e.g. ReadValidationConfig, ParseValidationConfig, MissingField etc.
As an aside, the reason you have to check is because the code that Snafu generates "uses" each enum variant so the compiler can't warn you about unused variants.
#[snafu(display("Failed to describe images in {}: {}", region, source))] | ||
DescribeImages { | ||
region: String, | ||
source: SdkError<DescribeImagesError>, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where-ever we're using a SdkError
as source we need to do this to get the full error message:
#[snafu(display("Failed to describe images in {}: {}", region, source))] | |
DescribeImages { | |
region: String, | |
source: SdkError<DescribeImagesError>, | |
}, | |
use aws_smithy_types::error::display::DisplayErrorContext; | |
#[snafu(display("Failed to describe images in {}: {}", region, DisplayErrorContext(source)))] | |
DescribeImages { | |
region: String, | |
source: SdkError<DescribeImagesError>, | |
}, |
See https://docs.rs/aws-smithy-client/latest/aws_smithy_client/enum.SdkError.html
When logging an error from the SDK, it is recommended that you either wrap the error in DisplayErrorContext, use another error reporter library that visits the error’s cause/source chain, or call Error::source for more details about the underlying cause.
Note: This needs to be a tree wide change, but might as well start here.
Please create an issue for the other places if you're willing.
57dc749
to
0e83ef4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Sorry to pile on more feedback -- this is a substantial change! Just some tidying up.
} | ||
} | ||
|
||
/// Fetches all images with IDs in `image_ids`. The map `expected_image_public` is used to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the docstrings for this and describe_images_in_region
may not have been updated for some subsequent changes to the code. I would fix this before merging.
match result { | ||
Ok(result) => Ok(result), | ||
Err(_) => Err(error::Error::UnreachableRegion { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit/tip (don't feel compelled to change this): I think this is the same as
result.map_err(|_| error::Error::UnreachableRegion { region: region.to_string() })
match
on Result
and Option
when the operation to be carried out is already expressed as a method on those structs tends to result in deep nesting.
incorrect: i32, | ||
missing: i32, | ||
unexpected: i32, | ||
accessible: bool, | ||
unreachable: i32, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: these should probably be unsigned.
Err(_) => Err(error::Error::UnreachableRegion { | ||
region: region.to_string(), | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense for this to hold onto the original error?
I think we just want to make sure the reason for the unavailability can be surfaced. Ensuring that it is logged somewhere at ERROR
level probably suffices, since it's not like the deeper result information is stored in our serialized format.
0e83ef4
to
2e8b67d
Compare
Description of changes:
Added a
validate-ami
subcommand to thepubsys
tool. This command has the following signature:The function takes a path to a file containing an image object per region. An example file looks like this:
The function will call the
describe-images
function on each image in the file to ensure that the retrieved id, name, and publicity match the expected values. Ifpublic
is false, then the specific launch_permissions are retrieved usingdescribe-image-attribute
and included in the comparison. Aside from publicity and launch_permissions, the function will check thatsriov_net_support
issimple
andena_support
istrue
.The result of the function looks like this:
correct
indicates that all retrieved values match the expected valuesincorrect
indicates that at least one value does not match the expected valuemissing
indicates that the specific image failed to be retrievedunreachable
indicates that the images in this region could not be retrieved because the region could not be reached--write-results-path
and--write-results-filter
allow the user to write the results of the ami validation to a JSON file. The filter determines which statuses will be written to this file. The file looks like this:Modified the SSM validation results to represent the new
Unreachable
status as well.Testing done:
correct
.Terms of contribution:
By submitting this pull request, I agree that this contribution is dual-licensed under the terms of both the Apache License, version 2.0, and the MIT license.