From 443ddf68d2909466b4754ae5d54b92d749b6fac1 Mon Sep 17 00:00:00 2001 From: Akseli Lukkarila Date: Wed, 9 Oct 2024 13:02:31 +0300 Subject: [PATCH] rust: properly separate cli from library in vault init (#530) * rust: properly separate cli from library in vault init * drop anim duration to 600ms --- rust/Cargo.lock | 2 +- rust/Cargo.toml | 12 +-- rust/src/cli.rs | 77 ++++++++++++++- rust/src/cloudformation.rs | 165 ++++++++++++++++++++++++++++++++ rust/src/errors.rs | 5 +- rust/src/lib.rs | 128 +++++-------------------- rust/src/main.rs | 2 +- rust/src/vault.rs | 188 ++++++++----------------------------- 8 files changed, 314 insertions(+), 265 deletions(-) create mode 100644 rust/src/cloudformation.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f0c663a4..3dae6ee0 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "nitor-vault" -version = "1.0.0" +version = "1.1.0" dependencies = [ "aes-gcm", "anyhow", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 71910d68..26c8dd0c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nitor-vault" -version = "1.0.0" +version = "1.1.0" edition = "2021" description = "Encrypted AWS key-value storage utility." license = "Apache-2.0" @@ -14,13 +14,13 @@ authors = [ [dependencies] aes-gcm = "0.10.3" anyhow = "1.0.89" -aws-config = { version = "1.4.0", features = ["behavior-version-latest"] } -aws-sdk-cloudformation = "1.30.0" -aws-sdk-kms = "1.26.0" -aws-sdk-s3 = "1.29.0" +aws-config = { version = "1.5.8", features = ["behavior-version-latest"] } +aws-sdk-cloudformation = "1.50.0" +aws-sdk-kms = "1.46.0" +aws-sdk-s3 = "1.54.0" aws-sdk-sts = { version = "1.45.0", features = ["behavior-version-latest"] } base64 = "0.22.1" -clap = { version = "4.5.19", features = ["derive", "env"] } +clap = { version = "4.5.20", features = ["derive", "env"] } colored = "2.1.0" rand = "0.8.5" serde = { version = "1.0.210", features = ["derive"] } diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 02b95d29..3a5b8ffd 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -1,9 +1,39 @@ +use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use aws_sdk_cloudformation::types::StackStatus; use colored::Colorize; +use tokio::time::Duration; -use nitor_vault::{Value, Vault}; +use nitor_vault::{cloudformation, CreateStackResult, Value, Vault}; + +const INIT_WAIT_ANIMATION_DURATION: Duration = Duration::from_millis(600); + +pub async fn init_vault_stack( + stack_name: Option, + region: Option, + bucket: Option, +) -> Result<()> { + match Vault::init(stack_name, region, bucket).await? { + CreateStackResult::AlreadyInitialized { data } => { + println!("Vault stack already initialized"); + println!("{data}"); + } + 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?; + } + } + + Ok(()) +} /// Store a key-value pair pub async fn store( @@ -111,6 +141,51 @@ 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, + stack_name: &str, +) -> Result<()> { + let mut last_status: Option = None; + let clear_line = "\x1b[2K"; + let dots = [".", "..", "...", ""]; + loop { + let stack_data = cloudformation::get_stack_data(cf_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}"); + println!("{}", "Stack creation completed successfully".green()); + break; + } + StackStatus::CreateFailed + | StackStatus::RollbackFailed + | StackStatus::RollbackComplete => { + println!("{clear_line}{stack_data}"); + anyhow::bail!("Stack creation failed"); + } + _ => { + // Print status if it has changed + if last_status.as_ref() != Some(status) { + last_status = Some(status.clone()); + println!("status: {status}"); + } + // Continue waiting for stack creation to complete + for dot in &dots { + print!("\r{clear_line}{dot}"); + std::io::stdout().flush()?; + tokio::time::sleep(INIT_WAIT_ANIMATION_DURATION).await; + } + } + } + } + } + + Ok(()) +} + /// Try to get the filename for the given filepath fn get_filename_from_path(path: &str) -> Result { let path = Path::new(path); diff --git a/rust/src/cloudformation.rs b/rust/src/cloudformation.rs new file mode 100644 index 00000000..64edd4e7 --- /dev/null +++ b/rust/src/cloudformation.rs @@ -0,0 +1,165 @@ +use std::fmt; + +use aws_sdk_cloudformation::operation::describe_stacks::DescribeStacksOutput; +use aws_sdk_cloudformation::types::{Output, StackStatus}; + +use crate::errors::VaultError; + +#[derive(Debug, Clone, Default)] +/// Parameter values for Cloudformation resources. +pub struct CloudFormationParams { + pub bucket_name: String, + pub key_arn: Option, + pub stack_name: String, +} + +#[derive(Debug, Default, Clone)] +/// Cloudformation stack status information. +pub struct CloudFormationStackData { + pub bucket_name: Option, + pub key_arn: Option, + pub version: Option, + pub status: Option, + pub status_reason: Option, +} + +impl CloudFormationParams { + #[must_use] + /// Create `CloudFormationParams` from owned parameters. + pub const fn new(bucket_name: String, key_arn: Option, stack_name: String) -> Self { + Self { + bucket_name, + key_arn, + stack_name, + } + } + + #[must_use] + /// Create `CloudFormationParams` from references. + pub fn from(bucket_name: &str, key_arn: Option<&str>, stack_name: &str) -> Self { + Self { + bucket_name: bucket_name.to_owned(), + key_arn: key_arn.map(std::borrow::ToOwned::to_owned), + stack_name: stack_name.to_owned(), + } + } + + /// Get `CloudFormationParams` from Cloudformation describe stack output. + pub async fn from_stack( + client: &aws_sdk_cloudformation::Client, + stack: String, + ) -> Result { + let describe_stack_output = client + .describe_stacks() + .stack_name(stack.clone()) + .send() + .await?; + + let stack_output = describe_stack_output + .stacks() + .first() + .map(aws_sdk_cloudformation::types::Stack::outputs) + .ok_or(VaultError::StackOutputsMissingError)?; + + let bucket_name = Self::parse_output_value_from_key("vaultBucketName", stack_output) + .ok_or(VaultError::BucketNameMissingError)?; + + let key_arn = Self::parse_output_value_from_key("kmsKeyArn", stack_output); + + Ok(Self::new(bucket_name, key_arn, stack)) + } + + fn parse_output_value_from_key(key: &str, output: &[Output]) -> Option { + output + .iter() + .find(|output| output.output_key() == Some(key)) + .map(|output| output.output_value().unwrap_or_default().to_owned()) + } +} + +impl fmt::Display for CloudFormationParams { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "bucket: {}\nkey: {}\nstack: {}", + self.bucket_name, + self.key_arn.as_ref().map_or("None", |k| k), + self.stack_name + ) + } +} + +impl fmt::Display for CloudFormationStackData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "status: {}\nbucket: {}\nkey ARN: {}\nversion: {}{}", + self.status + .as_ref() + .map_or("None".to_string(), std::string::ToString::to_string), + self.bucket_name.as_deref().unwrap_or("None"), + self.key_arn.as_deref().unwrap_or("None"), + self.version.map_or("None".to_string(), |v| v.to_string()), + self.status_reason + .as_ref() + .map_or_else(String::new, |reason| format!("\nreason: {reason}")) + ) + } +} + +/// Extract relevant information from Cloudformation stack outputs +pub async fn get_stack_data( + cf_client: &aws_sdk_cloudformation::Client, + stack_name: &str, +) -> Result { + let stack_response = describe_stack(cf_client, stack_name).await?; + + let mut data = CloudFormationStackData::default(); + if let Some(stacks) = stack_response.stacks { + if let Some(stack) = stacks.first() { + data.status.clone_from(&stack.stack_status); + data.status_reason.clone_from(&stack.stack_status_reason); + if let Some(outputs) = &stack.outputs { + for output in outputs { + if let Some(output_key) = output.output_key() { + match output_key { + "vaultBucketName" => { + if let Some(output_value) = output.output_value() { + data.bucket_name = Some(output_value.to_string()); + } + } + "kmsKeyArn" => { + if let Some(output_value) = output.output_value() { + data.key_arn = Some(output_value.to_string()); + } + } + "vaultStackVersion" => { + if let Some(output_value) = output.output_value() { + if let Ok(version) = output_value.parse::() { + data.version = Some(version); + } + } + } + _ => {} + } + } + } + } + } + } + + Ok(data) +} + +// Get Cloudformation describe stack output. +pub async fn describe_stack( + cf_client: &aws_sdk_cloudformation::Client, + stack_name: &str, +) -> Result { + cf_client + .describe_stacks() + .stack_name(stack_name) + .send() + .await + .map_err(VaultError::from) +} diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 0682ac88..336383be 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -16,6 +16,7 @@ use aws_sdk_s3::operation::head_object::HeadObjectError; use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error; use aws_sdk_s3::operation::put_object::PutObjectError; use aws_sdk_sts::operation::get_caller_identity::GetCallerIdentityError; + use thiserror::Error; #[derive(Debug, Error)] @@ -86,6 +87,6 @@ pub enum VaultError { CallerIdError(#[from] SdkError), #[error("Failed to create stack: {0}")] CreateStackError(#[from] SdkError), - #[error("{0}")] - Error(String), + #[error("Failed to get stack ID for new vault stack")] + MissingStackIdError, } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3a3151bb..23e9bec8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,119 +1,71 @@ +pub mod cloudformation; pub mod errors; + mod template; mod value; mod vault; -use std::fmt; +// Expose `Vault` and `Value` so they can be used as if they were defined here +pub use crate::value::Value; +pub use crate::vault::Vault; -use aws_sdk_cloudformation::types::{Output, StackStatus}; -use aws_sdk_cloudformation::Client as CloudFormationClient; use aws_sdk_s3::types::ObjectIdentifier; use base64::Engine; use serde::{Deserialize, Serialize}; +use crate::cloudformation::CloudFormationStackData; use crate::errors::VaultError; -// Expose `Vault` and `Value` so they can be used as if they were defined here -pub use crate::value::Value; -pub use crate::vault::Vault; - #[derive(Debug, Clone)] -pub struct CloudFormationParams { - bucket_name: String, - key_arn: Option, - stack_name: String, -} - -#[derive(Debug, Default)] -pub struct CloudFormationStackData { - pub bucket_name: Option, - pub key_arn: Option, - pub version: Option, - pub status: Option, - pub status_reason: Option, +/// Result data for initializing a new vault stack +pub enum CreateStackResult { + AlreadyInitialized { + data: CloudFormationStackData, + }, + Created { + stack_name: String, + stack_id: String, + region: aws_config::Region, + }, } #[derive(Debug, Clone)] -struct EncryptObject { +pub(crate) struct EncryptObject { data_key: Vec, aes_gcm_ciphertext: Vec, meta: String, } #[derive(Debug, Serialize, Deserialize)] -struct Meta { +pub(crate) struct Meta { alg: String, nonce: String, } #[derive(Debug, Clone)] -struct S3DataKeys { +/// S3 object identifier names for a single value. +pub(crate) struct S3DataKeys { key: String, cipher: String, meta: String, } -impl CloudFormationParams { - #[must_use] - pub const fn new(bucket_name: String, key_arn: Option, stack_name: String) -> Self { - Self { - bucket_name, - key_arn, - stack_name, - } - } - - pub fn from(bucket_name: &str, key_arn: Option<&str>, stack_name: &str) -> Self { - Self { - bucket_name: bucket_name.to_owned(), - key_arn: key_arn.map(std::borrow::ToOwned::to_owned), - stack_name: stack_name.to_owned(), - } - } - - /// Get `CloudFormation` parameters based on config and stack name - async fn from_stack(client: &CloudFormationClient, stack: String) -> Result { - let describe_stack_output = client - .describe_stacks() - .stack_name(stack.clone()) - .send() - .await?; - - let stack_output = describe_stack_output - .stacks() - .first() - .map(aws_sdk_cloudformation::types::Stack::outputs) - .ok_or(VaultError::StackOutputsMissingError)?; - - let bucket_name = Self::parse_output_value_from_key("vaultBucketName", stack_output) - .ok_or(VaultError::BucketNameMissingError)?; - - let key_arn = Self::parse_output_value_from_key("kmsKeyArn", stack_output); - - Ok(Self::new(bucket_name, key_arn, stack)) - } - - fn parse_output_value_from_key(key: &str, out: &[Output]) -> Option { - out.iter() - .find(|output| output.output_key() == Some(key)) - .map(|output| output.output_value().unwrap_or_default().to_owned()) - } -} - impl Meta { - pub fn new(algorithm: &str, nonce: &[u8]) -> Self { + #[must_use] + fn new(algorithm: &str, nonce: &[u8]) -> Self { Self { alg: algorithm.to_owned(), nonce: base64::engine::general_purpose::STANDARD.encode(nonce), } } - pub fn aesgcm(nonce: &[u8]) -> Self { + #[must_use] + fn aesgcm(nonce: &[u8]) -> Self { Self::new("AESGCM", nonce) } /// Serialize Meta to JSON string. - pub fn to_json(&self) -> serde_json::Result { + fn to_json(&self) -> serde_json::Result { serde_json::to_string(&self) } } @@ -145,33 +97,3 @@ impl S3DataKeys { .collect() } } - -impl fmt::Display for CloudFormationParams { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "bucket: {}\nkey: {}\nstack: {}", - self.bucket_name, - self.key_arn.as_ref().map_or("None", |k| k), - self.stack_name - ) - } -} - -impl fmt::Display for CloudFormationStackData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "status: {}\nbucket: {}\nkey ARN: {}\nversion: {}{}", - self.status - .as_ref() - .map_or("None".to_string(), std::string::ToString::to_string), - self.bucket_name.as_deref().unwrap_or("None"), - self.key_arn.as_deref().unwrap_or("None"), - self.version.map_or("None".to_string(), |v| v.to_string()), - self.status_reason - .as_ref() - .map_or_else(String::new, |reason| format!("\nreason: {reason}")) - ) - } -} diff --git a/rust/src/main.rs b/rust/src/main.rs index f89942a3..5fd84b33 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -169,7 +169,7 @@ async fn main() -> Result<()> { if let Some(command) = args.command { match command { Command::Init { name } => { - Vault::init(args.vault_stack.or(name), args.region, args.bucket) + cli::init_vault_stack(args.vault_stack.or(name), args.region, args.bucket) .await .with_context(|| "Failed to init vault stack")?; } diff --git a/rust/src/vault.rs b/rust/src/vault.rs index a3bccafb..57077848 100644 --- a/rust/src/vault.rs +++ b/rust/src/vault.rs @@ -1,13 +1,11 @@ -use std::io::Write; use std::{env, fmt}; use aes_gcm::aead::{Aead, Payload}; use aes_gcm::aes::{cipher, Aes256}; use aes_gcm::{AesGcm, KeyInit, Nonce}; use aws_config::meta::region::RegionProviderChain; -use aws_config::Region; -use aws_sdk_cloudformation::operation::describe_stacks::DescribeStacksOutput; -use aws_sdk_cloudformation::types::{Capability, Parameter, StackStatus}; +use aws_config::{Region, SdkConfig}; +use aws_sdk_cloudformation::types::{Capability, Parameter}; use aws_sdk_cloudformation::Client as CloudFormationClient; use aws_sdk_kms::primitives::Blob; use aws_sdk_kms::types::DataKeySpec; @@ -18,16 +16,14 @@ use aws_sdk_s3::types::Delete; use aws_sdk_s3::Client as S3Client; use aws_sdk_sts::Client as stsClient; use base64::Engine; -use colored::Colorize; use rand::Rng; -use tokio::time::Duration; +use crate::cloudformation; +use crate::cloudformation::{CloudFormationParams, CloudFormationStackData}; use crate::errors::VaultError; use crate::template::{template, VAULT_STACK_VERSION}; use crate::value::Value; -use crate::{CloudFormationParams, CloudFormationStackData, EncryptObject, Meta, S3DataKeys}; - -const WAIT_ANIMATION_DURATION: Duration = Duration::from_millis(1000); +use crate::{CreateStackResult, EncryptObject, Meta, S3DataKeys}; #[derive(Debug)] pub struct Vault { @@ -43,17 +39,18 @@ pub struct Vault { } impl Vault { - /// Construct Vault with defaults for an existing stack. + /// Construct Vault for an existing vault stack with defaults. /// This will try reading environment variables for the config values, - /// and otherwise fall back to current AWS config. + /// and otherwise fall back to current AWS config and/or retrieve config values from the + /// Cloudformation stack description. /// /// The Default trait can't be implemented for Vault since it can fail. pub async fn default() -> Result { Self::new(None, None, None, None, None).await } - /// Construct Vault with optional arguments for an existing stack. - /// This will try reading environment variables for the config values if they are `None`. + /// Construct Vault for an existing vault stack with optional arguments. + /// This will try reading environment variables for the config values that are `None`. pub async fn new( vault_stack: Option, region: Option, @@ -61,11 +58,7 @@ impl Vault { key: Option, prefix: Option, ) -> Result { - let config = aws_config::from_env() - .region(get_region_provider(region)) - .load() - .await; - + let config = Self::get_aws_config(region).await; let region = config .region() .map(ToOwned::to_owned) @@ -103,17 +96,15 @@ impl Vault { }) } - /// Initialize new Vault stack + /// Initialize new Vault stack. + /// This will create all required resources in AWS, + /// after which the Vault can be used to store and lookup values. pub async fn init( vault_stack: Option, region: Option, bucket: Option, - ) -> Result<(), VaultError> { - let config = aws_config::from_env() - .region(get_region_provider(region)) - .load() - .await; - + ) -> Result { + let config = Self::get_aws_config(region).await; let region = config .region() .map(ToOwned::to_owned) @@ -143,12 +134,10 @@ impl Vault { let cf_client = CloudFormationClient::new(&config); - if Self::describe_stack(&cf_client, &stack_name).await.is_ok() { - println!("Vault stack '{stack_name}' already initialized"); - return match Self::get_cloudformation_stack_data(&cf_client, &stack_name).await { - Ok(stack_data) => Ok(println!("{stack_data}")), - Err(err) => Err(err), - }; + // 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 }); } let parameters = Parameter::builder() @@ -166,19 +155,18 @@ impl Vault { .await .map_err(VaultError::from)?; - if let Some(stack_id) = response.stack_id { - println!("{stack_id}"); - } + let stack_id = response.stack_id.ok_or(VaultError::MissingStackIdError)?; - println!("Waiting for Vault stack '{stack_name}' to be created"); - Self::wait_for_stack_creation(&cf_client, &stack_name).await?; - - Ok(()) + Ok(CreateStackResult::Created { + stack_name, + stack_id, + region, + }) } pub async fn update_stack(&self) -> Result<(), VaultError> { let stack_name = &self.cloudformation_params.stack_name; - let stack_data = Self::get_cloudformation_stack_data(&self.cf, stack_name).await?; + let stack_data = cloudformation::get_stack_data(&self.cf, stack_name).await?; println!("{stack_data}"); let deployed_version = stack_data .version @@ -213,8 +201,9 @@ impl Vault { Ok(()) } + /// Get Cloudformation stack status pub async fn stack_status(&self) -> Result { - Self::get_cloudformation_stack_data(&self.cf, &self.cloudformation_params.stack_name).await + cloudformation::get_stack_data(&self.cf, &self.cloudformation_params.stack_name).await } /// Get all available secrets @@ -242,7 +231,7 @@ impl Vault { .collect::>()) } - /// Get `CloudFormation` stack information + /// Get Cloudformation parameters #[must_use] pub fn stack_info(&self) -> CloudFormationParams { self.cloudformation_params.clone() @@ -341,6 +330,13 @@ impl Vault { } } + pub async fn get_aws_config(region: Option) -> SdkConfig { + aws_config::from_env() + .region(get_region_provider(region)) + .load() + .await + } + /// Get S3 Object data for given key as a vec of bytes async fn get_s3_object(&self, key: String) -> Result, VaultError> { self.s3 @@ -432,56 +428,6 @@ impl Vault { .await?) } - /// Poll Cloudformation for stack status until it has been created or creation failed. - async fn wait_for_stack_creation( - cf_client: &CloudFormationClient, - stack_name: &str, - ) -> Result<(), VaultError> { - let mut last_status: Option = None; - let clear_line = "\x1b[2K"; - let dots = [".", "..", "...", ""]; - loop { - match Self::get_cloudformation_stack_data(cf_client, stack_name).await { - Ok(stack_data) => { - 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}"); - println!("{}", "Stack creation completed successfully".green()); - break; - } - StackStatus::CreateFailed - | StackStatus::RollbackFailed - | StackStatus::RollbackComplete => { - println!("{clear_line}{stack_data}"); - return Err(VaultError::Error("Stack creation failed".to_string())); - } - _ => { - // Print status if it has changed - if last_status.as_ref() != Some(status) { - last_status = Some(status.clone()); - println!("status: {status}"); - } - // Continue waiting for stack creation to complete - for dot in &dots { - print!("\r{clear_line}{dot}"); - std::io::stdout().flush()?; - tokio::time::sleep(WAIT_ANIMATION_DURATION).await; - } - } - } - } - } - Err(err) => { - return Err(err); - } - } - } - - Ok(()) - } - /// Add prefix to key if prefix has been specified. fn full_key_name(&self, name: &str) -> String { if self.prefix.is_empty() { @@ -490,66 +436,6 @@ impl Vault { format!("{}{}", self.prefix, name) } } - - async fn describe_stack( - cf_client: &CloudFormationClient, - stack_name: &str, - ) -> Result { - cf_client - .describe_stacks() - .stack_name(stack_name) - .send() - .await - .map_err(VaultError::from) - } - - /// Extract relevant information from `CloudFormation` stack outputs - async fn get_cloudformation_stack_data( - cf_client: &CloudFormationClient, - stack_name: &str, - ) -> Result { - let stack_response = cf_client - .describe_stacks() - .stack_name(stack_name) - .send() - .await?; - - let mut data = CloudFormationStackData::default(); - if let Some(stacks) = stack_response.stacks { - if let Some(stack) = stacks.first() { - data.status.clone_from(&stack.stack_status); - data.status_reason.clone_from(&stack.stack_status_reason); - if let Some(outputs) = &stack.outputs { - for output in outputs { - if let Some(output_key) = output.output_key() { - match output_key { - "vaultBucketName" => { - if let Some(output_value) = output.output_value() { - data.bucket_name = Some(output_value.to_string()); - } - } - "kmsKeyArn" => { - if let Some(output_value) = output.output_value() { - data.key_arn = Some(output_value.to_string()); - } - } - "vaultStackVersion" => { - if let Some(output_value) = output.output_value() { - if let Ok(version) = output_value.parse::() { - data.version = Some(version); - } - } - } - _ => {} - } - } - } - } - } - } - - Ok(data) - } } impl fmt::Display for Vault {