Skip to main content

pv_core/
legacy.rs

1//! Compatibility with installations made before the ProofVisor rename.
2//!
3//! Releases before the rename read `AV_*` environment variables and looked
4//! for `agentvisor.toml` and `~/.agentvisor/`. Existing deployments keep
5//! those names in systemd units, compose files and Kubernetes manifests, so
6//! an upgraded binary must still honour them. The new names always win when
7//! both are present.
8
9use std::ffi::OsString;
10use std::path::{Path, PathBuf};
11
12/// Prefix of the environment variables before the rename.
13pub const LEGACY_ENV_PREFIX: &str = "AV_";
14/// Prefix of the environment variables now.
15pub const ENV_PREFIX: &str = "PV_";
16
17/// The `PV_*` names to set from `AV_*` variables, as (new name, value)
18/// pairs. A legacy variable is adopted only when its new name is unset or
19/// empty, so an operator who already migrated is never overridden. Empty
20/// counts as unset because compose files render `PV_X: ${PV_X:-}`, and the
21/// daemon already reads an empty value as "not set".
22pub fn legacy_env_adoptions<I>(vars: I) -> Vec<(OsString, OsString)>
23where
24    I: IntoIterator<Item = (OsString, OsString)>,
25{
26    let vars: Vec<(OsString, OsString)> = vars.into_iter().collect();
27    let present = |name: &OsString| vars.iter().any(|(key, value)| key == name && !value.is_empty());
28    let mut adopt = Vec::new();
29    for (key, value) in &vars {
30        let Some(name) = key.to_str() else { continue };
31        let Some(rest) = name.strip_prefix(LEGACY_ENV_PREFIX) else {
32            continue;
33        };
34        if rest.is_empty() {
35            continue;
36        }
37        let new_name = OsString::from(format!("{ENV_PREFIX}{rest}"));
38        if !present(&new_name) && !adopt.iter().any(|(n, _): &(OsString, OsString)| *n == new_name) {
39            adopt.push((new_name, value.clone()));
40        }
41    }
42    adopt
43}
44
45/// Copy every `AV_*` variable to its `PV_*` name when that name is unset,
46/// and say so on stderr (names only, never values).
47///
48/// Call this first thing in `main`, before any thread is started: changing
49/// the environment while another thread reads it is not safe on every
50/// platform.
51pub fn adopt_legacy_env() {
52    let adoptions = legacy_env_adoptions(std::env::vars_os());
53    if adoptions.is_empty() {
54        return;
55    }
56    let names: Vec<String> = adoptions
57        .iter()
58        .map(|(name, _)| name.to_string_lossy().into_owned())
59        .collect();
60    for (name, value) in adoptions {
61        std::env::set_var(name, value);
62    }
63    eprintln!(
64        "warning: read {} from the pre-rename AV_* variables; rename them to PV_* (the AV_* names will be removed in a later release)",
65        names.join(", ")
66    );
67}
68
69/// `current` if it exists, else `legacy` if that exists, else `current`.
70/// For default file locations that moved with the rename: an installation
71/// that still has only the old file keeps working, and a fresh one gets
72/// the new location.
73pub fn existing_or_legacy(current: PathBuf, legacy: &Path) -> PathBuf {
74    if !current.exists() && legacy.exists() {
75        legacy.to_path_buf()
76    } else {
77        current
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    #![allow(clippy::unwrap_used)]
84    use super::*;
85
86    fn pairs(list: &[(&str, &str)]) -> Vec<(OsString, OsString)> {
87        list.iter()
88            .map(|(k, v)| (OsString::from(k), OsString::from(v)))
89            .collect()
90    }
91
92    #[test]
93    fn adopts_only_unset_new_names() {
94        let got = legacy_env_adoptions(pairs(&[
95            ("AV_CONFIG", "/etc/old.toml"),
96            ("AV_CONSOLE_TOKEN", "old-token"),
97            ("PV_CONSOLE_TOKEN", "new-token"),
98            ("AV_", "ignored"),
99            ("AV_LISTEN", "0.0.0.0:8484"),
100            ("PV_LISTEN", ""),
101            ("HOME", "/root"),
102        ]));
103        assert_eq!(
104            got,
105            pairs(&[("PV_CONFIG", "/etc/old.toml"), ("PV_LISTEN", "0.0.0.0:8484")])
106        );
107    }
108
109    #[test]
110    fn falls_back_to_the_legacy_file_only_when_the_new_one_is_missing() {
111        let dir = tempfile::tempdir().unwrap();
112        let current = dir.path().join("proofvisor.toml");
113        let legacy = dir.path().join("agentvisor.toml");
114        assert_eq!(existing_or_legacy(current.clone(), &legacy), current);
115        std::fs::write(&legacy, "").unwrap();
116        assert_eq!(existing_or_legacy(current.clone(), &legacy), legacy);
117        std::fs::write(&current, "").unwrap();
118        assert_eq!(existing_or_legacy(current.clone(), &legacy), current);
119    }
120}