From e8794646c6cfb5a920c3ad32be932ef261b72a9a Mon Sep 17 00:00:00 2001 From: Tobias Krischer Date: Sun, 24 Aug 2025 15:36:49 +0200 Subject: [PATCH] feat: add support for openbao flavor --- src/exec.rs | 16 ++- src/helpers.rs | 69 +++++++++--- src/init.rs | 14 ++- src/main.rs | 31 +++-- src/show.rs | 12 +- src/unseal.rs | 6 +- src/upgrade.rs | 45 ++++++-- src/wait.rs | 44 ++++---- tests/00-setup.rs | 31 +++++ tests/01-openbao.rs | 77 +++++++++++++ tests/02-vault.rs | 77 +++++++++++++ tests/{e2e => common}/helm.rs | 18 +-- tests/common/mod.rs | 11 ++ tests/{e2e => common}/prepare.rs | 44 ++++++-- tests/{e2e => common}/setup.rs | 49 +++----- tests/common/show.rs | 21 ++++ tests/{e2e => common}/upgrade.rs | 106 ++++++++++-------- tests/common/values-openbao.yaml | 39 +++++++ .../values.yaml => common/values-vault.yaml} | 0 tests/e2e/main.rs | 5 - tests/e2e/show.rs | 23 ---- 21 files changed, 539 insertions(+), 199 deletions(-) create mode 100644 tests/00-setup.rs create mode 100644 tests/01-openbao.rs create mode 100644 tests/02-vault.rs rename tests/{e2e => common}/helm.rs (88%) create mode 100644 tests/common/mod.rs rename tests/{e2e => common}/prepare.rs (76%) rename tests/{e2e => common}/setup.rs (60%) create mode 100644 tests/common/show.rs rename tests/{e2e => common}/upgrade.rs (70%) create mode 100644 tests/common/values-openbao.yaml rename tests/{e2e/helm/values.yaml => common/values-vault.yaml} (100%) delete mode 100644 tests/e2e/main.rs delete mode 100644 tests/e2e/show.rs diff --git a/src/exec.rs b/src/exec.rs index 61cb79d..615b444 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -6,7 +6,7 @@ use secrecy::{ExposeSecret, Secret}; use std::collections::HashMap; use tokio::io::AsyncWriteExt; -use crate::{list_vault_pods, LABEL_KEY_VAULT_ACTIVE, LABEL_KEY_VAULT_SEALED}; +use crate::{list_vault_pods, Flavor}; #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] pub enum ExecIn { @@ -25,11 +25,11 @@ impl std::fmt::Display for ExecIn { } impl ExecIn { - pub fn to_label_selector(&self) -> String { + pub fn to_label_selector(&self, flavor: &str) -> String { match self { - ExecIn::Active => format!("{}=true", LABEL_KEY_VAULT_ACTIVE), - ExecIn::Standby => format!("{}=false", LABEL_KEY_VAULT_ACTIVE), - ExecIn::Sealed => format!("{}=true", LABEL_KEY_VAULT_SEALED), + ExecIn::Active => format!("{}=true", &format!("{}-active", flavor)), + ExecIn::Standby => format!("{}=false", &format!("{}-active", flavor)), + ExecIn::Sealed => format!("{}=true", &format!("{}-sealed", flavor)), } } } @@ -39,10 +39,14 @@ pub async fn exec( api: &Api, cmd: String, exec_in: ExecIn, + flavor: Flavor, env: HashMap>, ) -> anyhow::Result<()> { let pods = api - .list(&list_vault_pods().labels(&exec_in.to_label_selector())) + .list( + &list_vault_pods(&flavor.to_string()) + .labels(&exec_in.to_label_selector(&flavor.to_string())), + ) .await?; let pod = pods .items diff --git a/src/helpers.rs b/src/helpers.rs index 93aff67..1e8cf76 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -4,24 +4,21 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::{BytesBody, HttpForwarderService}; -pub const LABEL_KEY_VAULT_ACTIVE: &str = "vault-active"; -pub const LABEL_KEY_VAULT_SEALED: &str = "vault-sealed"; - -pub fn list_vault_pods() -> ListParams { - ListParams::default().labels("app.kubernetes.io/name=vault") +pub fn list_vault_pods(flavor: &str) -> ListParams { + ListParams::default().labels(&format!("app.kubernetes.io/name={}", flavor)) } /// Check if the vault pod is sealed based on its labels /// Returns an error if the pod does not have the expected labels -pub fn is_sealed(pod: &Pod) -> anyhow::Result { +pub fn is_sealed(pod: &Pod, flavor: &str) -> anyhow::Result { match pod.metadata.labels.as_ref() { None => Err(anyhow::anyhow!("pod does not have labels")), - Some(labels) => match labels.get(LABEL_KEY_VAULT_SEALED) { + Some(labels) => match labels.get(&format!("{}-sealed", flavor)) { Some(x) if x.as_str() == "true" => Ok(true), Some(x) if x.as_str() == "false" => Ok(false), _ => Err(anyhow::anyhow!( "pod does not have a {} label", - LABEL_KEY_VAULT_SEALED + &format!("{}-sealed", flavor) )), }, } @@ -29,15 +26,15 @@ pub fn is_sealed(pod: &Pod) -> anyhow::Result { /// Check if the vault pod is active based on its labels /// Returns an error if the pod does not have the expected labels -pub fn is_active(pod: &Pod) -> anyhow::Result { +pub fn is_active(pod: &Pod, flavor: &str) -> anyhow::Result { match pod.metadata.labels.as_ref() { None => Err(anyhow::anyhow!("pod does not have labels")), - Some(labels) => match labels.get(LABEL_KEY_VAULT_ACTIVE) { + Some(labels) => match labels.get(&format!("{}-active", flavor)) { Some(x) if x.as_str() == "true" => Ok(true), Some(x) if x.as_str() == "false" => Ok(false), _ => Err(anyhow::anyhow!( "pod does not have a {} label", - LABEL_KEY_VAULT_ACTIVE + &format!("{}-active", flavor) )), }, } @@ -49,11 +46,50 @@ pub struct PodApi { pub api: Api, tls: bool, domain: String, + pub flavor: Flavor, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Flavor { + OpenBao, + Vault, +} + +impl std::fmt::Display for Flavor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Flavor::OpenBao => write!(f, "openbao"), + Flavor::Vault => write!(f, "vault"), + } + } +} + +impl std::str::FromStr for Flavor { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "openbao" => Ok(Flavor::OpenBao), + "vault" => Ok(Flavor::Vault), + _ => Err(anyhow::anyhow!("invalid flavor: {}", s)), + } + } +} + +impl Flavor { + pub fn container_name(&self) -> String { + self.to_string() + } } impl PodApi { - pub fn new(api: Api, tls: bool, domain: String) -> Self { - Self { api, tls, domain } + pub fn new(api: Api, tls: bool, domain: String, flavor: Flavor) -> Self { + Self { + api, + tls, + domain, + flavor, + } } } @@ -91,10 +127,11 @@ impl PodApi { /// Wrapper around the kube::Api type for the Vault statefulset pub struct StatefulSetApi { pub api: Api, + pub flavor: Flavor, } -impl From> for StatefulSetApi { - fn from(api: Api) -> Self { - Self { api } +impl StatefulSetApi { + pub fn new(api: Api, flavor: Flavor) -> Self { + Self { api, flavor } } } diff --git a/src/init.rs b/src/init.rs index eed86ce..80ca059 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,7 +6,7 @@ use kube::Api; use secrecy::Secret; use tracing::*; -use crate::{init_request, raft_join_request, BytesBody, HttpRequest, PodApi, VAULT_PORT}; +use crate::{init_request, raft_join_request, BytesBody, Flavor, HttpRequest, PodApi, VAULT_PORT}; #[derive(Debug, serde::Serialize)] pub struct InitRequest { @@ -105,12 +105,17 @@ where } #[tracing::instrument(skip_all)] -pub async fn init(domain: String, api: &Api, pod_name: &str) -> anyhow::Result { +pub async fn init( + domain: String, + api: &Api, + flavor: Flavor, + pod_name: &str, +) -> anyhow::Result { let pod = api.get(pod_name).await?; info!("initializing: {}", pod_name); - let pods = PodApi::new(api.clone(), true, domain); + let pods = PodApi::new(api.clone(), true, domain, flavor); let mut pf = pods .http( pod.metadata @@ -158,6 +163,7 @@ pub async fn init(domain: String, api: &Api, pod_name: &str) -> anyhow::Res pub async fn raft_join( domain: String, api: &Api, + flavor: Flavor, pod_name: &str, join_to: &str, ) -> anyhow::Result<()> { @@ -172,7 +178,7 @@ pub async fn raft_join( join_to, ); - let pods = PodApi::new(api.clone(), true, domain); + let pods = PodApi::new(api.clone(), true, domain, flavor); let mut pf = pods .http( pod.metadata diff --git a/src/main.rs b/src/main.rs index d7290a5..78414c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use std::io; use std::str::FromStr; use tokio::task::spawn_blocking; use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; +use vault_mgmt_lib::Flavor; use vault_mgmt_lib::{ construct_table, is_statefulset_ready, GetUnsealKeys, GetUnsealKeysFromVault, StepDown, @@ -48,6 +49,17 @@ struct Cli { #[arg(long)] no_tls: bool, + /// Server Flavor + #[arg( + short = 'f', + long, + default_value = "vault", + value_parser = clap::builder::PossibleValuesParser::new( + ["openbao", "vault"] + ).map(|s| Flavor::from_str(&s).expect("invalid flavor")), + )] + flavor: Flavor, + /// Subcommand to run #[command(subcommand)] command: Commands, @@ -188,7 +200,7 @@ async fn main() -> anyhow::Result<()> { } Commands::Show {} => { let api = setup_api(&cli.namespace).await?; - let table = construct_table(&api).await?; + let table = construct_table(&api, cli.flavor).await?; table.printstd(); } @@ -200,18 +212,21 @@ async fn main() -> anyhow::Result<()> { } => { let api = setup_api(&cli.namespace).await?; let env = collect_env(env, env_keys)?; - exec(&api, cmd.join(" "), exec_in, env).await?; + exec(&api, cmd.join(" "), exec_in, cli.flavor, env).await?; } Commands::StepDown { token } => { let api = setup_api(&cli.namespace).await?; let active = api - .list(&list_vault_pods().labels(&ExecIn::Active.to_label_selector())) + .list( + &list_vault_pods(&cli.flavor.to_string()) + .labels(&ExecIn::Active.to_label_selector(&cli.flavor.to_string())), + ) .await?; let active = active.iter().next().ok_or(anyhow::anyhow!( "no active vault pod found. is vault sealed?" ))?; - PodApi::new(api, !cli.no_tls, cli.domain) + PodApi::new(api, !cli.no_tls, cli.domain, cli.flavor) .http( active .metadata @@ -240,7 +255,7 @@ async fn main() -> anyhow::Result<()> { key_cmd, } => { let api = setup_api(&cli.namespace).await?; - let sealed = list_sealed_pods(&api).await?; + let sealed = list_sealed_pods(&api, &cli.flavor.to_string()).await?; if sealed.is_empty() { return Ok(()); @@ -277,7 +292,7 @@ async fn main() -> anyhow::Result<()> { } for pod in sealed.iter() { - PodApi::new(api.clone(), !cli.no_tls, cli.domain.clone()) + PodApi::new(api.clone(), !cli.no_tls, cli.domain.clone(), cli.flavor) .http( pod.metadata .name @@ -333,10 +348,10 @@ async fn main() -> anyhow::Result<()> { let sts = stss.get(&cli.statefulset).await?; - StatefulSetApi::from(stss.clone()) + StatefulSetApi::new(stss.clone(), cli.flavor) .upgrade( sts.clone(), - &PodApi::new(pods.clone(), !cli.no_tls, cli.domain), + &PodApi::new(pods.clone(), !cli.no_tls, cli.domain, cli.flavor), token, !do_not_unseal, force_upgrade, diff --git a/src/show.rs b/src/show.rs index 0150592..bffb927 100644 --- a/src/show.rs +++ b/src/show.rs @@ -2,10 +2,10 @@ use k8s_openapi::api::core::v1::Pod; use kube::api::Api; use prettytable::{color, Attr, Cell, Row, Table}; -use crate::list_vault_pods; +use crate::{list_vault_pods, Flavor}; #[tracing::instrument(skip_all)] -pub async fn construct_table(api: &Api) -> anyhow::Result { +pub async fn construct_table(api: &Api, flavor: Flavor) -> anyhow::Result
{ let mut table = Table::new(); table.set_titles(row![ "NAME", @@ -17,7 +17,7 @@ pub async fn construct_table(api: &Api) -> anyhow::Result
{ "READY", ]); - let pods = api.list(&list_vault_pods()).await?; + let pods = api.list(&list_vault_pods(&flavor.to_string())).await?; let get_vault_label = |pod: &Pod, label: &str| match pod.metadata.labels { Some(ref labels) => labels @@ -51,7 +51,7 @@ pub async fn construct_table(api: &Api) -> anyhow::Result
{ .clone() .ok_or(anyhow::anyhow!("container does not have an image"))?; - let initialized = get_vault_label(p, "vault-initialized"); + let initialized = get_vault_label(p, &format!("{}-initialized", flavor)); let initialized = Cell::new(&initialized).with_style(Attr::ForegroundColor(match initialized.as_str() { "true" => color::GREEN, @@ -59,14 +59,14 @@ pub async fn construct_table(api: &Api) -> anyhow::Result
{ _ => color::YELLOW, })); - let sealed = get_vault_label(p, "vault-sealed"); + let sealed = get_vault_label(p, &format!("{}-sealed", flavor)); let sealed = Cell::new(&sealed).with_style(Attr::ForegroundColor(match sealed.as_str() { "true" => color::RED, "false" => color::GREEN, _ => color::YELLOW, })); - let active = get_vault_label(p, "vault-active"); + let active = get_vault_label(p, &format!("{}-active", flavor)); let active = Cell::new(&active).with_style(Attr::ForegroundColor(match active.as_str() { "true" => color::GREEN, "false" => color::WHITE, diff --git a/src/unseal.rs b/src/unseal.rs index 7053efc..e516e0c 100644 --- a/src/unseal.rs +++ b/src/unseal.rs @@ -28,9 +28,9 @@ pub async fn get_unseal_keys(key_cmd: &str) -> anyhow::Result } /// List all pods that are sealed -pub async fn list_sealed_pods(api: &Api) -> anyhow::Result> { +pub async fn list_sealed_pods(api: &Api, flavor: &str) -> anyhow::Result> { let pods = api - .list(&list_vault_pods().labels(&ExecIn::Sealed.to_label_selector())) + .list(&list_vault_pods(flavor).labels(&ExecIn::Sealed.to_label_selector(flavor))) .await?; Ok(pods.items) @@ -295,7 +295,7 @@ mod tests { async fn get_sealed_pods_returns_sealed_pods() { let (api, service, cancel) = setup().await; - let pods = list_sealed_pods(&api).await.unwrap(); + let pods = list_sealed_pods(&api, "vault").await.unwrap(); assert_eq!(pods.len(), 3); diff --git a/src/upgrade.rs b/src/upgrade.rs index 8e43999..523914e 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -49,13 +49,17 @@ impl PodApi { // if Pod version is outdated (or upgrade is forced) if !Self::is_current(&pod, target)? || force_upgrade { // if Pod is active - if is_active(&pod)? { + if is_active(&pod, &self.flavor.to_string())? { // Step down active pod self.http(name, VAULT_PORT).await?.step_down(token).await?; // Wait for other pod to take over - kube::runtime::wait::await_condition(self.api.clone(), name, is_pod_standby()) - .await?; + kube::runtime::wait::await_condition( + self.api.clone(), + name, + is_pod_standby(self.flavor), + ) + .await?; } // Delete pod @@ -79,7 +83,7 @@ impl PodApi { kube::runtime::wait::await_condition( self.api.clone(), name, - is_pod_exporting_seal_status(), + is_pod_exporting_seal_status(self.flavor), ) .await?; @@ -88,7 +92,7 @@ impl PodApi { if Self::is_current(&pod, target)? { // Pod is sealed - if is_sealed(&pod)? { + if is_sealed(&pod, &self.flavor.to_string())? { if should_unseal { let mut pf = Retry::spawn( ExponentialBackoff::from_millis(50).map(jitter).take(5), @@ -123,7 +127,12 @@ impl PodApi { } } // Wait for pod to be unsealed - kube::runtime::wait::await_condition(self.api.clone(), name, is_pod_unsealed()).await?; + kube::runtime::wait::await_condition( + self.api.clone(), + name, + is_pod_unsealed(self.flavor), + ) + .await?; // Wait for pod to be ready kube::runtime::wait::await_condition(self.api.clone(), name, is_pod_ready()).await?; } @@ -171,7 +180,10 @@ impl StatefulSetApi { let standby = pods .api - .list(&list_vault_pods().labels(&ExecIn::Standby.to_label_selector())) + .list( + &list_vault_pods(&self.flavor.to_string()) + .labels(&ExecIn::Standby.to_label_selector(&self.flavor.to_string())), + ) .await?; if standby.items.is_empty() { @@ -181,7 +193,10 @@ impl StatefulSetApi { let active = pods .api - .list(&list_vault_pods().labels(&ExecIn::Active.to_label_selector())) + .list( + &list_vault_pods(&self.flavor.to_string()) + .labels(&ExecIn::Active.to_label_selector(&self.flavor.to_string())), + ) .await?; if active.items.is_empty() { @@ -382,7 +397,12 @@ mod tests { let (api, service, cancel) = setup().await; - let pods = PodApi::new(api, false, "vault-mgmt-e2e".to_string()); + let pods = PodApi::new( + api, + false, + "vault-mgmt-e2e".to_string(), + crate::Flavor::Vault, + ); let pod = pods.api.get("vault-mgmt-e2e-2274-1").await.unwrap(); @@ -412,7 +432,12 @@ mod tests { let (api, service, cancel) = setup().await; - let pods = PodApi::new(api, false, "vault-mgmt-e2e".to_string()); + let pods = PodApi::new( + api, + false, + "vault-mgmt-e2e".to_string(), + crate::Flavor::Vault, + ); let pod = pods.api.get("vault-mgmt-e2e-2274-1").await.unwrap(); diff --git a/src/wait.rs b/src/wait.rs index 37084d6..953dfdb 100644 --- a/src/wait.rs +++ b/src/wait.rs @@ -1,6 +1,8 @@ use k8s_openapi::api::{apps::v1::StatefulSet, core::v1::Pod}; use kube::runtime::wait::Condition; +use crate::Flavor; + /// Returns true if the StatefulSet is considered ready. /// This means that all replicas are available and ready. #[must_use] @@ -38,7 +40,7 @@ pub fn is_statefulset_updated() -> impl Condition { /// Returns true if the StatefulSet template is using the given version. #[must_use] -pub fn statefulset_has_version(version: String) -> impl Condition { +pub fn statefulset_has_version(version: String, flavor: Flavor) -> impl Condition { move |obj: Option<&StatefulSet>| { if let Some(sts) = &obj { if let Some(spec) = &sts.spec { @@ -47,7 +49,7 @@ pub fn statefulset_has_version(version: String) -> impl Condition { .containers .iter() .filter_map(|c| { - if c.name == "vault" { + if c.name == flavor.to_string() { c.image.clone() } else { None @@ -79,13 +81,15 @@ pub fn is_pod_ready() -> impl Condition { } /// Returns true if the Pod has the seal status label. -/// This is determined by looking if the `vault-sealed` label exists. +/// This is determined by looking if the `-sealed` label exists. #[must_use] -pub fn is_pod_exporting_seal_status() -> impl Condition { - |obj: Option<&Pod>| { +pub fn is_pod_exporting_seal_status(flavor: Flavor) -> impl Condition { + move |obj: Option<&Pod>| { if let Some(pod) = &obj { if let Some(labels) = &pod.metadata.labels { - return labels.get("vault-sealed").is_some(); + return labels + .get(&format!("{}-sealed", flavor)) + .is_some(); } } false @@ -93,20 +97,20 @@ pub fn is_pod_exporting_seal_status() -> impl Condition { } /// Returns true if the Pod is unsealed. -/// This is determined by looking at the `vault-sealed` label. +/// This is determined by looking at the `-sealed` label. #[must_use] -pub fn is_pod_unsealed() -> impl Condition { - Condition::not(is_pod_sealed()) +pub fn is_pod_unsealed(flavor: Flavor) -> impl Condition { + Condition::not(is_pod_sealed(flavor)) } /// Returns true if the Pod is sealed. -/// This is determined by looking at the `vault-sealed` label. +/// This is determined by looking at the `-sealed` label. #[must_use] -pub fn is_pod_sealed() -> impl Condition { - |obj: Option<&Pod>| { +pub fn is_pod_sealed(flavor: Flavor) -> impl Condition { + move |obj: Option<&Pod>| { if let Some(pod) = &obj { if let Some(labels) = &pod.metadata.labels { - if let Some(sealed) = labels.get("vault-sealed") { + if let Some(sealed) = labels.get(&format!("{}-sealed", flavor)) { return sealed.as_str() == "true"; } } @@ -116,13 +120,13 @@ pub fn is_pod_sealed() -> impl Condition { } /// Returns true if the Pod is the active replica. -/// This is determined by looking at the `vault-active` label. +/// This is determined by looking at the `-active` label. #[must_use] -pub fn is_pod_active() -> impl Condition { - |obj: Option<&Pod>| { +pub fn is_pod_active(flavor: Flavor) -> impl Condition { + move |obj: Option<&Pod>| { if let Some(pod) = &obj { if let Some(labels) = &pod.metadata.labels { - if let Some(active) = labels.get("vault-active") { + if let Some(active) = labels.get(&format!("{}-active", flavor)) { return active.as_str() == "true"; } } @@ -132,10 +136,10 @@ pub fn is_pod_active() -> impl Condition { } /// Returns true if the Pod is a standby replica. -/// This is determined by looking at the `vault-active` label. +/// This is determined by looking at the `-active` label. #[must_use] -pub fn is_pod_standby() -> impl Condition { - Condition::not(is_pod_active()) +pub fn is_pod_standby(flavor: Flavor) -> impl Condition { + Condition::not(is_pod_active(flavor)) } #[cfg(test)] diff --git a/tests/00-setup.rs b/tests/00-setup.rs new file mode 100644 index 0000000..8548bbb --- /dev/null +++ b/tests/00-setup.rs @@ -0,0 +1,31 @@ +use k8s_openapi::api::core::v1::Pod; +use kube::{Api, Client}; +use tokio::process::Command; + +pub mod common; + +use common::get_namespace; + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn kube_connection_succeeds() { + common::setup_crypto_provider().await; + + let client = Client::try_default().await.unwrap(); + let pods: Api = Api::namespaced(client, &get_namespace()); + + pods.list(&Default::default()).await.unwrap(); +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn helm_cli_available() { + let helm = which::which("helm").unwrap(); + + let output = Command::new(helm).arg("version").output().await.unwrap(); + + assert!(output.status.success()); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("version.BuildInfo")); +} diff --git a/tests/01-openbao.rs b/tests/01-openbao.rs new file mode 100644 index 0000000..5d8903a --- /dev/null +++ b/tests/01-openbao.rs @@ -0,0 +1,77 @@ +use vault_mgmt_lib::Flavor; + +pub mod common; + +pub(crate) const VERSION_OLD: &str = "2.2.1"; +pub(crate) const VERSION_CURRENT: &str = "2.3.2"; +pub(crate) const IMAGE_NAME: &str = "openbao/openbao"; + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_show_succeeds() { + common::show_succeeds(VERSION_CURRENT, Flavor::OpenBao).await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_upgrade_pod_succeeds_if_already_current() { + common::upgrade_pod_succeeds_if_already_current(VERSION_CURRENT, Flavor::OpenBao).await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_upgrade_pod_succeeds_if_already_current_with_force_upgrade() { + common::upgrade_pod_succeeds_if_already_current_with_force_upgrade( + VERSION_CURRENT, + Flavor::OpenBao, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_upgrade_pod_succeeds_if_outdated_and_standby() { + common::upgrade_pod_succeeds_if_outdated_and_standby( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::OpenBao, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_upgrade_pod_succeeds_if_outdated_and_active() { + common::upgrade_pod_succeeds_if_outdated_and_active( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::OpenBao, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_upgrade_pod_succeeds_fails_with_missing_external_unseal() { + common::upgrade_pod_succeeds_fails_with_missing_external_unseal( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::OpenBao, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn openbao_upgrade_pod_succeeds_with_external_unseal() { + common::upgrade_pod_succeeds_with_external_unseal( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::OpenBao, + ) + .await; +} diff --git a/tests/02-vault.rs b/tests/02-vault.rs new file mode 100644 index 0000000..f350676 --- /dev/null +++ b/tests/02-vault.rs @@ -0,0 +1,77 @@ +use vault_mgmt_lib::Flavor; + +pub mod common; + +pub(crate) const VERSION_OLD: &str = "1.16.0"; +pub(crate) const VERSION_CURRENT: &str = "1.17.0"; +pub(crate) const IMAGE_NAME: &str = "hashicorp/vault"; + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_show_succeeds() { + common::show_succeeds(VERSION_CURRENT, Flavor::Vault).await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_upgrade_pod_succeeds_if_already_current() { + common::upgrade_pod_succeeds_if_already_current(VERSION_CURRENT, Flavor::Vault).await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_upgrade_pod_succeeds_if_already_current_with_force_upgrade() { + common::upgrade_pod_succeeds_if_already_current_with_force_upgrade( + VERSION_CURRENT, + Flavor::Vault, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_upgrade_pod_succeeds_if_outdated_and_standby() { + common::upgrade_pod_succeeds_if_outdated_and_standby( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::Vault, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_upgrade_pod_succeeds_if_outdated_and_active() { + common::upgrade_pod_succeeds_if_outdated_and_active( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::Vault, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_upgrade_pod_succeeds_fails_with_missing_external_unseal() { + common::upgrade_pod_succeeds_fails_with_missing_external_unseal( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::Vault, + ) + .await; +} + +#[ignore = "needs a running kubernetes cluster and the helm cli"] +#[tokio::test] +async fn vault_upgrade_pod_succeeds_with_external_unseal() { + common::upgrade_pod_succeeds_with_external_unseal( + VERSION_OLD, + VERSION_CURRENT, + IMAGE_NAME, + Flavor::Vault, + ) + .await; +} diff --git a/tests/e2e/helm.rs b/tests/common/helm.rs similarity index 88% rename from tests/e2e/helm.rs rename to tests/common/helm.rs index 6d4e305..1405fed 100644 --- a/tests/e2e/helm.rs +++ b/tests/common/helm.rs @@ -1,5 +1,6 @@ use std::process::Stdio; use tokio::{io::AsyncWriteExt, process::Command}; +use vault_mgmt_lib::Flavor; pub async fn add_repo() -> anyhow::Result { let helm = which::which("helm")?; @@ -8,8 +9,8 @@ pub async fn add_repo() -> anyhow::Result { .args([ "repo", "add", - "hashicorp", - "https://helm.releases.hashicorp.com", + "openbao", + "https://openbao.github.io/openbao-helm", ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -33,6 +34,8 @@ pub async fn install_chart( namespace: &str, name: &str, version: Option<&str>, + flavor: Flavor, + values: String, ) -> anyhow::Result { let helm = which::which("helm")?; @@ -42,7 +45,10 @@ pub async fn install_chart( "upgrade", "--install", name, - "hashicorp/vault", + match flavor { + Flavor::OpenBao => "openbao/openbao", + Flavor::Vault => "hashicorp/vault", + }, "--namespace", namespace, "-f", @@ -67,11 +73,9 @@ pub async fn install_chart( .spawn()?; let mut stdin = cmd.stdin.take().expect("failed to take stdin"); + tokio::spawn(async move { - stdin - .write_all(include_str!("helm/values.yaml").as_bytes()) - .await - .unwrap(); + stdin.write_all(values.as_bytes()).await.unwrap(); }); let output = cmd.wait_with_output().await?; diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..3a7f40b --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,11 @@ +mod helm; +mod prepare; +mod setup; +mod show; +mod upgrade; + +pub use helm::*; +pub use prepare::*; +pub use setup::*; +pub use show::*; +pub use upgrade::*; diff --git a/tests/e2e/prepare.rs b/tests/common/prepare.rs similarity index 76% rename from tests/e2e/prepare.rs rename to tests/common/prepare.rs index 2f08924..fbbb756 100644 --- a/tests/e2e/prepare.rs +++ b/tests/common/prepare.rs @@ -57,9 +57,9 @@ pub async fn init_unseal_cluster( false } - if !has_label(pod, "vault-initialized", None) - || !has_label(pod, "vault-sealed", None) - || !has_label(pod, "vault-active", None) + if !has_label(pod, "openbao-initialized", None) + || !has_label(pod, "openbao-sealed", None) + || !has_label(pod, "openbao-active", None) { return false; } @@ -83,33 +83,57 @@ pub async fn init_unseal_cluster( let first = format!("{}-0", &name); - let mut pf = PodApi::new(pods.clone(), false, "".to_string()) - .http(&first, VAULT_PORT) - .await - .unwrap(); + let mut pf = PodApi::new( + pods.clone(), + false, + "".to_string(), + vault_mgmt_lib::Flavor::OpenBao, + ) + .http(&first, VAULT_PORT) + .await + .unwrap(); // initialize vault let init_result = pf.init(InitRequest::default()).await?; + println!("init_result: {:#?}", init_result); + pf.await_seal_status(is_seal_status_initialized()).await?; + println!("seal status initialized"); + // unseal vault pf.unseal(&init_result.keys).await?; + println!("first pod unsealed"); + kube::runtime::wait::await_condition(pods.clone(), &first, is_pod_ready()).await?; + println!("first pod ready"); + // unseal other pods for pod in [1, 2] { - let mut pf = PodApi::new(pods.clone(), false, "".to_string()) - .http(&format!("{}-{}", &name, pod), VAULT_PORT) - .await?; + let mut pf = PodApi::new( + pods.clone(), + false, + "".to_string(), + vault_mgmt_lib::Flavor::OpenBao, + ) + .http(&format!("{}-{}", &name, pod), VAULT_PORT) + .await?; pf.await_seal_status(is_seal_status_initialized()).await?; + println!("pod {} seal status initialized", pod); + pf.unseal(&init_result.keys).await?; + + println!("pod {} unsealed", pod); } kube::runtime::wait::await_condition(stss.clone(), name, is_statefulset_ready()).await?; + println!("statefulset {} ready", name); + Ok(init_result) } diff --git a/tests/e2e/setup.rs b/tests/common/setup.rs similarity index 60% rename from tests/e2e/setup.rs rename to tests/common/setup.rs index 9dc9ed7..d8f6bc1 100644 --- a/tests/e2e/setup.rs +++ b/tests/common/setup.rs @@ -1,24 +1,20 @@ use k8s_openapi::api::{apps::v1::StatefulSet, core::v1::Pod}; use kube::{Api, Client}; use secrecy::ExposeSecret; -use tokio::{process::Command, sync::OnceCell}; +use tokio::sync::OnceCell; use vault_mgmt_lib::{ - raft_configuration_all_voters, GetRaftConfiguration, InitResult, PodApi, VAULT_PORT, + raft_configuration_all_voters, Flavor, GetRaftConfiguration, InitResult, PodApi, VAULT_PORT, }; -use crate::{helm, prepare}; +use super::{helm, prepare}; pub(crate) fn get_namespace() -> String { std::env::var("VAULT_MGMT_E2E_NAMESPACE").unwrap_or_else(|_| "vault-mgmt-e2e".to_string()) } -pub(crate) const VAULT_VERSION_OLD: &str = "1.16.0"; -pub(crate) const VAULT_VERSION_CURRENT: &str = "1.17.0"; -pub(crate) const VAULT_IMAGE_NAME: &str = "hashicorp/vault"; - static ONCE_CRYPTO_SETUP: OnceCell<()> = OnceCell::const_new(); -pub(crate) async fn setup_crypto_provider() { +pub async fn setup_crypto_provider() { ONCE_CRYPTO_SETUP .get_or_init(|| async { rustls::crypto::ring::default_provider() @@ -29,6 +25,7 @@ pub(crate) async fn setup_crypto_provider() { } pub(crate) async fn setup( + flavor: Flavor, prefix: &str, version: &str, ) -> ( @@ -47,8 +44,13 @@ pub(crate) async fn setup( let suffix = rand::random::(); let name = dbg!(format!("{}-{}", prefix, suffix)); + let values = match flavor { + Flavor::OpenBao => include_str!("values-openbao.yaml").to_string(), + Flavor::Vault => include_str!("values-vault.yaml").to_string(), + }; + helm::add_repo().await.unwrap(); - helm::install_chart(&namespace, &name, Some(version)) + helm::install_chart(&namespace, &name, Some(version), flavor, values) .await .unwrap(); @@ -68,7 +70,12 @@ pub(crate) async fn setup( &init.root_token.expose_secret() ); - let pod_api = PodApi::new(pods.clone(), false, "".to_string()); + let pod_api = PodApi::new( + pods.clone(), + false, + "".to_string(), + vault_mgmt_lib::Flavor::OpenBao, + ); let mut pf = pod_api .http(&format!("{}-0", name), VAULT_PORT) @@ -85,25 +92,3 @@ pub(crate) async fn setup( pub(crate) async fn teardown(namespace: &str, name: &str) { helm::uninstall_chart(namespace, name).await.unwrap(); } - -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn kube_connection_succeeds() { - let client = Client::try_default().await.unwrap(); - let pods: Api = Api::namespaced(client, &get_namespace()); - - pods.list(&Default::default()).await.unwrap(); -} - -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn helm_cli_available() { - let helm = which::which("helm").unwrap(); - - let output = Command::new(helm).arg("version").output().await.unwrap(); - - assert!(output.status.success()); - - let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("version.BuildInfo")); -} diff --git a/tests/common/show.rs b/tests/common/show.rs new file mode 100644 index 0000000..c56b7b9 --- /dev/null +++ b/tests/common/show.rs @@ -0,0 +1,21 @@ +use vault_mgmt_lib::{construct_table, Flavor}; + +use super::setup::{setup, teardown}; + +pub async fn show_succeeds(version_current: &str, flavor: Flavor) { + let (namespace, name, pods, _, _, _) = setup(flavor, "show", version_current).await; + + let table = construct_table(&pods, flavor).await.unwrap(); + + let mut buf = Vec::new(); + table.print(&mut buf).unwrap(); + let output = std::str::from_utf8(buf.as_slice()).unwrap().to_string(); + + if !output.contains(version_current) { + table.printstd(); + + assert!(output.contains(version_current)); + } + + teardown(&namespace, &name).await; +} diff --git a/tests/e2e/upgrade.rs b/tests/common/upgrade.rs similarity index 70% rename from tests/e2e/upgrade.rs rename to tests/common/upgrade.rs index 8cb4fc3..084562b 100644 --- a/tests/e2e/upgrade.rs +++ b/tests/common/upgrade.rs @@ -6,14 +6,13 @@ use kube::{ ResourceExt, }; -use vault_mgmt_lib::{is_pod_sealed, Unseal, VaultVersion, VAULT_PORT}; +use vault_mgmt_lib::{is_pod_sealed, Flavor, Unseal, VaultVersion, VAULT_PORT}; -use crate::setup::{setup, teardown, VAULT_IMAGE_NAME, VAULT_VERSION_CURRENT, VAULT_VERSION_OLD}; +use super::setup::{setup, teardown}; -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn upgrade_pod_succeeds_if_already_current() { - let (namespace, name, _, stss, init, pods) = setup("upgrade-noop", VAULT_VERSION_CURRENT).await; +pub async fn upgrade_pod_succeeds_if_already_current(version_current: &str, flavor: Flavor) { + let (namespace, name, _, stss, init, pods) = + setup(flavor, "upgrade-noop", version_current).await; let sts = stss.get(&name).await.unwrap(); let pod = pods.api.get(&format!("{}-0", name)).await.unwrap(); @@ -32,11 +31,12 @@ async fn upgrade_pod_succeeds_if_already_current() { teardown(&namespace, &name).await; } -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn upgrade_pod_succeeds_if_already_current_with_force_upgrade() { +pub async fn upgrade_pod_succeeds_if_already_current_with_force_upgrade( + version_current: &str, + flavor: Flavor, +) { let (namespace, name, _, stss, init, pods) = - setup("upgrade-force", VAULT_VERSION_CURRENT).await; + setup(flavor, "upgrade-force", version_current).await; let sts = stss.get(&name).await.unwrap(); let pod = pods.api.get(&format!("{}-1", name)).await.unwrap(); @@ -55,11 +55,14 @@ async fn upgrade_pod_succeeds_if_already_current_with_force_upgrade() { teardown(&namespace, &name).await; } -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn upgrade_pod_succeeds_if_outdated_and_standby() { +pub async fn upgrade_pod_succeeds_if_outdated_and_standby( + version_old: &str, + version_current: &str, + image_name: &str, + flavor: vault_mgmt_lib::Flavor, +) { let (namespace, name, _, stss, init, pods) = - setup("upgrade-outdated-stby", VAULT_VERSION_OLD).await; + setup(flavor, "upgrade-outdated-stby", version_old).await; match stss.entry(&name).await.unwrap() { Entry::Occupied(sts) => { @@ -75,10 +78,9 @@ async fn upgrade_pod_succeeds_if_outdated_and_standby() { .containers .iter_mut() { - if container.name == "vault" { - container.image = Some( - format!("{}:{}", VAULT_IMAGE_NAME, VAULT_VERSION_CURRENT).to_string(), - ); + if container.name == flavor.container_name() { + container.image = + Some(format!("{}:{}", image_name, version_current).to_string()); } } }) @@ -94,7 +96,7 @@ async fn upgrade_pod_succeeds_if_outdated_and_standby() { assert_eq!( VaultVersion::try_from(&pod).unwrap(), - VaultVersion::from_str(VAULT_VERSION_OLD).unwrap() + VaultVersion::from_str(version_old).unwrap() ); pods.upgrade( @@ -111,17 +113,20 @@ async fn upgrade_pod_succeeds_if_outdated_and_standby() { let pod = pods.api.get(&format!("{}-1", name)).await.unwrap(); assert_eq!( VaultVersion::try_from(&pod).unwrap(), - VaultVersion::from_str(VAULT_VERSION_CURRENT).unwrap() + VaultVersion::from_str(version_current).unwrap() ); teardown(&namespace, &name).await; } -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn upgrade_pod_succeeds_if_outdated_and_active() { +pub async fn upgrade_pod_succeeds_if_outdated_and_active( + version_old: &str, + version_current: &str, + image_name: &str, + flavor: vault_mgmt_lib::Flavor, +) { let (namespace, name, _, stss, init, pods) = - setup("upgrade-outdated-act", VAULT_VERSION_OLD).await; + setup(flavor, "upgrade-outdated-act", version_old).await; match stss.entry(&name).await.unwrap() { Entry::Occupied(sts) => { @@ -137,10 +142,9 @@ async fn upgrade_pod_succeeds_if_outdated_and_active() { .containers .iter_mut() { - if container.name == "vault" { - container.image = Some( - format!("{}:{}", VAULT_IMAGE_NAME, VAULT_VERSION_CURRENT).to_string(), - ); + if container.name == flavor.container_name() { + container.image = + Some(format!("{}:{}", image_name, version_current).to_string()); } } }) @@ -156,7 +160,7 @@ async fn upgrade_pod_succeeds_if_outdated_and_active() { assert_eq!( VaultVersion::try_from(&pod).unwrap(), - VaultVersion::from_str(VAULT_VERSION_OLD).unwrap() + VaultVersion::from_str(version_old).unwrap() ); pods.upgrade( @@ -173,17 +177,20 @@ async fn upgrade_pod_succeeds_if_outdated_and_active() { let pod = pods.api.get(&format!("{}-0", name)).await.unwrap(); assert_eq!( VaultVersion::try_from(&pod).unwrap(), - VaultVersion::from_str(VAULT_VERSION_CURRENT).unwrap() + VaultVersion::from_str(version_current).unwrap() ); teardown(&namespace, &name).await; } -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn upgrade_pod_succeeds_fails_with_missing_external_unseal() { +pub async fn upgrade_pod_succeeds_fails_with_missing_external_unseal( + version_old: &str, + version_current: &str, + image_name: &str, + flavor: vault_mgmt_lib::Flavor, +) { let (namespace, name, _, stss, init, pods) = - setup("upgrade-miss-ext-unseal", VAULT_VERSION_OLD).await; + setup(flavor, "upgrade-miss-ext-unseal", version_old).await; match stss.entry(&name).await.unwrap() { Entry::Occupied(sts) => { @@ -199,10 +206,9 @@ async fn upgrade_pod_succeeds_fails_with_missing_external_unseal() { .containers .iter_mut() { - if container.name == "vault" { - container.image = Some( - format!("{}:{}", VAULT_IMAGE_NAME, VAULT_VERSION_CURRENT).to_string(), - ); + if container.name == flavor.container_name() { + container.image = + Some(format!("{}:{}", image_name, version_current).to_string()); } } }) @@ -218,7 +224,7 @@ async fn upgrade_pod_succeeds_fails_with_missing_external_unseal() { assert_eq!( VaultVersion::try_from(&pod).unwrap(), - VaultVersion::from_str(VAULT_VERSION_OLD).unwrap() + VaultVersion::from_str(version_old).unwrap() ); tokio::time::timeout( @@ -238,11 +244,14 @@ async fn upgrade_pod_succeeds_fails_with_missing_external_unseal() { teardown(&namespace, &name).await; } -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn upgrade_pod_succeeds_with_external_unseal() { +pub async fn upgrade_pod_succeeds_with_external_unseal( + version_old: &str, + version_current: &str, + image_name: &str, + flavor: vault_mgmt_lib::Flavor, +) { let (namespace, name, _, stss, init, pods) = - setup("upgrade-with-ext-unseal", VAULT_VERSION_OLD).await; + setup(flavor, "upgrade-with-ext-unseal", version_old).await; match stss.entry(&name).await.unwrap() { Entry::Occupied(sts) => { @@ -258,10 +267,9 @@ async fn upgrade_pod_succeeds_with_external_unseal() { .containers .iter_mut() { - if container.name == "vault" { - container.image = Some( - format!("{}:{}", VAULT_IMAGE_NAME, VAULT_VERSION_CURRENT).to_string(), - ); + if container.name == flavor.container_name() { + container.image = + Some(format!("{}:{}", image_name, version_current).to_string()); } } }) @@ -277,7 +285,7 @@ async fn upgrade_pod_succeeds_with_external_unseal() { assert_eq!( VaultVersion::try_from(&pod).unwrap(), - VaultVersion::from_str(VAULT_VERSION_OLD).unwrap() + VaultVersion::from_str(version_old).unwrap() ); let pods_unseal = pods.clone(); @@ -297,7 +305,7 @@ async fn upgrade_pod_succeeds_with_external_unseal() { kube::runtime::wait::await_condition( pods_unseal.api.clone(), &format!("{}-1", &name_unseal), - is_pod_sealed(), + is_pod_sealed(flavor), ) .await .unwrap(); diff --git a/tests/common/values-openbao.yaml b/tests/common/values-openbao.yaml new file mode 100644 index 0000000..b6a5db6 --- /dev/null +++ b/tests/common/values-openbao.yaml @@ -0,0 +1,39 @@ +fullnameOverride: "" # set by tests + +server: + affinity: "" + ha: + enabled: true + raft: + enabled: true + config: | + ui = true + + log_requests_level = "trace" + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + } + + storage "raft" { + path = "/openbao/data" + + retry_join { + auto_join = "provider=k8s label_selector=\"app.kubernetes.io/name=openbao,component=server,app.kubernetes.io/instance={{ .Release.Name }}\" namespace=\"{{ .Release.Namespace }}\"" + auto_join_scheme = "http" + } + } + + service_registration "kubernetes" {} + dataStorage: + enabled: false + volumes: + - name: data + emptyDir: {} + volumeMounts: + - name: data + mountPath: /openbao/data +injector: + enabled: false diff --git a/tests/e2e/helm/values.yaml b/tests/common/values-vault.yaml similarity index 100% rename from tests/e2e/helm/values.yaml rename to tests/common/values-vault.yaml diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs deleted file mode 100644 index 9a60012..0000000 --- a/tests/e2e/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod helm; -mod prepare; -mod setup; -mod show; -mod upgrade; diff --git a/tests/e2e/show.rs b/tests/e2e/show.rs deleted file mode 100644 index 8433865..0000000 --- a/tests/e2e/show.rs +++ /dev/null @@ -1,23 +0,0 @@ -use vault_mgmt_lib::construct_table; - -use crate::setup::{setup, teardown, VAULT_VERSION_CURRENT}; - -#[ignore = "needs a running kubernetes cluster and the helm cli"] -#[tokio::test] -async fn show_succeeds() { - let (namespace, name, pods, _, _, _) = setup("show", VAULT_VERSION_CURRENT).await; - - let table = construct_table(&pods).await.unwrap(); - - let mut buf = Vec::new(); - table.print(&mut buf).unwrap(); - let output = std::str::from_utf8(buf.as_slice()).unwrap().to_string(); - - if !output.contains(VAULT_VERSION_CURRENT) { - table.printstd(); - - assert!(output.contains(VAULT_VERSION_CURRENT)); - } - - teardown(&namespace, &name).await; -}