Skip to content
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: write SSM parameters and AMI permissions to output files #2987

Merged
merged 2 commits into from
Apr 14, 2023

Conversation

mjsterckx
Copy link
Contributor

@mjsterckx mjsterckx commented Apr 6, 2023

Description of changes:

Modified the pubsys ssm and pubsys promote-ssm commands to allow the parameters to be written to a file. Modified the pubsys ami and pubsys publish-ami commands to have them write AMI launch permissions and publicity info to the output file.

  • pubsys ssm has one new argument: ssm_parameter_output. If set, the rendered parameters will be written to the file at this path with the following structure:
{
  "<region-name>": {
    "<parameter-name>": "<parameter-value>",
    ...
  }
}
  • pubsys promote-ssm has one new argument: ssm_parameter_output. The newly promoted parameters will be combined with the parameters already present in the file and written to it.

  • pubsys ami now writes public and launch_permissions to the output file along with id and name. The public field is set to false by default when registering the AMI, but if the AMI ID has already been registered before, then the field is set to the publicity of that AMI. The launch_permissions field gets an empty vec by default, but if the AMI is already registered, then the launch permissions for that AMI are retrieved and stored in that field.

  • pubsys publish-ami updates the public and launch_permissions fields in the input file if anything has changed after the publishing has finished.

Testing done:

  • Unit tests
  • Ran cargo make, cargo make ami, cargo make ssm with the new argument, and got a file like this:
{
  "us-west-2": {
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_id": "ami-012345678",
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_version": "1.14.0-f782c96d-dirty"
  }
}

The AMI file looked like this:

{
    "us-west-2": {
        "id": "ami-012345678",
        "name": "bottlerocket-aws-k8s-1.24-x86_64-v1.14.0-f782c96d-dirty",
        "public": false,
        "launch_permissions": [ ]
    }
}
  • Ran cargo make promote-ssm with the above file as input and keep_original_parameters false, and got a file like this:
{
  "us-west-2": {
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0/image_version": "1.14.0-f782c96d-dirty",
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0/image_id": "ami-012345678",
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_id": "ami-012345678",
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_version": "1.14.0-f782c96d-dirty"
  }
}
  • Ran cargo make ami-public and the amis.json file looked like this afterwards:
{
  "us-west-2": {
    "id": "ami-012345678",
    "name": "bottlerocket-aws-k8s-1.24-x86_64-v1.14.0-f782c96d-dirty",
    "public": true,
    "launch_permissions": [
      {
        "group": "all",
        "user_id": null,
        "organization_arn": null,
        "organizational_unit_arn": null
      }
    ]
  }
}

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.

@mjsterckx mjsterckx requested review from cbgbt and webern April 6, 2023 22:03
@bcressey
Copy link
Contributor

bcressey commented Apr 6, 2023

{
  "us-west-2": {
    "ami-012345678": {
      "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_id": "ami-012345678",
      "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_version": "1.14.0-f782c96d-dirty"
    }
  }
}

Why not always write the file, the way that pubsys ami always writes amis.json? That would have these benefits:

  • cargo make ssm and cargo make promote-ssm would just work and keep ssm-parameters.json up to date
  • the input / output / keep original distinctions could go away, since the input and output would be the same
  • overall the behavior would be more like pubsys ami which reduces developer surprise

I am also not convinced that everything should go under an AMI key like ami-012345678. The image_version field is unrelated to the AMI, and the image_id field duplicates that information.

A flattened map like this seems more intuitive:

{
  "us-west-2": {
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_id": "ami-012345678",
    "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_version": "1.14.0-f782c96d-dirty",
    "/msterckx/aws-k8s-1.24/x86_64/latest/image_id": "ami-012345678",
    "/msterckx/aws-k8s-1.24/x86_64/latest/image_version": "1.14.0-f782c96d-dirty"
  }
}

@mjsterckx
Copy link
Contributor Author

