use serde::Serialize;
use crate::dog_index::AvailableDog;
use crate::output::Render;
#[derive(Debug, Serialize)]
pub struct KvEntry {
pub key: String,
pub value: String,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct KvRows(pub Vec<KvEntry>);
impl Render for KvRows {
fn headers() -> &'static [&'static str] {
&["KEY", "VALUE"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|entry| vec![entry.key.clone(), entry.value.clone()])
.collect()
}
const PRIORITIES: &'static [u8] = &[0, 0];
}
#[derive(Debug, Serialize)]
pub struct SecretKeyRow {
pub key: String,
pub environments: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct SecretKeyRows(pub Vec<SecretKeyRow>);
impl Render for SecretKeyRows {
fn headers() -> &'static [&'static str] {
&["KEY", "ENVIRONMENTS"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|row| vec![row.key.clone(), row.environments.join(", ")])
.collect()
}
const PRIORITIES: &'static [u8] = &[0, 0];
}
#[derive(Debug, Serialize)]
pub struct SecretSlotRow {
pub key: String,
pub environment: String,
}
impl Render for SecretSlotRow {
fn headers() -> &'static [&'static str] {
&["KEY", "ENVIRONMENT"]
}
fn rows(&self) -> Vec<Vec<String>> {
vec![vec![self.key.clone(), self.environment.clone()]]
}
const PRIORITIES: &'static [u8] = &[0, 0];
}
#[derive(Serialize)]
pub struct SecretValueRow {
pub key: String,
pub value: String,
}
impl std::fmt::Debug for SecretValueRow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretValueRow")
.field("key", &self.key)
.field("value", &"<redacted>")
.finish()
}
}
impl Render for SecretValueRow {
fn headers() -> &'static [&'static str] {
&["KEY", "VALUE"]
}
fn rows(&self) -> Vec<Vec<String>> {
vec![vec![self.key.clone(), self.value.clone()]]
}
const PRIORITIES: &'static [u8] = &[0, 0];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretStatus {
Resolved,
Missing,
Uncached,
}
impl SecretStatus {
#[must_use]
pub fn from_resolution(resolution: &shep_core::secrets::Resolution<'_>) -> Self {
match resolution {
shep_core::secrets::Resolution::Found(_) => Self::Resolved,
shep_core::secrets::Resolution::MissingKey => Self::Missing,
shep_core::secrets::Resolution::MissingNamespace => Self::Uncached,
}
}
#[must_use]
pub fn as_table_word(self) -> &'static str {
match self {
Self::Resolved => "resolved",
Self::Missing => "missing",
Self::Uncached => "not cached; a provider may still have it",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DescribedSecret {
pub name: String,
pub reference: String,
pub environment: String,
pub status: SecretStatus,
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct AvailableDogRows(pub Vec<AvailableDog>);
fn describe_for_row(dog: &AvailableDog) -> String {
if dog.is_unversioned_cargo() {
format!("{} (not installable: no version approved)", dog.description)
} else {
dog.description.clone()
}
}
impl Render for AvailableDogRows {
fn headers() -> &'static [&'static str] {
&["NAME", "PACKAGE", "CATEGORY", "DESCRIPTION"]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|dog| {
vec![
dog.name.clone(),
dog.package.clone(),
dog.category.clone(),
describe_for_row(dog),
]
})
.collect()
}
const JSON_ONLY: &'static [&'static str] = &[
"adopt_as",
"repo", "license",
"source",
];
const PRIORITIES: &'static [u8] = &[0, 0, 6, 7];
}
#[derive(Debug, Serialize)]
pub struct KvUnsetRow {
pub removed: u32,
}
impl Render for KvUnsetRow {
fn headers() -> &'static [&'static str] {
&["REMOVED"]
}
fn rows(&self) -> Vec<Vec<String>> {
vec![vec![self.removed.to_string()]]
}
const PRIORITIES: &'static [u8] = &[0];
}
#[cfg(test)]
mod tests {
use super::super::tests::assert_no_drift;
use super::*;
#[test]
fn kv_rows_do_not_drift() {
let rows = KvRows(vec![KvEntry {
key: "bark.cooldown".to_string(),
value: "30s".to_string(),
}]);
assert_no_drift(&rows, |j| &j[0], &[]);
}
#[test]
fn kv_unset_row_does_not_drift() {
assert_no_drift(&KvUnsetRow { removed: 2 }, |j| j, &[]);
}
#[test]
fn secret_key_rows_do_not_drift() {
let rows = SecretKeyRows(vec![SecretKeyRow {
key: "DB_PASSWORD".to_string(),
environments: vec!["all".to_string(), "staging".to_string()],
}]);
assert_no_drift(&rows, |j| &j[0], &["ENVIRONMENTS"]);
}
#[test]
fn secret_slot_row_does_not_drift() {
let row = SecretSlotRow {
key: "DB_PASSWORD".to_string(),
environment: "staging".to_string(),
};
assert_no_drift(&row, |j| j, &[]);
}
#[test]
fn secret_value_row_does_not_drift() {
let row = SecretValueRow {
key: "DB_PASSWORD".to_string(),
value: "hunter2".to_string(),
};
assert_no_drift(&row, |j| j, &[]);
}
#[test]
fn only_secret_value_row_carries_a_value_and_its_debug_is_redacted() {
assert!(!SecretKeyRows::headers().contains(&"VALUE"));
assert!(!SecretSlotRow::headers().contains(&"VALUE"));
let json = serde_json::to_string(&SecretSlotRow {
key: "K".to_string(),
environment: "all".to_string(),
})
.unwrap();
assert!(!json.contains("value"), "{json}");
let row = SecretValueRow {
key: "K".to_string(),
value: "hunter2".to_string(),
};
let rendered = format!("{row:?}");
assert!(!rendered.contains("hunter2"), "{rendered}");
assert_eq!(
rendered,
r#"SecretValueRow { key: "K", value: "<redacted>" }"#
);
}
fn sample_available_dog() -> AvailableDog {
AvailableDog {
name: "Spot".to_string(),
package: "shep-log-rotate".to_string(),
adopt_as: "log-rotate".to_string(),
description: "Rotates grown log files and asks the shepherd to reopen them."
.to_string(),
repo: "https://github.com/shep-pm/shep-log-rotate".to_string(),
license: "MIT OR Apache-2.0".to_string(),
category: "logs".to_string(),
source: crate::dog_index::DogSourceKind::CargoGit {
url: "https://github.com/shep-pm/shep-log-rotate".to_string(),
},
}
}
#[test]
fn available_dog_rows_do_not_drift() {
assert_no_drift(
&AvailableDogRows(vec![sample_available_dog()]),
|j| &j[0],
&["DESCRIPTION"],
);
}
fn cargo_dog(version: Option<&str>) -> AvailableDog {
AvailableDog {
source: crate::dog_index::DogSourceKind::Cargo {
version: version.map(str::to_owned),
sha256: std::collections::BTreeMap::new(),
},
..sample_available_dog()
}
}
#[test]
fn a_cargo_row_with_no_approved_version_says_so_on_its_row() {
let rows = AvailableDogRows(vec![cargo_dog(None), cargo_dog(Some("0.2.4"))]).rows();
assert_eq!(
rows[0][3],
"Rotates grown log files and asks the shepherd to reopen them. \
(not installable: no version approved)"
);
assert_eq!(
rows[1][3],
"Rotates grown log files and asks the shepherd to reopen them."
);
}
}