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