diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3dae6ee0..e265a528 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "nitor-vault" -version = "1.1.0" +version = "1.2.0" dependencies = [ "aes-gcm", "anyhow", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 26c8dd0c..5e0a9a16 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -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" diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 3a5b8ffd..227c54fe 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -16,10 +16,16 @@ pub async fn init_vault_stack( bucket: Option, ) -> 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, @@ -27,11 +33,9 @@ pub async fn init_vault_stack( } => { 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(()) } @@ -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 = None; + let client = aws_sdk_cloudformation::Client::new(config); let clear_line = "\x1b[2K"; let dots = [".", "..", "...", ""]; + let mut last_status: Option = 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}"); @@ -182,7 +185,6 @@ async fn wait_for_stack_creation_to_finish( } } } - Ok(()) } diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 336383be..be861b89 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -89,4 +89,6 @@ pub enum VaultError { CreateStackError(#[from] SdkError), #[error("Failed to get stack ID for new vault stack")] MissingStackIdError, + #[error("Failed to get stack status for vault stack")] + MissingStackStatusError, } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 23e9bec8..de2521af 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -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, diff --git a/rust/src/main.rs b/rust/src/main.rs index 5fd84b33..ccd48573 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -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( @@ -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 { .. } diff --git a/rust/src/template.rs b/rust/src/template.rs index ca1179eb..e2a7ea5e 100644 --- a/rust/src/template.rs +++ b/rust/src/template.rs @@ -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 = LazyLock::new(|| { let raw_template = r#" { @@ -521,11 +528,6 @@ static TEMPLATE_STRING: LazyLock = LazyLock::new(|| { ) }); -/// Workaround for accessing string inside `LazyLock`. -pub fn template() -> &'static str { - &TEMPLATE_STRING -} - #[cfg(test)] mod test { use super::*; diff --git a/rust/src/value.rs b/rust/src/value.rs index 2a6314a0..3468d8f7 100644 --- a/rust/src/value.rs +++ b/rust/src/value.rs @@ -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 { @@ -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() } } diff --git a/rust/src/vault.rs b/rust/src/vault.rs index 57077848..28cf3ad6 100644 --- a/rust/src/vault.rs +++ b/rust/src/vault.rs @@ -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; @@ -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()