Skip to main content

runner_core/
env.rs

1use greentic_config_types::PathsConfig;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use anyhow::{Context, Result, anyhow, bail};
6use url::Url;
7
8/// Environment-driven configuration for pack management.
9#[derive(Debug, Clone)]
10pub struct PackConfig {
11    pub source: PackSource,
12    pub index_location: IndexLocation,
13    pub cache_dir: PathBuf,
14    pub public_key: Option<String>,
15    pub network: Option<greentic_config_types::NetworkConfig>,
16}
17
18impl PackConfig {
19    /// Build a [`PackConfig`] using greentic-config paths and sensible defaults.
20    pub fn default_for_paths(paths: &PathsConfig) -> Result<Self> {
21        let cache_dir = paths.cache_dir.join("packs");
22        let default_index = paths.greentic_root.join("index.json");
23        let index_location = if default_index.exists() {
24            IndexLocation::File(default_index)
25        } else if Path::new("examples/index.json").exists() {
26            IndexLocation::File(PathBuf::from("examples/index.json"))
27        } else {
28            IndexLocation::File(default_index)
29        };
30        Ok(Self {
31            source: PackSource::Fs,
32            index_location,
33            cache_dir,
34            public_key: None,
35            network: None,
36        })
37    }
38
39    /// Build from the structured packs section of greentic-config.
40    pub fn from_packs(cfg: &greentic_config_types::PacksConfig) -> Result<Self> {
41        let index_location = match &cfg.source {
42            greentic_config_types::PackSourceConfig::LocalIndex { path } => {
43                IndexLocation::File(path.clone())
44            }
45            greentic_config_types::PackSourceConfig::HttpIndex { url } => {
46                IndexLocation::from_value(url)?
47            }
48            greentic_config_types::PackSourceConfig::OciRegistry { reference } => {
49                IndexLocation::from_value(reference)?
50            }
51        };
52        let public_key = cfg
53            .trust
54            .as_ref()
55            .and_then(|trust| trust.public_keys.first().cloned());
56        Ok(Self {
57            source: PackSource::Fs,
58            index_location,
59            cache_dir: cfg.cache_dir.clone(),
60            public_key,
61            network: None,
62        })
63    }
64}
65
66/// Location of the pack index document (supports file paths and HTTP/S URLs).
67#[derive(Debug, Clone)]
68pub enum IndexLocation {
69    File(PathBuf),
70    Remote(Url),
71}
72
73impl IndexLocation {
74    pub fn from_value(value: &str) -> Result<Self> {
75        if value.starts_with("http://") || value.starts_with("https://") {
76            let url = Url::parse(value).context("PACK_INDEX_URL is not a valid URL")?;
77            return Ok(Self::Remote(url));
78        }
79        if value.starts_with("file://") {
80            let url = Url::parse(value).context("PACK_INDEX_URL is not a valid file:// URL")?;
81            let path = url
82                .to_file_path()
83                .map_err(|_| anyhow!("PACK_INDEX_URL points to an invalid file URI"))?;
84            return Ok(Self::File(path));
85        }
86        Ok(Self::File(PathBuf::from(value)))
87    }
88
89    pub fn display(&self) -> String {
90        match self {
91            Self::File(path) => path.display().to_string(),
92            Self::Remote(url) => url.to_string(),
93        }
94    }
95}
96
97/// Supported default sources for packs when the index omits the URI scheme.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PackSource {
100    Fs,
101    Http,
102    Oci,
103    S3,
104    Gcs,
105    AzBlob,
106}
107
108impl PackSource {
109    pub fn scheme(self) -> &'static str {
110        match self {
111            Self::Fs => "fs",
112            Self::Http => "http",
113            Self::Oci => "oci",
114            Self::S3 => "s3",
115            Self::Gcs => "gcs",
116            Self::AzBlob => "azblob",
117        }
118    }
119}
120
121impl FromStr for PackSource {
122    type Err = anyhow::Error;
123
124    fn from_str(value: &str) -> Result<Self> {
125        match value.to_ascii_lowercase().as_str() {
126            "fs" => Ok(Self::Fs),
127            "http" | "https" => Ok(Self::Http),
128            "oci" => Ok(Self::Oci),
129            "s3" => Ok(Self::S3),
130            "gcs" => Ok(Self::Gcs),
131            "azblob" | "azure" | "azureblob" => Ok(Self::AzBlob),
132            other => bail!("unsupported PACK_SOURCE `{other}`"),
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use greentic_config_types::{PackSourceConfig, PackTrustConfig, PacksConfig, PathsConfig};
141
142    fn paths(root: &std::path::Path) -> PathsConfig {
143        PathsConfig {
144            greentic_root: root.join("greentic"),
145            state_dir: root.join("state"),
146            cache_dir: root.join("cache"),
147            logs_dir: root.join("logs"),
148        }
149    }
150
151    #[test]
152    fn index_location_parses_remote_and_file_values() {
153        match IndexLocation::from_value("https://example.com/index.json").unwrap() {
154            IndexLocation::Remote(url) => {
155                assert_eq!(url.as_str(), "https://example.com/index.json")
156            }
157            IndexLocation::File(_) => panic!("expected remote index"),
158        }
159
160        match IndexLocation::from_value("file:///tmp/index.json").unwrap() {
161            IndexLocation::File(path) => assert_eq!(path, PathBuf::from("/tmp/index.json")),
162            IndexLocation::Remote(_) => panic!("expected file index"),
163        }
164
165        match IndexLocation::from_value("relative/index.json").unwrap() {
166            IndexLocation::File(path) => assert_eq!(path, PathBuf::from("relative/index.json")),
167            IndexLocation::Remote(_) => panic!("expected local file"),
168        }
169    }
170
171    #[test]
172    fn default_for_paths_prefers_greentic_root_index_when_present() {
173        let temp = tempfile::tempdir_in(std::env::current_dir().expect("cwd")).expect("tempdir");
174        let path_cfg = paths(temp.path());
175        std::fs::create_dir_all(&path_cfg.cache_dir).expect("cache dir");
176        std::fs::create_dir_all(&path_cfg.greentic_root).expect("greentic root");
177        let expected_index = path_cfg.greentic_root.join("index.json");
178        std::fs::write(&expected_index, "{}").expect("index file");
179
180        let config = PackConfig::default_for_paths(&path_cfg).expect("default config");
181        match config.index_location {
182            IndexLocation::File(path) => assert_eq!(path, expected_index),
183            IndexLocation::Remote(_) => panic!("expected local example index"),
184        }
185        assert_eq!(config.cache_dir, path_cfg.cache_dir.join("packs"));
186    }
187
188    #[test]
189    fn from_packs_uses_cache_and_public_key() {
190        let cfg = PacksConfig {
191            source: PackSourceConfig::HttpIndex {
192                url: "https://example.com/index.json".into(),
193            },
194            cache_dir: PathBuf::from("/tmp/packs-cache"),
195            index_cache_ttl_secs: None,
196            trust: Some(PackTrustConfig {
197                public_keys: vec!["ed25519:abc".into()],
198                require_signatures: true,
199            }),
200        };
201
202        let pack = PackConfig::from_packs(&cfg).expect("pack config");
203        assert_eq!(pack.cache_dir, PathBuf::from("/tmp/packs-cache"));
204        assert_eq!(pack.public_key.as_deref(), Some("ed25519:abc"));
205        match pack.index_location {
206            IndexLocation::Remote(url) => {
207                assert_eq!(url.as_str(), "https://example.com/index.json")
208            }
209            IndexLocation::File(_) => panic!("expected remote index"),
210        }
211    }
212
213    #[test]
214    fn pack_source_accepts_aliases() {
215        assert_eq!(PackSource::from_str("https").unwrap(), PackSource::Http);
216        assert_eq!(
217            PackSource::from_str("azureblob").unwrap(),
218            PackSource::AzBlob
219        );
220        assert_eq!(PackSource::AzBlob.scheme(), "azblob");
221        assert!(PackSource::from_str("ftp").is_err());
222    }
223}