1pub mod admin;
7pub mod answers_crypto;
8pub mod bundle;
9pub mod bundle_source;
10pub mod capabilities;
11pub mod card_setup;
12pub mod cli_args;
13pub mod cli_commands;
14pub mod cli_helpers;
15pub mod cli_i18n;
16pub mod config_envelope;
17pub mod deployment_targets;
18pub mod discovery;
19pub mod doctor;
20pub mod engine;
21pub mod env_mode;
22pub mod env_wizard;
23pub mod flow;
24pub mod gtbundle;
25pub mod no_ui_oauth;
26pub mod oauth_callback;
27pub mod oauth_device;
28pub mod plan;
29pub mod platform_setup;
30pub mod provider_state;
31pub mod reload;
32pub mod secret_name;
33pub mod secrets;
34pub mod setup_actions;
35pub mod setup_input;
36pub mod setup_to_formspec;
37pub mod setup_tunnel;
38pub mod tenant_config;
39pub mod webhook;
40
41#[cfg(feature = "ui")]
42pub mod ui;
43
44pub mod qa {
45 pub mod bridge;
48 pub mod persist;
49 pub mod prompts;
50 pub mod shared_questions;
51 pub mod wizard;
52}
53
54pub use bundle_source::BundleSource;
55pub use engine::SetupEngine;
56pub use plan::{SetupMode, SetupPlan, SetupStep, SetupStepKind};
57
58pub use qa::wizard::{
60 ProviderFormSpec, SHARED_QUESTION_IDS, SharedQuestionsResult, build_provider_form_specs,
61 collect_shared_questions, prompt_shared_questions, run_qa_setup_with_shared,
62};
63
64pub fn version() -> &'static str {
66 env!("CARGO_PKG_VERSION")
67}
68
69pub const DEFAULT_ENV_ID: &str = "local";
73
74pub const LEGACY_ENV_ID: &str = "dev";
78
79pub const DISABLE_ALIAS_ENV_VAR: &str = "GREENTIC_DISABLE_DEV_ALIAS";
86
87pub fn resolve_env(override_env: Option<&str>) -> String {
96 let raw = override_env
97 .map(|v| v.to_string())
98 .or_else(|| std::env::var("GREENTIC_ENV").ok())
99 .unwrap_or_else(|| DEFAULT_ENV_ID.to_string());
100 compat_alias::apply_dev_alias(&raw)
101}
102
103mod compat_alias {
104 use std::sync::atomic::{AtomicBool, Ordering};
111
112 use super::{DEFAULT_ENV_ID, DISABLE_ALIAS_ENV_VAR, LEGACY_ENV_ID};
113
114 static WARNED: AtomicBool = AtomicBool::new(false);
115
116 pub fn apply_dev_alias(env: &str) -> String {
121 if env != LEGACY_ENV_ID {
122 return env.to_string();
123 }
124 if alias_disabled() {
125 panic!(
129 "environment `{LEGACY_ENV_ID}` is no longer accepted (set via {DISABLE_ALIAS_ENV_VAR}=1). \
130 Migrate to `{DEFAULT_ENV_ID}` via `gtc op env migrate-dev {DEFAULT_ENV_ID} --check` then `--apply`, \
131 or pass `--env {DEFAULT_ENV_ID}` / unset $GREENTIC_ENV.",
132 );
133 }
134 if !WARNED.swap(true, Ordering::SeqCst) {
135 tracing::warn!(
136 target: "greentic_setup::compat_alias",
137 legacy = LEGACY_ENV_ID,
138 target_env = DEFAULT_ENV_ID,
139 "env `{LEGACY_ENV_ID}` is deprecated; resolving as `{DEFAULT_ENV_ID}` for this process. \
140 Plan the migration with `gtc op env migrate-dev {DEFAULT_ENV_ID} --check`; \
141 set {DISABLE_ALIAS_ENV_VAR}=1 to hard-fail on `{LEGACY_ENV_ID}` in CI.",
142 );
143 }
144 DEFAULT_ENV_ID.to_string()
145 }
146
147 fn alias_disabled() -> bool {
148 std::env::var(DISABLE_ALIAS_ENV_VAR)
149 .ok()
150 .map(|v| {
151 let v = v.trim().to_ascii_lowercase();
152 matches!(v.as_str(), "1" | "true" | "yes" | "on")
153 })
154 .unwrap_or(false)
155 }
156
157 #[cfg(test)]
160 pub(super) fn reset_warning_latch_for_tests() {
161 WARNED.store(false, Ordering::SeqCst);
162 }
163}
164
165pub fn canonical_secret_uri(
174 env: &str,
175 tenant: &str,
176 team: Option<&str>,
177 provider: &str,
178 key: &str,
179) -> String {
180 let team_segment = greentic_secrets_lib::normalize_team(team)
181 .unwrap_or_else(|| greentic_secrets_lib::TEAM_PLACEHOLDER.to_string());
182 let provider_segment = if provider.is_empty() {
183 "messaging".to_string()
184 } else {
185 provider.to_string()
186 };
187 let normalized_key = secret_name::canonical_secret_name(key);
188 format!("secrets://{env}/{tenant}/{team_segment}/{provider_segment}/{normalized_key}")
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use std::sync::Mutex;
195
196 static ENV_LOCK: Mutex<()> = Mutex::new(());
200
201 fn with_clean_env<R>(body: impl FnOnce() -> R) -> R {
202 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
203 let prev_env = std::env::var_os("GREENTIC_ENV");
204 let prev_disable = std::env::var_os(DISABLE_ALIAS_ENV_VAR);
205 unsafe {
209 std::env::remove_var("GREENTIC_ENV");
210 std::env::remove_var(DISABLE_ALIAS_ENV_VAR);
211 }
212 compat_alias::reset_warning_latch_for_tests();
213 let out = body();
214 unsafe {
215 match prev_env {
216 Some(v) => std::env::set_var("GREENTIC_ENV", v),
217 None => std::env::remove_var("GREENTIC_ENV"),
218 }
219 match prev_disable {
220 Some(v) => std::env::set_var(DISABLE_ALIAS_ENV_VAR, v),
221 None => std::env::remove_var(DISABLE_ALIAS_ENV_VAR),
222 }
223 }
224 out
225 }
226
227 #[test]
228 fn version_is_correct() {
229 assert!(version().starts_with("1.1"));
230 }
231
232 #[test]
233 fn secret_uri_basic() {
234 let uri = canonical_secret_uri("dev", "demo", None, "messaging-telegram", "bot_token");
235 assert_eq!(uri, "secrets://dev/demo/_/messaging-telegram/bot_token");
236 }
237
238 #[test]
239 fn secret_uri_with_team() {
240 let uri = canonical_secret_uri("dev", "acme", Some("ops"), "state-redis", "redis_url");
241 assert_eq!(uri, "secrets://dev/acme/ops/state-redis/redis_url");
242 }
243
244 #[test]
245 fn secret_uri_default_team_becomes_wildcard() {
246 let uri = canonical_secret_uri(
247 "dev",
248 "demo",
249 Some("default"),
250 "messaging-slack",
251 "bot_token",
252 );
253 assert_eq!(uri, "secrets://dev/demo/_/messaging-slack/bot_token");
254 }
255
256 #[test]
257 fn resolve_env_returns_local_by_default() {
258 with_clean_env(|| {
259 assert_eq!(resolve_env(None), "local");
260 });
261 }
262
263 #[test]
264 fn resolve_env_passes_through_non_legacy_override() {
265 with_clean_env(|| {
266 assert_eq!(resolve_env(Some("staging")), "staging");
267 assert_eq!(resolve_env(Some("prod")), "prod");
268 assert_eq!(resolve_env(Some("local")), "local");
269 });
270 }
271
272 #[test]
273 fn resolve_env_remaps_dev_override_to_local() {
274 with_clean_env(|| {
275 assert_eq!(resolve_env(Some("dev")), "local");
276 });
277 }
278
279 #[test]
280 fn resolve_env_remaps_dev_env_var_to_local() {
281 with_clean_env(|| {
282 unsafe {
284 std::env::set_var("GREENTIC_ENV", "dev");
285 }
286 assert_eq!(resolve_env(None), "local");
287 });
288 }
289
290 #[test]
291 fn alias_warning_fires_only_once_per_process() {
292 with_clean_env(|| {
296 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
302 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
303 compat_alias::reset_warning_latch_for_tests();
306 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
307 });
308 }
309
310 #[test]
311 fn disable_alias_env_var_panics_on_dev() {
312 with_clean_env(|| {
313 unsafe {
315 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
316 }
317 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
318 assert!(
319 result.is_err(),
320 "resolve_env should panic when alias is disabled and input is `dev`"
321 );
322 });
323 }
324
325 #[test]
326 fn disable_alias_accepts_truthy_strings() {
327 for value in ["1", "true", "TRUE", "yes", "YES", "on", " true "] {
328 with_clean_env(|| {
329 unsafe {
331 std::env::set_var(DISABLE_ALIAS_ENV_VAR, value);
332 }
333 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
334 assert!(
335 result.is_err(),
336 "DISABLE value `{value}` should hard-fail on dev resolution"
337 );
338 });
339 }
340 }
341
342 #[test]
343 fn disable_alias_does_not_panic_on_non_legacy_values() {
344 with_clean_env(|| {
345 unsafe {
347 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
348 }
349 assert_eq!(resolve_env(Some("local")), "local");
352 assert_eq!(resolve_env(Some("staging")), "staging");
353 assert_eq!(resolve_env(None), "local");
354 });
355 }
356}