The goal of this code was to generate files that were easily consumed by the SSM validation and EC2 validation code (#2969 and #2983). I should've probably made that more clear in the description.

Doing it this way allows us to create a file for version 1.14.0. and a separate file for latest, which is what we use for the validation. Having the AMI id as a key lets the EC2 validation use the same files to check all of the images in there, without having to parse each parameter to see if it contains an image id.

@bcressey
Copy link
Contributor

bcressey commented Apr 7, 2023

The goal of this code was to generate files that were easily consumed by the SSM validation and EC2 validation code (#2969 and #2983).

I don't think it makes sense for the SSM validation code to be organizing parameters under an AMI ID - the AMI ID is irrelevant there. All the SSM validation should care about is whether the current parameter values match the expected values.

For EC2 validation, it kind of does seem like the validator logic should be going through and finding the subset of parameters that refer to an AMI ID. But if you wanted something more robust than a suffix match, you could organize it slightly differently:

{
  "us-west-2": {
    "image_ids": {
      "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_id": "ami-012345678",
      "/msterckx/aws-k8s-1.24/x86_64/latest/image_id": "ami-012345678",
    },
    "image_versions": {
      "/msterckx/aws-k8s-1.24/x86_64/1.14.0-f782c96d-dirty/image_version": "1.14.0-f782c96d-dirty",
      "/msterckx/aws-k8s-1.24/x86_64/latest/image_version": "1.14.0-f782c96d-dirty"
    }
  }
}

Doing it this way allows us to create a file for version 1.14.0. and a separate file for latest, which is what we use for the validation.

For pubsys / makefile integration, having the ability to easily work with all published artifacts for a specific build ID (like 1.14.0-f782c96d-dirty) is valuable. If I make AMIs and publish SSM parameters, I should be able to run cargo make validate-ssm and have it validate everything. If I subsequently promote SSM parameters, I'd expect cargo make validate-ssm to validate all the SSM parameters I've promoted. I would not expect to need to pass different arguments or point these operations to different JSON files; this should all just be maintained for me in the JSON file that resides in build artifacts.

For other use cases, like an account-wide ssm-params.json covering all SSM parameters ever published, a single unified file also seems to make the most sense. It'd be pretty easy to generate that file by concatenating all the files together, overwriting fields that are also present in later files (like "latest" params). And then the code path for doing the validation would be the same as in the local developer case.

@mjsterckx
Copy link
Contributor Author

^ Addressed @bcressey's suggestions and added the writing of AMI publicity info and launch permissions to the pubsys ami and pubsys publish-ami commands.

@mjsterckx mjsterckx changed the title pubsys: write SSM parameters to file pubsys: write SSM parameters and AMI publicity info and launch permissions to output files Apr 12, 2023
@mjsterckx mjsterckx changed the title pubsys: write SSM parameters and AMI publicity info and launch permissions to output files pubsys: write SSM parameters and AMI permissions to output files Apr 12, 2023
@mjsterckx mjsterckx requested a review from rpkelly April 13, 2023 00:13
@cbgbt
Copy link
Contributor

cbgbt commented Apr 13, 2023

It would be a lot easier to review this if it were split into two commits, one for each feature. Would you mind resubmitting it?

@mjsterckx
Copy link
Contributor Author

^ Split the one commit into two.

@@ -372,20 +418,63 @@ async fn _run(args: &Args, ami_args: &AmiArgs) -> Result<HashMap<String, Image>>
Ok(amis)
}

#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)]
pub(crate) struct LaunchPermissionDef {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be an enum, something like:

#[derive(Serialize, Deserialize, etc)]
#[serde(rename_all = "lowercase")]
enum LaunchPermissionDef {
    Group(String),
    UserId(String),
    OrganizationArn(String),
    OrganizationalUnitArn(String),
}

I'm pretty sure this still serializes to JSON the way you want.

Comment on lines 437 to 443
fn from(launch_permission: LaunchPermission) -> Self {
Self {
group: launch_permission
.group()
.map(|group| group.as_str().to_owned()),
user_id: launch_permission
.user_id()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supposing you do make this an enum, you could rewrite this pretty succinctly with a match:

let LaunchPermission { group, user_id, organization_arn, organizational_unit_arn } = launch_permission;
    match (group, user_id, organization_arn, organizational_unit_arn) {
        (Some(group), None, None, None) => LaunchPermissionDef::Group(group),
        (None, Some(user_id), None, None) => LaunchPermissionDef::UserId(user_id),
        (None, None, Some(organization_arn), None) => LaunchPermissionDef::OrganizationArn(organization_arn),
        (None, None, None, Some(organizational_unit_arn)) => LaunchPermissionDef::OrganizationalUnitArn(organizational_unit_arn),
        _ => // error!
    };

You would have to change this from impl From to impl TryFrom to handle the error case of all of the elements being None though.

Comment on lines 218 to 222

fn write_amis(ami_output: &PathBuf, amis: &HashMap<Region, Image>) -> Result<()> {
let file = File::create(ami_output).context(error::FileSnafu {
op: "write amis to file",
path: ami_output,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should probably also be used in pubsys ami command, right?

@@ -216,8 +231,86 @@ pub(crate) async fn run(args: &Args, promote_args: &PromoteArgs) -> Result<()> {
Ok(())
}

/// Read parameters in given file, add newly promoted parameters, and write combined parameters to
/// the given file
async fn write_rendered_parameters(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think append_rendered_parameters may be more telling, since write often carries a connotation of overwrite

"Writing promoted SSM parameters to {}",
ssm_parameters_output.display()
);
let parsed_parameters = parse_expected_parameters(&ssm_parameters_output.to_owned())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be parse_existing_parameters or just parse_parameters? Seems like the name is a holdover from parsing it in the validation code, but we need the method more generically.

Suggested change
let parsed_parameters = parse_expected_parameters(&ssm_parameters_output.to_owned())
let parsed_parameters = parse_parameter_file(&ssm_parameters_output.to_owned())

let combined_parameters: HashMap<Region, HashMap<SsmKey, String>> =
combine_parameters(parsed_parameters, set_parameters, source_target_map);

serde_json::to_writer_pretty(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a function somewhere? We write out this parameter data structure in more than one place now.

Comment on lines 269 to 270
/// Return a HashMap of Region mapped to a HashMap of SsmKey, String pairs, representing the newly
/// promoted parameters as well as the original parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the behavior is correct, but it's worth calling out that any parameter collisions take the promoted parameter value.

That case probably deserves a test too.

@@ -213,6 +223,31 @@ pub(crate) async fn run(args: &Args, ssm_args: &SsmArgs) -> Result<()> {
Ok(())
}

/// Write rendered parameters to the file at `ssm_parameters_output`
async fn write_rendered_parameters(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you do have one! Just need to share it.

Comment on lines +247 to +254
.context(error::ParseExistingSsmParametersSnafu {
path: ssm_parameters_output,
})?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct that this will fail if the file is empty or doesn't exist? Is it better to just write the new parameters in that case, rather than refusing to proceed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct, but I don't know if we just want to write the new parameters. One reason it could fail is that the syntax in the file is incorrect, in which case I think it's better to alert the user and halt the program than completely overwrite the content of the file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted offline and concluded that we could specially handle Error::ReadExpectedParameterFile.

@mjsterckx mjsterckx force-pushed the pubsys-write-parameters branch 2 times, most recently from d6b95a2 to d29cefa Compare April 13, 2023 23:40
Copy link
Contributor

@cbgbt cbgbt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me other than the casing.

Comment on lines 32 to 34
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)]
pub(crate) enum LaunchPermissionDef {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more coherent with our other serialization code if you lowercased these:

Suggested change
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)]
pub(crate) enum LaunchPermissionDef {
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LaunchPermissionDef {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to use snake_case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I git grep for rename_all in Bottlerocket, it seems like we typically use kebab-case or lowercase

organization_arn,
organizational_unit_arn,
..
} = launch_permission.clone();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am surprised that you had to clone this.

Comment on lines +247 to +254
.context(error::ParseExistingSsmParametersSnafu {
path: ssm_parameters_output,
})?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted offline and concluded that we could specially handle Error::ReadExpectedParameterFile.

@mjsterckx mjsterckx merged commit d2104ad into bottlerocket-os:develop Apr 14, 2023
@mjsterckx mjsterckx deleted the pubsys-write-parameters branch April 14, 2023 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants