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}