Skip to content

Commit

Permalink
Rust: check that existing cloudformation stack is in a valid state (#531
Browse files Browse the repository at this point in the history
)

* move cf client init to function where it is used

* check that existing cloudformation stack is in a valid state

* streamline value output

* bump version

* lint fix
  • Loading branch information
Esgrove authored Oct 9, 2024
1 parent 443ddf6 commit 63ecac7
Show file tree
Hide file tree
Showing 9 changed files with 49 additions and 36 deletions.
2 changes: 1 addition & 1 deletion rust/Cargo.lock

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

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nitor-vault"
version = "1.1.0"
version = "1.2.0"
edition = "2021"
description = "Encrypted AWS key-value storage utility."
license = "Apache-2.0"
Expand Down
22 changes: 12 additions & 10 deletions rust/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,26 @@ pub async fn init_vault_stack(
bucket: Option<String>,
) -> Result<()> {
match Vault::init(stack_name, region, bucket).await? {
CreateStackResult::AlreadyInitialized { data } => {
CreateStackResult::Exists { data } => {
println!("Vault stack already initialized");
println!("{data}");
}
CreateStackResult::ExistsWithFailedState { data } => {
anyhow::bail!(
"{}\n{data}",
"Vault stack exists but is in a failed state".red()
)
}
CreateStackResult::Created {
stack_name,
stack_id,
region,
} => {
println!("Stack created with ID: {stack_id}");
let config = aws_config::from_env().region(region).load().await;
let client = aws_sdk_cloudformation::Client::new(&config);
wait_for_stack_creation_to_finish(&client, &stack_name).await?;
wait_for_stack_creation_to_finish(&config, &stack_name).await?;
}
}

Ok(())
}

Expand Down Expand Up @@ -143,17 +147,16 @@ pub async fn exists(vault: &Vault, key: &str) -> Result<()> {

/// Poll Cloudformation for stack status until it has been created or creation failed.
async fn wait_for_stack_creation_to_finish(
cf_client: &aws_sdk_cloudformation::Client,
config: &aws_config::SdkConfig,
stack_name: &str,
) -> Result<()> {
let mut last_status: Option<StackStatus> = None;
let client = aws_sdk_cloudformation::Client::new(config);
let clear_line = "\x1b[2K";
let dots = [".", "..", "...", ""];
let mut last_status: Option<StackStatus> = None;
loop {
let stack_data = cloudformation::get_stack_data(cf_client, stack_name).await?;

let stack_data = cloudformation::get_stack_data(&client, stack_name).await?;
if let Some(ref status) = stack_data.status {
// Check if stack has reached a terminal state
match status {
StackStatus::CreateComplete => {
println!("{clear_line}{stack_data}");
Expand Down Expand Up @@ -182,7 +185,6 @@ async fn wait_for_stack_creation_to_finish(
}
}
}

Ok(())
}

Expand Down
2 changes: 2 additions & 0 deletions rust/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,6 @@ pub enum VaultError {
CreateStackError(#[from] SdkError<CreateStackError>),
#[error("Failed to get stack ID for new vault stack")]
MissingStackIdError,
#[error("Failed to get stack status for vault stack")]
MissingStackStatusError,
}
10 changes: 6 additions & 4 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ use crate::cloudformation::CloudFormationStackData;
use crate::errors::VaultError;

#[derive(Debug, Clone)]
/// Result data for initializing a new vault stack
/// Result data for initializing a new vault stack.
pub enum CreateStackResult {
AlreadyInitialized {
data: CloudFormationStackData,
},
/// Vault stack has already been initialized.
Exists { data: CloudFormationStackData },
/// Vault stack exists but is not in a usable state.
ExistsWithFailedState { data: CloudFormationStackData },
/// A new vault stack has been created.
Created {
stack_name: String,
stack_id: String,
Expand Down
4 changes: 2 additions & 2 deletions rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async fn main() -> Result<()> {
Command::Init { name } => {
cli::init_vault_stack(args.vault_stack.or(name), args.region, args.bucket)
.await
.with_context(|| "Failed to init vault stack")?;
.with_context(|| "Failed to init vault stack".red())?;
}
Command::Update { name } => {
let vault = Vault::new(
Expand All @@ -186,7 +186,7 @@ async fn main() -> Result<()> {
vault
.update_stack()
.await
.with_context(|| "Failed to update vault stack")?;
.with_context(|| "Failed to update vault stack".red())?;
}
Command::All {}
| Command::Delete { .. }
Expand Down
12 changes: 7 additions & 5 deletions rust/src/template.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use std::sync::LazyLock;

/// Cloudformation stack version.
pub const VAULT_STACK_VERSION: u32 = 25;

/// Return Cloudformation stack template JSON.
/// Workaround for accessing string inside `LazyLock`.
pub fn template() -> &'static str {
&TEMPLATE_STRING
}

static TEMPLATE_STRING: LazyLock<String> = LazyLock::new(|| {
let raw_template = r#"
{
Expand Down Expand Up @@ -521,11 +528,6 @@ static TEMPLATE_STRING: LazyLock<String> = LazyLock::new(|| {
)
});

/// Workaround for accessing string inside `LazyLock`.
pub fn template() -> &'static str {
&TEMPLATE_STRING
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
11 changes: 2 additions & 9 deletions rust/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Value {
}
}

/// Returns the data as a byte slice (`&[u8]`)
/// Returns the data as a byte slice `&[u8]`
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
match self {
Expand Down Expand Up @@ -79,14 +79,7 @@ impl Value {
pub fn output_to_file(&self, path: &Path) -> io::Result<()> {
let file = std::fs::File::create(path)?;
let mut writer = BufWriter::new(file);
match self {
Self::Utf8(ref string) => {
writer.write_all(string.as_bytes())?;
}
Self::Binary(ref bytes) => {
writer.write_all(bytes)?;
}
}
writer.write_all(self.as_bytes())?;
writer.flush()
}
}
Expand Down
20 changes: 16 additions & 4 deletions rust/src/vault.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use aes_gcm::aes::{cipher, Aes256};
use aes_gcm::{AesGcm, KeyInit, Nonce};
use aws_config::meta::region::RegionProviderChain;
use aws_config::{Region, SdkConfig};
use aws_sdk_cloudformation::types::{Capability, Parameter};
use aws_sdk_cloudformation::types::{Capability, Parameter, StackStatus};
use aws_sdk_cloudformation::Client as CloudFormationClient;
use aws_sdk_kms::primitives::Blob;
use aws_sdk_kms::types::DataKeySpec;
Expand Down Expand Up @@ -134,10 +134,22 @@ impl Vault {

let cf_client = CloudFormationClient::new(&config);

// TODO: Stack might technically exist but not be in a usable state.
// Check `StackStatus` is green and not in error state
if let Ok(data) = cloudformation::get_stack_data(&cf_client, &stack_name).await {
return Ok(CreateStackResult::AlreadyInitialized { data });
return if let Some(ref status) = data.status {
match status {
// Stack might exist but not be in a usable state.
StackStatus::CreateFailed
| StackStatus::RollbackFailed
| StackStatus::DeleteFailed
| StackStatus::DeleteInProgress
| StackStatus::DeleteComplete => {
Ok(CreateStackResult::ExistsWithFailedState { data })
}
_ => Ok(CreateStackResult::Exists { data }),
}
} else {
Err(VaultError::MissingStackStatusError)
};
}

let parameters = Parameter::builder()
Expand Down

0 comments on commit 63ecac7

Please sign in to comment.