stack-deploy 0.0.3

AWS CloudFormation stack deployment orchestration library with change set review, event monitoring, and Lambda deployment support
Documentation
use aws_sdk_cloudformation::operation::describe_change_set::DescribeChangeSetOutput;
use aws_sdk_cloudformation::types::{
    ResourceChange, ResourceChangeDetail, ResourceTargetDefinition,
};
use std::fmt::Display;
use std::io::{self, Write};

struct IndentWriter<'a> {
    writer: &'a mut dyn Write,
    level: usize,
    indent_str: &'static str,
}

impl<'a> IndentWriter<'a> {
    fn new(writer: &'a mut dyn Write) -> Self {
        IndentWriter {
            writer,
            level: 0,
            indent_str: "  ",
        }
    }

    fn indent(&mut self) -> IndentWriter<'_> {
        IndentWriter {
            writer: self.writer,
            level: self.level + 1,
            indent_str: self.indent_str,
        }
    }

    fn write_line<T: Display>(&mut self, value: T) -> io::Result<()> {
        for _ in 0..self.level {
            write!(self.writer, "{}", self.indent_str)?;
        }
        writeln!(self.writer, "{value}")
    }
}

pub fn print_change_set_output(output: &DescribeChangeSetOutput) {
    let mut stdout = io::stdout();
    let mut writer = IndentWriter::new(&mut stdout);
    writer.write_line("=== Change Set Details ===").unwrap();
    print_basic_details(output, &mut writer);
    print_changes(output, &mut writer);
}

fn print_basic_details(output: &DescribeChangeSetOutput, writer: &mut IndentWriter<'_>) {
    if let Some(name) = &output.change_set_name {
        writer
            .write_line(format!("Change Set Name: {name}"))
            .unwrap();
    }
    if let Some(id) = &output.change_set_id {
        writer.write_line(format!("Change Set ID: {id}")).unwrap();
    }
    if let Some(stack_name) = &output.stack_name {
        writer
            .write_line(format!("Stack Name: {stack_name}"))
            .unwrap();
    }
    if let Some(execution_status) = &output.execution_status {
        writer
            .write_line(format!("Execution Status: {execution_status}"))
            .unwrap();
    }
    if let Some(status) = &output.status {
        writer.write_line(format!("Status: {status:?}")).unwrap();
    }
    if let Some(status_reason) = &output.status_reason {
        writer
            .write_line(format!("Status Reason: {status_reason}"))
            .unwrap();
    }
    if let Some(creation_time) = &output.creation_time {
        writer
            .write_line(format!("Creation Time: {creation_time}"))
            .unwrap();
    }
    if let Some(description) = &output.description {
        writer
            .write_line(format!("Description: {description}"))
            .unwrap();
    }
}

fn print_changes(output: &DescribeChangeSetOutput, writer: &mut IndentWriter<'_>) {
    if let Some(changes) = &output.changes {
        for change in changes {
            if let Some(resource_change) = &change.resource_change {
                print_resource_change(resource_change, writer);
            }
        }
    }
}

fn print_resource_change(resource_change: &ResourceChange, writer: &mut IndentWriter<'_>) {
    let logical_resource_id = resource_change.logical_resource_id.as_deref().unwrap();
    let resource_type = resource_change.resource_type.as_deref().unwrap();
    let action = resource_change.action.as_ref().unwrap();

    let mut indented_writer = writer.indent();
    indented_writer
        .write_line(format!("{logical_resource_id}:"))
        .unwrap();

    let mut detail_writer = indented_writer.indent();
    detail_writer
        .write_line(format!("Resource Type: {resource_type}"))
        .unwrap();
    detail_writer
        .write_line(format!("Action: {action:?}"))
        .unwrap();

    if let Some(replacement) = &resource_change.replacement {
        detail_writer
            .write_line(format!("Replacement: {replacement:?}"))
            .unwrap();
    }

    if let Some(details) = &resource_change.details
        && !details.is_empty()
    {
        detail_writer.write_line("Details:").unwrap();
        for detail in details {
            print_resource_change_detail(detail, &mut detail_writer);
        }
    }
}

fn print_resource_change_detail(detail: &ResourceChangeDetail, writer: &mut IndentWriter<'_>) {
    let mut indented_writer = writer.indent();
    indented_writer.write_line("Detail:").unwrap();

    let mut inner_writer = indented_writer.indent();
    if let Some(target) = &detail.target {
        print_target(target, &mut inner_writer);
    }
    if let Some(change_source) = &detail.change_source {
        inner_writer
            .write_line(format!("Change Source: {change_source:?}"))
            .unwrap();
    }
    if let Some(causing_entity) = &detail.causing_entity {
        inner_writer
            .write_line(format!("Causing Entity: {causing_entity}"))
            .unwrap();
    }
    if let Some(evaluation) = &detail.evaluation {
        inner_writer
            .write_line(format!("Evaluation: {evaluation:?}"))
            .unwrap();
    }
}

fn print_target(target: &ResourceTargetDefinition, writer: &mut IndentWriter<'_>) {
    writer.write_line("Target:").unwrap();

    let mut indented_writer = writer.indent();
    if let Some(attribute) = &target.attribute {
        indented_writer
            .write_line(format!("Attribute: {attribute:?}"))
            .unwrap();
    }
    print_optional_field("Name", &target.name, &mut indented_writer);
    if let Some(requires_recreation) = &target.requires_recreation {
        indented_writer
            .write_line(format!("Requires Recreation: {requires_recreation}"))
            .unwrap();
    }
    print_optional_field("Path", &target.path, &mut indented_writer);
    print_optional_field("Before Value", &target.before_value, &mut indented_writer);
    print_optional_field("After Value", &target.after_value, &mut indented_writer);
    print_optional_field(
        "Attribute Change Type",
        &target.attribute_change_type,
        &mut indented_writer,
    );
}

fn print_optional_field<T: Display>(
    field_name: &str,
    field: &Option<T>,
    writer: &mut IndentWriter<'_>,
) {
    if let Some(value) = field {
        writer.write_line(format!("{field_name}: {value}")).unwrap();
    }
}