use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use super::Server;
use crate::config::ContextsConfig;
use crate::context::{self, KubeContext, RemoteContexts, Reported, TfSeen};
use crate::layout::PaneId;
use crate::pane::Pane;
use crate::tools::ToolContext;
pub(super) const CONTEXT_STALE: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Production {
pub why: String,
what: String,
}
impl Production {
pub fn new(why: impl Into<String>, what: impl Into<String>) -> Production {
Production { why: why.into(), what: what.into() }
}
pub fn and(self, other: Production) -> Production {
if self == other {
return self;
}
Production { why: format!("{}; {}", self.why, other.why), what: format!("{} + {}", self.what, other.what) }
}
}
impl std::fmt::Display for Production {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.why)
}
}
fn kube_what(value: &str, flags: &context::KubeFlags, read: u64) -> String {
format!("kubeconfig {value:?} {flags:?} read #{read}")
}
type TfDir = (PathBuf, Option<String>);
const TF_DIRS_KEPT: usize = 512;
fn forget_unused(read: &mut HashMap<TfDir, Option<String>>, wanted: &HashSet<TfDir>) {
if read.len() > TF_DIRS_KEPT {
read.retain(|k, ws| wanted.contains(k) || ws.is_some());
}
}
#[derive(Default)]
pub(super) struct TfDirs {
read: HashMap<TfDir, Option<String>>,
reading: HashMap<TfDir, (Instant, u64)>,
reads: u64,
}
fn tf_place((dir, data_dir): &TfDir) -> String {
match data_dir {
Some(d) => format!("{}, TF_DATA_DIR {d}", dir.display()),
None => dir.display().to_string(),
}
}
pub(super) fn observe_tools(cfg: &ContextsConfig, pane: &mut Pane, argv: &[String]) -> bool {
let kube = cfg.kubernetes && context::runs_kube_tool(argv);
let aws = cfg.aws && context::runs_aws_tool(argv);
let tf = cfg.terraform && context::runs_terraform(argv);
if !kube && !aws && !tf {
return false;
}
let Some(pid) = pane.foreground_pid() else { return false };
let before = (pane.seen.kubeconfig.clone(), pane.seen.tf_workspace.clone());
let env = crate::process::environ(
pid,
&["KUBECONFIG", "AWS_PROFILE", "AWS_DEFAULT_PROFILE", "TF_WORKSPACE", "TF_DATA_DIR"],
);
if kube {
let flags = context::kube_flags(argv);
let value = flags.kubeconfig.clone().or_else(|| env.get("KUBECONFIG").cloned()).unwrap_or_default();
let base = crate::process::cwd(pid);
pane.seen.kubeconfig = Some(context::absolute_kubeconfig(&value, base.as_deref()));
pane.seen.kube_flags = context::KubeFlags { kubeconfig: None, ..flags };
}
if aws {
let profile = context::aws_profile_flag(argv)
.or_else(|| env.get("AWS_PROFILE").cloned())
.or_else(|| env.get("AWS_DEFAULT_PROFILE").cloned())
.filter(|p| !p.is_empty());
pane.seen.aws_profile = Some(profile);
}
if tf {
let cwd = crate::process::cwd(pid);
pane.seen.tf_workspace = match &cwd {
Some(dir) => Some(context::terraform_command_workspace(argv, dir, &env)),
None => env.get("TF_WORKSPACE").filter(|w| !w.is_empty()).map(|w| TfSeen::Env(w.clone())),
};
}
(pane.seen.kubeconfig.clone(), pane.seen.tf_workspace.clone()) != before
}
fn spec_env(pane: &Pane, name: &str) -> Option<String> {
pane.spec.env.iter().find(|(k, _)| k == name).map(|(_, v)| v.clone())
}
fn shown(cfg: &ContextsConfig, r: &RemoteContexts) -> RemoteContexts {
let keep = |on: bool, v: &Reported<String>| if on { v.clone() } else { Reported::None };
RemoteContexts {
kube: if cfg.kubernetes { r.kube.clone() } else { Reported::None },
terraform: keep(cfg.terraform, &r.terraform),
aws: keep(cfg.aws, &r.aws),
}
}
fn reported_labels(r: &RemoteContexts, ascii: bool, words: &[String]) -> Vec<(String, bool)> {
let mut out = Vec::new();
match &r.terraform {
Reported::Known(ws) => out.push((format!("tf:{ws}"), context::names_production(ws, words))),
Reported::Unknown => out.push(("tf:?".into(), true)),
Reported::None => {}
}
match &r.kube {
Reported::Known(k) => {
let prod = context::names_production(&k.context, words)
|| k.namespace.as_deref().is_some_and(|ns| context::names_production(ns, words));
out.push((k.label(ascii), prod));
}
Reported::Unknown => out.push((if ascii { "k8s:?" } else { "⎈ ?" }.to_string(), true)),
Reported::None => {}
}
match &r.aws {
Reported::Known(p) => out.push((format!("aws:{p}"), context::names_production(p, words))),
Reported::Unknown => out.push(("aws:?".into(), true)),
Reported::None => {}
}
out
}
fn reported_production(r: &RemoteContexts, server: &crate::servers::Server, words: &[String]) -> Option<Production> {
let on = if server.name.is_empty() { &server.host } else { &server.name };
let what = |about: String| format!("server {server:?} reported {about}");
match &r.kube {
Reported::Known(k) if context::names_production(&k.context, words) => {
return Some(Production::new(format!("Kubernetes context {} on {on}", k.context), what(format!("{k:?}"))));
}
Reported::Known(k) => {
if let Some(ns) = k.namespace.as_ref().filter(|ns| context::names_production(ns, words)) {
return Some(Production::new(
format!("Kubernetes namespace {ns} in {} on {on}", k.context),
what(format!("{k:?}")),
));
}
}
Reported::Unknown => {
return Some(Production::new(format!("an unknown Kubernetes context on {on}"), what("kube ?".into())));
}
Reported::None => {}
}
match &r.terraform {
Reported::Known(ws) if context::names_production(ws, words) => {
return Some(Production::new(format!("Terraform workspace {ws} on {on}"), what(format!("tf {ws}"))));
}
Reported::Unknown => {
return Some(Production::new(format!("an unknown Terraform workspace on {on}"), what("tf ?".into())));
}
_ => {}
}
match &r.aws {
Reported::Known(p) if context::names_production(p, words) => {
Some(Production::new(format!("AWS profile {p} on {on}"), what(format!("aws {p}"))))
}
Reported::Unknown => Some(Production::new(format!("an unknown AWS profile on {on}"), what("aws ?".into()))),
_ => None,
}
}
impl Server {
pub(super) fn contexts_reported(&mut self, id: PaneId, contexts: RemoteContexts) {
if let Some(argv) = self.panes.get(&id).filter(|p| p.ssh.is_none()).and_then(|p| p.foreground_argv())
&& super::ssh::runs_ssh(&argv)
{
self.observe_ssh(id, &argv[1..]);
}
let Some(p) = self.panes.get_mut(&id) else { return };
let report = match p.foreground_pid() {
Some(session) if p.ssh.is_some() && p.exit_code.is_none() => Some((session, contexts)),
_ => None,
};
if p.reported != report {
p.reported = report;
self.mark_dirty();
}
}
pub(super) fn forget_ended_reports(&mut self) {
let mut changed = false;
for p in self.panes.values_mut() {
if let Some((session, _)) = &p.reported
&& (p.ssh.is_none() || p.exit_code.is_some() || p.foreground_pid() != Some(*session))
{
p.reported = None;
changed = true;
}
}
if changed {
self.mark_dirty();
}
}
pub(super) fn pane_reported(&self, id: PaneId) -> Option<RemoteContexts> {
let p = self.panes.get(&id)?;
let (_, r) = p.reported.as_ref().filter(|_| p.ssh.is_some() && p.exit_code.is_none())?;
Some(shown(&self.config.contexts, r))
}
fn pane_kubeconfig(&self, pane: &Pane) -> Option<String> {
if !self.config.contexts.kubernetes || pane.ssh.is_some() {
return None;
}
pane.seen.kubeconfig.clone().or_else(|| {
spec_env(pane, "KUBECONFIG").map(|v| context::absolute_kubeconfig(&v, pane.spec.cwd.as_deref()))
})
}
pub(super) fn refresh_kube(&mut self) {
let values: HashSet<String> = self.panes.values().filter_map(|p| self.pane_kubeconfig(p)).collect();
self.kube.retain(|v, _| values.contains(v));
self.kube_cache.retain_told(|v| values.contains(v));
for value in values {
let Some(number) = self.kube_cache.begin(&value) else { continue };
let (cache, read, done) = (self.kube_cache.clone(), value.clone(), value.clone());
let started = self.background(
move || cache.merged(&read),
move |server, file| {
server.kube_cache.end(&done);
server.tell_kube(done, number, file);
},
);
if !started {
self.kube_cache.end(&value);
}
}
}
pub(super) fn tell_kube(&mut self, value: String, number: u64, file: context::KubeFile) {
let changed = self.kube.get(&value) != Some(&file);
if self.kube_cache.told(&value, number, changed) && changed {
self.kube.insert(value, file);
self.mark_dirty();
}
}
pub(super) fn kubeconfig_of(&self, id: PaneId) -> Option<String> {
self.pane_kubeconfig(self.panes.get(&id)?)
}
fn kube_place(&self, id: PaneId, value: &str) -> String {
let mut place = if value.is_empty() { "~/.kube/config".to_string() } else { value.to_string() };
if let Some(p) = self.panes.get(&id) {
if let Some(c) = &p.seen.kube_flags.context {
place.push_str(&format!(", context {c}"));
}
if let Some(ns) = &p.seen.kube_flags.namespace {
place.push_str(&format!(", namespace {ns}"));
}
}
place
}
fn kube_what(&self, id: PaneId, value: &str, read: u64) -> String {
let flags = self.panes.get(&id).map(|p| p.seen.kube_flags.clone()).unwrap_or_default();
kube_what(value, &flags, read)
}
pub(super) fn kube_stalled(&self, id: PaneId, value: &str) -> Production {
let read = self.kube_cache.known_since(value).unwrap_or(0);
Production::new(
format!("an unknown Kubernetes context ({} does not answer)", self.kube_place(id, value)),
format!("stalled {}", self.kube_what(id, value, read)),
)
}
pub fn pane_kube(&self, id: PaneId) -> Option<KubeContext> {
let p = self.panes.get(&id)?;
if p.ssh.is_some() {
return self.pane_reported(id)?.kube.known().cloned();
}
self.kube.get(&self.pane_kubeconfig(p)?)?.resolve(&p.seen.kube_flags)
}
fn pane_kube_unknown(&self, id: PaneId) -> Option<Production> {
if let Some(stalled) = self.pane_kube_stalled(id) {
return Some(stalled);
}
let p = self.panes.get(&id)?;
let value = self.pane_kubeconfig(p)?;
let f = self.kube.get(&value)?;
if p.seen.kube_flags.context.is_some() {
return None;
}
let read = self.kube_cache.known_since(&value).unwrap_or(0);
f.unreadable.as_ref().map(|why| {
Production::new(
format!("an unknown Kubernetes context (unreadable kubeconfig: {why})"),
format!("unreadable {}", self.kube_what(id, &value, read)),
)
})
}
fn pane_kube_stalled(&self, id: PaneId) -> Option<Production> {
let p = self.panes.get(&id)?;
let value = self.pane_kubeconfig(p)?;
let (since, number) = self.kube_cache.reading(&value)?;
(since.elapsed() > CONTEXT_STALE).then(|| {
Production::new(
format!("an unknown Kubernetes context ({} does not answer)", self.kube_place(id, &value)),
format!("stalled {}", self.kube_what(id, &value, number)),
)
})
}
pub fn pane_kube_is_unknown(&self, id: PaneId) -> bool {
self.pane_kube_unknown(id).is_some()
}
fn pane_kube_pending(&self, id: PaneId) -> Option<Production> {
let value = self.kubeconfig_of(id)?;
if self.kube.contains_key(&value) {
return None;
}
let number = self.kube_cache.reading(&value).map_or(0, |(_, n)| n);
Some(Production::new(
format!("a Kubernetes context not read yet ({})", self.kube_place(id, &value)),
format!("pending {}", self.kube_what(id, &value, number)),
))
}
pub fn pane_context_reading(&self, id: PaneId) -> bool {
let fresh = |since: &Instant| since.elapsed() <= CONTEXT_STALE;
let kube = self.kubeconfig_of(id).and_then(|v| self.kube_cache.reading(&v)).is_some_and(|(t, _)| fresh(&t));
let tf = self.pane_tf_keys(id).any(|k| self.tf_dirs.reading.get(&k).is_some_and(|(t, _)| fresh(t)));
kube || tf
}
pub(super) fn refresh_tf_dirs(&mut self) {
let mut wanted: HashSet<TfDir> = HashSet::new();
let terraform = self.config.contexts.terraform;
for p in self.panes.values_mut() {
if let Some(TfSeen::Dir { from, dir, data_dir }) = &p.seen.tf_workspace {
if p.current_cwd().as_ref() == Some(from) {
wanted.insert((dir.clone(), data_dir.clone()));
} else {
p.seen.tf_workspace = None;
}
}
let shell_here = p.is_terminal() && p.ssh.is_none() && p.exit_code.is_none();
if terraform && shell_here && !matches!(p.seen.tf_workspace, Some(TfSeen::Env(_))) {
wanted.extend(p.current_cwd().map(|cwd| (cwd, None)));
}
}
forget_unused(&mut self.tf_dirs.read, &wanted);
for key in wanted {
if self.tf_dirs.reading.contains_key(&key) {
continue;
}
self.tf_dirs.reads += 1;
self.tf_dirs.reading.insert(key.clone(), (Instant::now(), self.tf_dirs.reads));
let (read, done) = (key.clone(), key.clone());
let started = self.background(
move || context::terraform_dir_workspace(&read.0, read.1.as_deref()),
move |server, ws| {
server.tf_dirs.reading.remove(&done);
if server.tf_dirs.read.get(&done) != Some(&ws) {
server.tf_dirs.read.insert(done, ws);
server.mark_dirty();
}
},
);
if !started {
self.tf_dirs.reading.remove(&key);
}
}
}
pub fn pane_terraform(&self, id: PaneId) -> Option<String> {
let p = self.panes.get(&id)?;
if p.ssh.is_some() {
return self.pane_reported(id)?.terraform.known().cloned();
}
if !self.config.contexts.terraform {
return None;
}
let cwd = p.current_cwd();
match &p.seen.tf_workspace {
Some(TfSeen::Env(ws)) => Some(ws.clone()),
Some(TfSeen::Dir { from, dir, data_dir }) if cwd.as_ref() == Some(from) => {
match self.tf_dirs.read.get(&(dir.clone(), data_dir.clone()))? {
Some(ws) => Some(ws.clone()),
None => self.tf_dirs.read.get(&(cwd?, None))?.clone(),
}
}
_ => self.tf_dirs.read.get(&(cwd?, None))?.clone(),
}
}
fn pane_tf_dir(&self, id: PaneId) -> Option<TfDir> {
let p = self.panes.get(&id)?;
if !self.config.contexts.terraform || p.ssh.is_some() {
return None;
}
let Some(TfSeen::Dir { from, dir, data_dir }) = &p.seen.tf_workspace else { return None };
(p.current_cwd().as_ref() == Some(from)).then(|| (dir.clone(), data_dir.clone()))
}
fn pane_tf_keys(&self, id: PaneId) -> impl Iterator<Item = TfDir> {
let here = self.panes.get(&id).filter(|p| {
self.config.contexts.terraform && p.ssh.is_none() && !matches!(p.seen.tf_workspace, Some(TfSeen::Env(_)))
});
self.pane_tf_dir(id).into_iter().chain(here.and_then(|p| p.current_cwd()).map(|cwd| (cwd, None)))
}
fn pane_terraform_unknown(&self, id: PaneId) -> Option<Production> {
self.pane_tf_keys(id).find_map(|key| {
let (since, number) = self.tf_dirs.reading.get(&key)?;
(since.elapsed() > CONTEXT_STALE).then(|| {
Production::new(
format!("an unknown Terraform workspace ({} does not answer)", tf_place(&key)),
format!("stalled terraform {key:?} read #{number}"),
)
})
})
}
fn pane_context_pending(&self, id: PaneId) -> Option<Production> {
if let Some(why) = self.pane_kube_pending(id) {
return Some(why);
}
let key = self.pane_tf_dir(id)?;
if self.tf_dirs.read.contains_key(&key) {
return None;
}
let number = self.tf_dirs.reading.get(&key).map_or(0, |(_, n)| *n);
Some(Production::new(
format!("a Terraform workspace not read yet ({})", tf_place(&key)),
format!("pending terraform {key:?} read #{number}"),
))
}
pub fn pane_aws(&self, id: PaneId) -> Option<String> {
let p = self.panes.get(&id)?;
if p.ssh.is_some() {
return self.pane_reported(id)?.aws.known().cloned();
}
if !self.config.contexts.aws {
return None;
}
match &p.seen.aws_profile {
Some(seen) => seen.clone(),
None => spec_env(p, "AWS_PROFILE"),
}
}
pub fn pane_server_entry(&self, id: PaneId) -> Option<crate::servers::Server> {
let mut s = self.panes.get(&id)?.ssh.clone()?;
if let Some(k) = self.known_servers.iter().find(|k| k.server.name == s.name) {
s.groups = k.server.groups.clone();
s.env = k.server.env.clone();
s.note = k.server.note.clone();
}
Some(s)
}
pub fn pane_production(&self, id: PaneId) -> Option<Production> {
if self.config.production.words.is_empty() {
return None;
}
self.pane_production_known(id).or_else(|| self.pane_context_pending(id))
}
pub fn pane_production_shown(&self, id: PaneId) -> Option<String> {
self.pane_production_known(id).map(|p| p.why)
}
fn pane_production_known(&self, id: PaneId) -> Option<Production> {
let words = &self.config.production.words;
if words.is_empty() {
return None;
}
if let Some(s) = self.pane_server_entry(id) {
if context::server_production(&s, words) {
return Some(Production::new(format!("server {}", s.name), format!("server {s:?}")));
}
return self.pane_reported(id).and_then(|r| reported_production(&r, &s, words));
}
if let Some(stalled) = self.pane_kube_stalled(id) {
return Some(stalled);
}
if let Some(k) = self.pane_kube(id) {
let value = self.kubeconfig_of(id).unwrap_or_default();
let what = self.kube_what(id, &value, self.kube_cache.known_since(&value).unwrap_or(0));
if context::names_production(&k.context, words) {
return Some(Production::new(format!("Kubernetes context {}", k.context), what));
}
if let Some(ns) = k.namespace.filter(|ns| context::names_production(ns, words)) {
return Some(Production::new(format!("Kubernetes namespace {ns} in {}", k.context), what));
}
}
if let Some(why) = self.pane_kube_unknown(id) {
return Some(why);
}
if let Some(why) = self.pane_terraform_unknown(id) {
return Some(why);
}
if let Some(ws) = self.pane_terraform(id).filter(|ws| context::names_production(ws, words)) {
let place = self
.pane_tf_dir(id)
.map(|k| format!("{k:?}"))
.or_else(|| self.panes.get(&id).and_then(|p| p.current_cwd()).map(|d| format!("{d:?}")));
return Some(Production::new(format!("Terraform workspace {ws}"), format!("terraform {place:?} {ws}")));
}
if let Some(p) = self.pane_aws(id).filter(|p| context::names_production(p, words)) {
return Some(Production::new(format!("AWS profile {p}"), format!("aws {p}")));
}
None
}
pub fn context_labels(&self, id: PaneId) -> Vec<(String, bool)> {
let ascii = crate::theme::current().ascii;
let words = &self.config.production.words;
if let Some(r) = self.pane_reported(id) {
return reported_labels(&r, ascii, words);
}
let mut out = Vec::new();
if let Some(ws) = self.pane_terraform(id) {
out.push((format!("tf:{ws}"), context::names_production(&ws, words)));
}
if let Some(k) = self.pane_kube(id) {
let prod = context::names_production(&k.context, words)
|| k.namespace.as_deref().is_some_and(|ns| context::names_production(ns, words));
out.push((k.label(ascii), prod));
} else if self.pane_kube_is_unknown(id) {
out.push((if ascii { "k8s:?" } else { "⎈ ?" }.to_string(), true));
}
if let Some(p) = self.pane_aws(id) {
out.push((format!("aws:{p}"), context::names_production(&p, words)));
}
out
}
pub fn tool_context(&self, id: PaneId) -> ToolContext {
let Some(p) = self.panes.get(&id) else { return ToolContext::default() };
let server = self.pane_server_entry(id);
ToolContext {
cwd: if server.is_none() { p.current_cwd() } else { None },
remote_cwd: server.as_ref().and_then(|_| p.remote_cwd()),
server,
git: self.pane_git(id).cloned(),
kube: self.pane_kube(id),
kubeconfig: self.pane_kubeconfig(p),
terraform: self.pane_terraform(id),
aws_profile: self.pane_aws(id),
aws_default: p.ssh.is_none() && matches!(p.seen.aws_profile, Some(None)),
selection: p.selection_text(),
production: self.pane_production(id).map(|p| p.why),
extra: Vec::new(),
secrets: Vec::new(),
}
}
pub(super) fn forget_ended_ssh(&mut self, ids: Vec<PaneId>) {
for id in ids {
let Some(p) = self.panes.get(&id) else { continue };
if self.ssh_target_of(&p.spec).is_none() {
self.panes.get_mut(&id).unwrap().ssh = None;
self.mark_dirty();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_terraform_directories_are_never_forgotten() {
let dir = |n: usize| (PathBuf::from(format!("/d{n}")), None);
let mut read: HashMap<TfDir, Option<String>> = (0..TF_DIRS_KEPT).map(|n| (dir(n), None)).collect();
read.insert(dir(9999), Some("prod".into()));
read.insert(dir(10000), None);
let wanted: HashSet<TfDir> = [dir(10000)].into_iter().collect();
forget_unused(&mut read, &wanted);
assert_eq!(read.get(&dir(9999)), Some(&Some("prod".into())), "a Terraform directory stays");
assert!(read.contains_key(&dir(10000)), "a wanted directory stays");
assert_eq!(read.len(), 2, "the unused others go");
}
#[test]
fn contexts_reported_over_ssh_are_shown_and_guarded_like_local_ones() {
let words: Vec<String> = ["prod", "production", "prd"].map(String::from).to_vec();
let bastion = crate::servers::Server { name: "bastion".into(), host: "10.0.0.5".into(), ..Default::default() };
let r = RemoteContexts::parse("k8s=arn:aws:eks:eu-west-1:1:cluster/prod-eks;ns=payments;tf=blue;aws=ops");
assert_eq!(
reported_labels(&r, false, &words),
vec![("tf:blue".to_string(), false), ("⎈ prod-eks/payments".to_string(), true), ("aws:ops".into(), false)]
);
let why = reported_production(&r, &bastion, &words).unwrap();
assert_eq!(why.to_string(), "Kubernetes context arn:aws:eks:eu-west-1:1:cluster/prod-eks on bastion");
let r = RemoteContexts::parse("k8s=dev;ns=prod");
assert_eq!(
reported_production(&r, &bastion, &words).unwrap().to_string(),
"Kubernetes namespace prod in dev on bastion"
);
let r = RemoteContexts::parse("tf=prod;aws=ops");
assert_eq!(
reported_production(&r, &bastion, &words).unwrap().to_string(),
"Terraform workspace prod on bastion"
);
let r = RemoteContexts::parse("aws=prd-admin");
assert_eq!(reported_production(&r, &bastion, &words).unwrap().to_string(), "AWS profile prd-admin on bastion");
assert_eq!(reported_production(&RemoteContexts::parse("k8s=dev;tf=default;aws=ops"), &bastion, &words), None);
let r = RemoteContexts::parse("k8s=?;tf=?;aws=x y");
assert_eq!(
reported_labels(&r, true, &words),
vec![("tf:?".to_string(), true), ("k8s:?".to_string(), true), ("aws:?".to_string(), true)]
);
let why = reported_production(&r, &bastion, &words).unwrap();
assert_eq!(why.to_string(), "an unknown Kubernetes context on bastion");
let other = crate::servers::Server { name: "bastion".into(), host: "10.0.0.6".into(), ..Default::default() };
let prod = RemoteContexts::parse("k8s=prod");
assert_ne!(reported_production(&prod, &bastion, &words), reported_production(&prod, &other, &words));
assert_eq!(reported_production(&prod, &bastion, &words), reported_production(&prod, &bastion, &words));
let cfg = ContextsConfig { kubernetes: false, terraform: true, aws: false };
let r = shown(&cfg, &RemoteContexts::parse("k8s=prod;tf=prod;aws=prod"));
assert_eq!((r.kube, r.terraform, r.aws), (Reported::None, Reported::Known("prod".into()), Reported::None));
}
#[test]
fn an_approval_holds_for_what_it_was_given_for() {
let flags = |c: &str| context::KubeFlags { context: Some(c.into()), ..Default::default() };
let stalled = |value: &str, f: &context::KubeFlags, read| {
Production::new(
"an unknown Kubernetes context (k does not answer)",
format!("stalled {}", kube_what(value, f, read)),
)
};
let asked = stalled("/k", &flags("prod-a"), 3);
assert_eq!(asked, stalled("/k", &flags("prod-a"), 3), "the same stalled file asked about twice");
assert_ne!(asked, stalled("/k", &flags("prod-b"), 3), "the same words, other flags");
assert_ne!(asked, stalled("/k", &flags("prod-a"), 4), "a newer read");
assert_ne!(asked, stalled("/other", &flags("prod-a"), 3), "another file");
let a = Production::new("Kubernetes context prod", kube_what("/a", &Default::default(), 1));
let b = Production::new("Kubernetes context prod", kube_what("/b", &Default::default(), 1));
assert_ne!(a, b);
assert_eq!(a.to_string(), "Kubernetes context prod");
assert_eq!(a.clone().and(a.clone()), a);
assert_eq!(a.and(b).to_string(), "Kubernetes context prod; Kubernetes context prod");
}
}