Skip to main content

greentic_operator/project/
scan.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use serde::Serialize;
5
6#[derive(Clone, Copy, Debug)]
7pub enum ScanFormat {
8    Text,
9    Json,
10    Yaml,
11}
12
13#[derive(Debug, Serialize)]
14pub struct ScanReport {
15    providers: BTreeMap<String, Vec<String>>,
16    packs: Vec<PackEntry>,
17    tenants: Vec<TenantEntry>,
18}
19
20#[derive(Debug, Serialize)]
21pub struct PackEntry {
22    kind: PackKind,
23    path: String,
24}
25
26#[derive(Debug, Serialize)]
27#[serde(rename_all = "lowercase")]
28pub enum PackKind {
29    Dir,
30    Gtpack,
31}
32
33#[derive(Debug, Serialize)]
34pub struct TenantEntry {
35    name: String,
36    teams: Vec<String>,
37}
38
39pub fn scan(root: &Path) -> anyhow::Result<ScanReport> {
40    let providers = scan_providers(&root.join("providers"))?;
41    let packs = scan_packs(&root.join("packs"))?;
42    let tenants = scan_tenants(&root.join("tenants"))?;
43    Ok(ScanReport {
44        providers,
45        packs,
46        tenants,
47    })
48}
49
50pub fn render_report(report: &ScanReport, format: ScanFormat) -> anyhow::Result<()> {
51    match format {
52        ScanFormat::Text => render_text(report),
53        ScanFormat::Json => {
54            let json = serde_json::to_string_pretty(report)?;
55            println!("{json}");
56            Ok(())
57        }
58        ScanFormat::Yaml => {
59            let yaml = serde_yaml_bw::to_string(report)?;
60            print!("{yaml}");
61            Ok(())
62        }
63    }
64}
65
66fn render_text(report: &ScanReport) -> anyhow::Result<()> {
67    println!("Providers:");
68    if report.providers.is_empty() {
69        println!("  (none)");
70    } else {
71        for (domain, packs) in &report.providers {
72            println!("  {domain}:");
73            if packs.is_empty() {
74                println!("    (none)");
75            } else {
76                for pack in packs {
77                    println!("    {pack}");
78                }
79            }
80        }
81    }
82
83    println!();
84    println!("Packs:");
85    if report.packs.is_empty() {
86        println!("  (none)");
87    } else {
88        for pack in &report.packs {
89            println!("  {} {}", pack.kind.label(), pack.path);
90        }
91    }
92
93    println!();
94    println!("Tenants:");
95    if report.tenants.is_empty() {
96        println!("  (none)");
97    } else {
98        for tenant in &report.tenants {
99            if tenant.teams.is_empty() {
100                println!("  {}", tenant.name);
101            } else {
102                println!("  {}:", tenant.name);
103                for team in &tenant.teams {
104                    println!("    {}", team);
105                }
106            }
107        }
108    }
109    Ok(())
110}
111
112fn scan_providers(root: &Path) -> anyhow::Result<BTreeMap<String, Vec<String>>> {
113    let mut providers = BTreeMap::new();
114    if !root.exists() {
115        return Ok(providers);
116    }
117    for entry in std::fs::read_dir(root)? {
118        let entry = entry?;
119        if !entry.file_type()?.is_dir() {
120            continue;
121        }
122        let domain = entry.file_name().to_string_lossy().to_string();
123        let mut packs = Vec::new();
124        for pack in std::fs::read_dir(entry.path())? {
125            let pack = pack?;
126            if pack.file_type()?.is_file() {
127                let path = pack.path();
128                if path.extension().and_then(|ext| ext.to_str()) == Some("gtpack") {
129                    packs.push(path_to_string(root, &path));
130                }
131            }
132        }
133        packs.sort();
134        providers.insert(domain, packs);
135    }
136    Ok(providers)
137}
138
139fn scan_packs(root: &Path) -> anyhow::Result<Vec<PackEntry>> {
140    let mut packs = Vec::new();
141    if !root.exists() {
142        return Ok(packs);
143    }
144    for entry in std::fs::read_dir(root)? {
145        let entry = entry?;
146        let path = entry.path();
147        if entry.file_type()?.is_dir() {
148            packs.push(PackEntry {
149                kind: PackKind::Dir,
150                path: path_to_string(root, &path),
151            });
152        } else if entry.file_type()?.is_file()
153            && path.extension().and_then(|ext| ext.to_str()) == Some("gtpack")
154        {
155            packs.push(PackEntry {
156                kind: PackKind::Gtpack,
157                path: path_to_string(root, &path),
158            });
159        }
160    }
161    packs.sort_by(|a, b| a.path.cmp(&b.path));
162    Ok(packs)
163}
164
165fn scan_tenants(root: &Path) -> anyhow::Result<Vec<TenantEntry>> {
166    let mut tenants = Vec::new();
167    if !root.exists() {
168        return Ok(tenants);
169    }
170    for entry in std::fs::read_dir(root)? {
171        let entry = entry?;
172        if !entry.file_type()?.is_dir() {
173            continue;
174        }
175        let name = entry.file_name().to_string_lossy().to_string();
176        let teams_dir = entry.path().join("teams");
177        let mut teams = Vec::new();
178        if teams_dir.exists() {
179            for team_entry in std::fs::read_dir(teams_dir)? {
180                let team_entry = team_entry?;
181                if team_entry.file_type()?.is_dir() {
182                    teams.push(team_entry.file_name().to_string_lossy().to_string());
183                }
184            }
185        }
186        teams.sort();
187        tenants.push(TenantEntry { name, teams });
188    }
189    tenants.sort_by(|a, b| a.name.cmp(&b.name));
190    Ok(tenants)
191}
192
193fn path_to_string(root: &Path, path: &Path) -> String {
194    path.strip_prefix(root)
195        .unwrap_or(path)
196        .to_string_lossy()
197        .to_string()
198}
199
200impl PackKind {
201    fn label(&self) -> &'static str {
202        match self {
203            PackKind::Dir => "dir",
204            PackKind::Gtpack => "gtpack",
205        }
206    }
207}