greentic_operator/project/
scan.rs1use 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}