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(crate) mod release_index;
37pub mod reload;
38pub mod schema_validation;
39pub mod secret_name;
40pub mod secrets;
41pub mod setup_actions;
42pub mod setup_backend_contract;
43pub mod setup_final_actions;
44pub mod setup_input;
45pub mod setup_machine;
46pub mod setup_to_formspec;
47pub mod setup_tunnel;
48pub mod shared_tunnel;
49pub mod tenant_config;
50pub mod webhook;
51
52#[cfg(feature = "ui")]
53pub mod ui;
54
55pub mod qa {
56 pub mod bridge;
59 pub mod persist;
60 pub mod prompts;
61 pub mod shared_questions;
62 pub mod wizard;
63}
64
65pub use bundle_source::BundleSource;
66pub use engine::SetupEngine;
67pub use plan::{SetupMode, SetupPlan, SetupStep, SetupStepKind};
68
69pub use qa::wizard::{
71 ProviderFormSpec, SHARED_QUESTION_IDS, SharedQuestionsResult, build_provider_form_specs,
72 collect_shared_questions, prompt_shared_questions, run_qa_setup_with_shared,
73};
74
75pub fn version() -> &'static str {
77 env!("CARGO_PKG_VERSION")
78}
79
80pub const DEFAULT_ENV_ID: &str = "local";
84
85pub const LEGACY_ENV_ID: &str = "dev";
89
90pub const DISABLE_ALIAS_ENV_VAR: &str = "GREENTIC_DISABLE_DEV_ALIAS";
97
98pub fn resolve_env(override_env: Option<&str>) -> String {
107 let raw = override_env
108 .map(|v| v.to_string())
109 .or_else(|| std::env::var("GREENTIC_ENV").ok())
110 .unwrap_or_else(|| DEFAULT_ENV_ID.to_string());
111 compat_alias::apply_dev_alias(&raw)
112}
113
114mod compat_alias {
115 use std::sync::atomic::{AtomicBool, Ordering};
122
123 use super::{DEFAULT_ENV_ID, DISABLE_ALIAS_ENV_VAR, LEGACY_ENV_ID};
124
125 static WARNED: AtomicBool = AtomicBool::new(false);
126
127 pub fn apply_dev_alias(env: &str) -> String {
132 if env != LEGACY_ENV_ID {
133 return env.to_string();
134 }
135 if alias_disabled() {
136 panic!(
140 "environment `{LEGACY_ENV_ID}` is no longer accepted (set via {DISABLE_ALIAS_ENV_VAR}=1). \
141 Migrate to `{DEFAULT_ENV_ID}` via `gtc op env migrate-dev {DEFAULT_ENV_ID} --check` then `--apply`, \
142 or pass `--env {DEFAULT_ENV_ID}` / unset $GREENTIC_ENV.",
143 );
144 }
145 if !WARNED.swap(true, Ordering::SeqCst) {
146 tracing::warn!(
147 target: "greentic_setup::compat_alias",
148 legacy = LEGACY_ENV_ID,
149 target_env = DEFAULT_ENV_ID,
150 "env `{LEGACY_ENV_ID}` is deprecated; resolving as `{DEFAULT_ENV_ID}` for this process. \
151 Plan the migration with `gtc op env migrate-dev {DEFAULT_ENV_ID} --check`; \
152 set {DISABLE_ALIAS_ENV_VAR}=1 to hard-fail on `{LEGACY_ENV_ID}` in CI.",
153 );
154 }
155 DEFAULT_ENV_ID.to_string()
156 }
157
158 fn alias_disabled() -> bool {
159 std::env::var(DISABLE_ALIAS_ENV_VAR)
160 .ok()
161 .map(|v| {
162 let v = v.trim().to_ascii_lowercase();
163 matches!(v.as_str(), "1" | "true" | "yes" | "on")
164 })
165 .unwrap_or(false)
166 }
167
168 #[cfg(test)]
171 pub(super) fn reset_warning_latch_for_tests() {
172 WARNED.store(false, Ordering::SeqCst);
173 }
174}
175
176pub fn canonical_secret_uri(
185 env: &str,
186 tenant: &str,
187 team: Option<&str>,
188 provider: &str,
189 key: &str,
190) -> String {
191 let team_segment = greentic_secrets_lib::normalize_team(team)
192 .unwrap_or_else(|| greentic_secrets_lib::TEAM_PLACEHOLDER.to_string());
193 let provider_segment = if provider.is_empty() {
198 "messaging".to_string()
199 } else {
200 secret_name::canonical_secret_name(provider)
201 };
202 let normalized_key = secret_name::canonical_secret_name(key);
203 format!("secrets://{env}/{tenant}/{team_segment}/{provider_segment}/{normalized_key}")
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn with_clean_env<R>(body: impl FnOnce() -> R) -> R {
220 let _guard = crate::secrets::test_support::env_lock();
221 let prev_env = std::env::var_os("GREENTIC_ENV");
222 let prev_disable = std::env::var_os(DISABLE_ALIAS_ENV_VAR);
223 unsafe {
227 std::env::remove_var("GREENTIC_ENV");
228 std::env::remove_var(DISABLE_ALIAS_ENV_VAR);
229 }
230 compat_alias::reset_warning_latch_for_tests();
231 let out = body();
232 unsafe {
233 match prev_env {
234 Some(v) => std::env::set_var("GREENTIC_ENV", v),
235 None => std::env::remove_var("GREENTIC_ENV"),
236 }
237 match prev_disable {
238 Some(v) => std::env::set_var(DISABLE_ALIAS_ENV_VAR, v),
239 None => std::env::remove_var(DISABLE_ALIAS_ENV_VAR),
240 }
241 }
242 out
243 }
244
245 #[test]
246 fn version_is_correct() {
247 assert!(version().starts_with("1.2"));
248 }
249
250 #[test]
251 fn secret_uri_basic() {
252 let uri = canonical_secret_uri("dev", "demo", None, "messaging-telegram", "bot_token");
253 assert_eq!(uri, "secrets://dev/demo/_/messaging_telegram/bot_token");
254 }
255
256 #[test]
257 fn secret_uri_with_team() {
258 let uri = canonical_secret_uri("dev", "acme", Some("ops"), "state-redis", "redis_url");
259 assert_eq!(uri, "secrets://dev/acme/ops/state_redis/redis_url");
260 }
261
262 #[test]
263 fn secret_uri_default_team_becomes_wildcard() {
264 let uri = canonical_secret_uri(
265 "dev",
266 "demo",
267 Some("default"),
268 "messaging-slack",
269 "bot_token",
270 );
271 assert_eq!(uri, "secrets://dev/demo/_/messaging_slack/bot_token");
272 }
273
274 #[test]
289 fn webex_secret_uri_contract_do_not_change() {
290 assert_eq!(
293 canonical_secret_uri(
294 "local",
295 "demo",
296 Some("default"),
297 "messaging-webex",
298 "webex_bot_token"
299 ),
300 "secrets://local/demo/_/messaging_webex/webex_bot_token",
301 "setup↔runtime secret-uri contract broke — read the DO-NOT-CHANGE doc above",
302 );
303 }
304
305 #[test]
306 fn secret_uri_normalizes_provider_segment() {
307 let stored = canonical_secret_uri(
311 "dev",
312 "demo",
313 None,
314 "messaging-webchat-gui",
315 "jwt_signing_key",
316 );
317 let fetched = canonical_secret_uri(
318 "dev",
319 "demo",
320 None,
321 "messaging.webchat-gui",
322 "jwt_signing_key",
323 );
324 assert_eq!(stored, fetched);
325 assert_eq!(
326 stored,
327 "secrets://dev/demo/_/messaging_webchat_gui/jwt_signing_key"
328 );
329 }
330
331 #[test]
332 fn resolve_env_returns_local_by_default() {
333 with_clean_env(|| {
334 assert_eq!(resolve_env(None), "local");
335 });
336 }
337
338 #[test]
339 fn resolve_env_passes_through_non_legacy_override() {
340 with_clean_env(|| {
341 assert_eq!(resolve_env(Some("staging")), "staging");
342 assert_eq!(resolve_env(Some("prod")), "prod");
343 assert_eq!(resolve_env(Some("local")), "local");
344 });
345 }
346
347 #[test]
348 fn resolve_env_remaps_dev_override_to_local() {
349 with_clean_env(|| {
350 assert_eq!(resolve_env(Some("dev")), "local");
351 });
352 }
353
354 #[test]
355 fn resolve_env_remaps_dev_env_var_to_local() {
356 with_clean_env(|| {
357 unsafe {
359 std::env::set_var("GREENTIC_ENV", "dev");
360 }
361 assert_eq!(resolve_env(None), "local");
362 });
363 }
364
365 #[test]
366 fn alias_warning_fires_only_once_per_process() {
367 with_clean_env(|| {
371 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
377 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
378 compat_alias::reset_warning_latch_for_tests();
381 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
382 });
383 }
384
385 #[test]
386 fn disable_alias_env_var_panics_on_dev() {
387 with_clean_env(|| {
388 unsafe {
390 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
391 }
392 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
393 assert!(
394 result.is_err(),
395 "resolve_env should panic when alias is disabled and input is `dev`"
396 );
397 });
398 }
399
400 #[test]
401 fn disable_alias_accepts_truthy_strings() {
402 for value in ["1", "true", "TRUE", "yes", "YES", "on", " true "] {
403 with_clean_env(|| {
404 unsafe {
406 std::env::set_var(DISABLE_ALIAS_ENV_VAR, value);
407 }
408 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
409 assert!(
410 result.is_err(),
411 "DISABLE value `{value}` should hard-fail on dev resolution"
412 );
413 });
414 }
415 }
416
417 #[test]
418 fn disable_alias_does_not_panic_on_non_legacy_values() {
419 with_clean_env(|| {
420 unsafe {
422 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
423 }
424 assert_eq!(resolve_env(Some("local")), "local");
427 assert_eq!(resolve_env(Some("staging")), "staging");
428 assert_eq!(resolve_env(None), "local");
429 });
430 }
431}