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_deploy;
22pub mod env_mode;
23pub mod env_wizard;
24pub mod flow;
25pub mod generated_secrets;
26pub mod gtbundle;
27pub mod http_client;
28pub mod no_ui_oauth;
29pub mod oauth_callback;
30pub mod oauth_device;
31pub mod plan;
32pub mod platform_setup;
33pub mod provider_commands;
34pub mod provider_registry;
35pub mod provider_state;
36pub mod reload;
37pub mod schema_validation;
38pub mod secret_name;
39pub mod secrets;
40pub mod setup_actions;
41pub mod setup_backend_contract;
42pub mod setup_final_actions;
43pub mod setup_input;
44pub mod setup_machine;
45pub mod setup_to_formspec;
46pub mod setup_tunnel;
47pub mod shared_tunnel;
48pub mod tenant_config;
49pub mod webhook;
50
51#[cfg(feature = "ui")]
52pub mod ui;
53
54pub mod qa {
55 pub mod bridge;
58 pub mod persist;
59 pub mod prompts;
60 pub mod shared_questions;
61 pub mod wizard;
62}
63
64pub use bundle_source::BundleSource;
65pub use engine::SetupEngine;
66pub use plan::{SetupMode, SetupPlan, SetupStep, SetupStepKind};
67
68pub use qa::wizard::{
70 ProviderFormSpec, SHARED_QUESTION_IDS, SharedQuestionsResult, build_provider_form_specs,
71 collect_shared_questions, prompt_shared_questions, run_qa_setup_with_shared,
72};
73
74pub fn version() -> &'static str {
76 env!("CARGO_PKG_VERSION")
77}
78
79pub const DEFAULT_ENV_ID: &str = "local";
83
84pub const LEGACY_ENV_ID: &str = "dev";
88
89pub const DISABLE_ALIAS_ENV_VAR: &str = "GREENTIC_DISABLE_DEV_ALIAS";
96
97pub fn resolve_env(override_env: Option<&str>) -> String {
106 let raw = override_env
107 .map(|v| v.to_string())
108 .or_else(|| std::env::var("GREENTIC_ENV").ok())
109 .unwrap_or_else(|| DEFAULT_ENV_ID.to_string());
110 compat_alias::apply_dev_alias(&raw)
111}
112
113mod compat_alias {
114 use std::sync::atomic::{AtomicBool, Ordering};
121
122 use super::{DEFAULT_ENV_ID, DISABLE_ALIAS_ENV_VAR, LEGACY_ENV_ID};
123
124 static WARNED: AtomicBool = AtomicBool::new(false);
125
126 pub fn apply_dev_alias(env: &str) -> String {
131 if env != LEGACY_ENV_ID {
132 return env.to_string();
133 }
134 if alias_disabled() {
135 panic!(
139 "environment `{LEGACY_ENV_ID}` is no longer accepted (set via {DISABLE_ALIAS_ENV_VAR}=1). \
140 Migrate to `{DEFAULT_ENV_ID}` via `gtc op env migrate-dev {DEFAULT_ENV_ID} --check` then `--apply`, \
141 or pass `--env {DEFAULT_ENV_ID}` / unset $GREENTIC_ENV.",
142 );
143 }
144 if !WARNED.swap(true, Ordering::SeqCst) {
145 tracing::warn!(
146 target: "greentic_setup::compat_alias",
147 legacy = LEGACY_ENV_ID,
148 target_env = DEFAULT_ENV_ID,
149 "env `{LEGACY_ENV_ID}` is deprecated; resolving as `{DEFAULT_ENV_ID}` for this process. \
150 Plan the migration with `gtc op env migrate-dev {DEFAULT_ENV_ID} --check`; \
151 set {DISABLE_ALIAS_ENV_VAR}=1 to hard-fail on `{LEGACY_ENV_ID}` in CI.",
152 );
153 }
154 DEFAULT_ENV_ID.to_string()
155 }
156
157 fn alias_disabled() -> bool {
158 std::env::var(DISABLE_ALIAS_ENV_VAR)
159 .ok()
160 .map(|v| {
161 let v = v.trim().to_ascii_lowercase();
162 matches!(v.as_str(), "1" | "true" | "yes" | "on")
163 })
164 .unwrap_or(false)
165 }
166
167 #[cfg(test)]
170 pub(super) fn reset_warning_latch_for_tests() {
171 WARNED.store(false, Ordering::SeqCst);
172 }
173}
174
175pub fn canonical_secret_uri(
184 env: &str,
185 tenant: &str,
186 team: Option<&str>,
187 provider: &str,
188 key: &str,
189) -> String {
190 let team_segment = greentic_secrets_lib::normalize_team(team)
191 .unwrap_or_else(|| greentic_secrets_lib::TEAM_PLACEHOLDER.to_string());
192 let provider_segment = if provider.is_empty() {
197 "messaging".to_string()
198 } else {
199 secret_name::canonical_secret_name(provider)
200 };
201 let normalized_key = secret_name::canonical_secret_name(key);
202 format!("secrets://{env}/{tenant}/{team_segment}/{provider_segment}/{normalized_key}")
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use std::sync::Mutex;
209
210 static ENV_LOCK: Mutex<()> = Mutex::new(());
214
215 fn with_clean_env<R>(body: impl FnOnce() -> R) -> R {
216 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
217 let prev_env = std::env::var_os("GREENTIC_ENV");
218 let prev_disable = std::env::var_os(DISABLE_ALIAS_ENV_VAR);
219 unsafe {
223 std::env::remove_var("GREENTIC_ENV");
224 std::env::remove_var(DISABLE_ALIAS_ENV_VAR);
225 }
226 compat_alias::reset_warning_latch_for_tests();
227 let out = body();
228 unsafe {
229 match prev_env {
230 Some(v) => std::env::set_var("GREENTIC_ENV", v),
231 None => std::env::remove_var("GREENTIC_ENV"),
232 }
233 match prev_disable {
234 Some(v) => std::env::set_var(DISABLE_ALIAS_ENV_VAR, v),
235 None => std::env::remove_var(DISABLE_ALIAS_ENV_VAR),
236 }
237 }
238 out
239 }
240
241 #[test]
242 fn version_is_correct() {
243 assert!(version().starts_with("1.2"));
244 }
245
246 #[test]
247 fn secret_uri_basic() {
248 let uri = canonical_secret_uri("dev", "demo", None, "messaging-telegram", "bot_token");
249 assert_eq!(uri, "secrets://dev/demo/_/messaging_telegram/bot_token");
250 }
251
252 #[test]
253 fn secret_uri_with_team() {
254 let uri = canonical_secret_uri("dev", "acme", Some("ops"), "state-redis", "redis_url");
255 assert_eq!(uri, "secrets://dev/acme/ops/state_redis/redis_url");
256 }
257
258 #[test]
259 fn secret_uri_default_team_becomes_wildcard() {
260 let uri = canonical_secret_uri(
261 "dev",
262 "demo",
263 Some("default"),
264 "messaging-slack",
265 "bot_token",
266 );
267 assert_eq!(uri, "secrets://dev/demo/_/messaging_slack/bot_token");
268 }
269
270 #[test]
271 fn secret_uri_normalizes_provider_segment() {
272 let stored = canonical_secret_uri(
276 "dev",
277 "demo",
278 None,
279 "messaging-webchat-gui",
280 "jwt_signing_key",
281 );
282 let fetched = canonical_secret_uri(
283 "dev",
284 "demo",
285 None,
286 "messaging.webchat-gui",
287 "jwt_signing_key",
288 );
289 assert_eq!(stored, fetched);
290 assert_eq!(
291 stored,
292 "secrets://dev/demo/_/messaging_webchat_gui/jwt_signing_key"
293 );
294 }
295
296 #[test]
297 fn resolve_env_returns_local_by_default() {
298 with_clean_env(|| {
299 assert_eq!(resolve_env(None), "local");
300 });
301 }
302
303 #[test]
304 fn resolve_env_passes_through_non_legacy_override() {
305 with_clean_env(|| {
306 assert_eq!(resolve_env(Some("staging")), "staging");
307 assert_eq!(resolve_env(Some("prod")), "prod");
308 assert_eq!(resolve_env(Some("local")), "local");
309 });
310 }
311
312 #[test]
313 fn resolve_env_remaps_dev_override_to_local() {
314 with_clean_env(|| {
315 assert_eq!(resolve_env(Some("dev")), "local");
316 });
317 }
318
319 #[test]
320 fn resolve_env_remaps_dev_env_var_to_local() {
321 with_clean_env(|| {
322 unsafe {
324 std::env::set_var("GREENTIC_ENV", "dev");
325 }
326 assert_eq!(resolve_env(None), "local");
327 });
328 }
329
330 #[test]
331 fn alias_warning_fires_only_once_per_process() {
332 with_clean_env(|| {
336 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
342 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
343 compat_alias::reset_warning_latch_for_tests();
346 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
347 });
348 }
349
350 #[test]
351 fn disable_alias_env_var_panics_on_dev() {
352 with_clean_env(|| {
353 unsafe {
355 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
356 }
357 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
358 assert!(
359 result.is_err(),
360 "resolve_env should panic when alias is disabled and input is `dev`"
361 );
362 });
363 }
364
365 #[test]
366 fn disable_alias_accepts_truthy_strings() {
367 for value in ["1", "true", "TRUE", "yes", "YES", "on", " true "] {
368 with_clean_env(|| {
369 unsafe {
371 std::env::set_var(DISABLE_ALIAS_ENV_VAR, value);
372 }
373 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
374 assert!(
375 result.is_err(),
376 "DISABLE value `{value}` should hard-fail on dev resolution"
377 );
378 });
379 }
380 }
381
382 #[test]
383 fn disable_alias_does_not_panic_on_non_legacy_values() {
384 with_clean_env(|| {
385 unsafe {
387 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
388 }
389 assert_eq!(resolve_env(Some("local")), "local");
392 assert_eq!(resolve_env(Some("staging")), "staging");
393 assert_eq!(resolve_env(None), "local");
394 });
395 }
396}