Skip to content

Commit 924f718

Browse files
committed
Restructure key files
1 parent 6073fbf commit 924f718

33 files changed

Lines changed: 20454 additions & 20064 deletions

migrations_sqlx/postgres/20250101000000_initial.sql

Lines changed: 219 additions & 122 deletions
Large diffs are not rendered by default.

migrations_sqlx/sqlite/20250101000000_initial.sql

Lines changed: 207 additions & 119 deletions
Large diffs are not rendered by default.

src/app.rs

Lines changed: 2014 additions & 0 deletions
Large diffs are not rendered by default.

src/cli/bootstrap.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
use super::resolve_config_path;
2+
use crate::{config, db, models, observability, services};
3+
4+
/// Run the bootstrap command: create initial org, SSO config, and API key from config.
5+
pub(crate) async fn run_bootstrap(explicit_config_path: Option<&str>, dry_run: bool) {
6+
// Resolve config path
7+
let (config_path, _) = match resolve_config_path(explicit_config_path) {
8+
Ok((path, is_new)) => (path, is_new),
9+
Err(e) => {
10+
eprintln!("Error: {e}");
11+
std::process::exit(1);
12+
}
13+
};
14+
15+
let config = match config::GatewayConfig::from_file(&config_path) {
16+
Ok(c) => c,
17+
Err(e) => {
18+
eprintln!("Failed to load config from {}: {e}", config_path.display());
19+
std::process::exit(1);
20+
}
21+
};
22+
23+
let _tracing_guard =
24+
observability::init_tracing(&config.observability).expect("Failed to initialize tracing");
25+
26+
let bootstrap = match &config.auth.bootstrap {
27+
Some(b) => b.clone(),
28+
None => {
29+
eprintln!("Error: No [auth.bootstrap] section in config file.");
30+
eprintln!("Add an [auth.bootstrap] section with initial_org and/or initial_api_key.");
31+
std::process::exit(1);
32+
}
33+
};
34+
35+
if config.database.is_none() {
36+
eprintln!("Error: Database is not configured. Bootstrap requires a database.");
37+
std::process::exit(1);
38+
}
39+
40+
if dry_run {
41+
println!("=== Bootstrap Dry Run ===");
42+
println!("Config: {}", config_path.display());
43+
if let Some(ref org) = bootstrap.initial_org {
44+
println!(" Create org: slug={}, name={}", org.slug, org.name);
45+
#[cfg(feature = "sso")]
46+
if let Some(ref sso) = org.sso {
47+
println!(
48+
" Configure SSO: provider={}, issuer={}",
49+
sso.provider_type,
50+
sso.issuer.as_deref().unwrap_or("(none)")
51+
);
52+
if !sso.allowed_email_domains.is_empty() {
53+
println!(" Email domains: {:?}", sso.allowed_email_domains);
54+
}
55+
}
56+
if !org.admin_identities.is_empty() {
57+
println!(" Admin identities: {:?}", org.admin_identities);
58+
}
59+
}
60+
if !bootstrap.auto_verify_domains.is_empty() {
61+
println!(" Auto-verify domains: {:?}", bootstrap.auto_verify_domains);
62+
}
63+
if let Some(ref key) = bootstrap.initial_api_key {
64+
println!(" Create API key: name={}", key.name);
65+
}
66+
println!("=== No changes applied (dry run) ===");
67+
std::process::exit(0);
68+
}
69+
70+
// Connect to database and run migrations
71+
let db = match db::DbPool::from_config(&config.database).await {
72+
Ok(pool) => {
73+
if let Err(e) = pool.run_migrations().await {
74+
eprintln!("Error: Database migrations failed: {e}");
75+
std::process::exit(1);
76+
}
77+
std::sync::Arc::new(pool)
78+
}
79+
Err(e) => {
80+
eprintln!("Error: Failed to connect to database: {e}");
81+
std::process::exit(1);
82+
}
83+
};
84+
85+
let file_storage: std::sync::Arc<dyn services::FileStorage> =
86+
std::sync::Arc::new(services::DatabaseFileStorage::new(db.clone()));
87+
let max_cel = config.auth.rbac.max_expression_length;
88+
let services = services::Services::new(db.clone(), file_storage, max_cel);
89+
90+
let api_key_prefix = config.auth.api_key_config().generation_prefix();
91+
let mut summary = Vec::new();
92+
93+
// 1. Create org if configured
94+
let org_id = if let Some(ref org_config) = bootstrap.initial_org {
95+
match services
96+
.organizations
97+
.create(models::CreateOrganization {
98+
slug: org_config.slug.clone(),
99+
name: org_config.name.clone(),
100+
})
101+
.await
102+
{
103+
Ok(org) => {
104+
let msg = format!("Created organization: {} ({})", org.slug, org.id);
105+
tracing::info!("{msg}");
106+
summary.push(msg);
107+
Some(org.id)
108+
}
109+
Err(db::DbError::Conflict(_)) => {
110+
let existing = services
111+
.organizations
112+
.get_by_slug(&org_config.slug)
113+
.await
114+
.unwrap_or(None);
115+
if let Some(org) = existing {
116+
let msg = format!("Organization already exists: {} ({})", org.slug, org.id);
117+
tracing::info!("{msg}");
118+
summary.push(msg);
119+
Some(org.id)
120+
} else {
121+
eprintln!("Error: Organization conflict but not found by slug");
122+
std::process::exit(1);
123+
}
124+
}
125+
Err(e) => {
126+
eprintln!("Error creating organization: {e}");
127+
std::process::exit(1);
128+
}
129+
}
130+
} else {
131+
None
132+
};
133+
134+
// 2. Configure SSO if specified
135+
#[cfg(feature = "sso")]
136+
if let Some(ref org_config) = bootstrap.initial_org
137+
&& let (Some(sso_config), Some(oid)) = (&org_config.sso, org_id)
138+
{
139+
// Check if SSO config already exists
140+
let existing = services.org_sso_configs.get_by_org_id(oid).await;
141+
if let Ok(Some(_)) = existing {
142+
let msg = format!("SSO config already exists for org {oid}");
143+
tracing::info!("{msg}");
144+
summary.push(msg);
145+
} else {
146+
// Initialize secret manager for SSO (reuse same logic as AppState)
147+
let secret_manager: std::sync::Arc<dyn crate::secrets::SecretManager> =
148+
match crate::init::init_secret_manager(&config).await {
149+
Ok(sm) => sm,
150+
Err(e) => {
151+
eprintln!("Error initializing secret manager for SSO: {e}");
152+
std::process::exit(1);
153+
}
154+
};
155+
156+
let provider_type = match sso_config.provider_type.as_str() {
157+
"saml" => models::SsoProviderType::Saml,
158+
_ => models::SsoProviderType::Oidc,
159+
};
160+
161+
let create_input = models::CreateOrgSsoConfig {
162+
provider_type,
163+
issuer: sso_config.issuer.clone(),
164+
discovery_url: sso_config.discovery_url.clone(),
165+
client_id: sso_config.client_id.clone(),
166+
client_secret: sso_config.client_secret.clone(),
167+
redirect_uri: sso_config.redirect_uri.clone(),
168+
allowed_email_domains: sso_config.allowed_email_domains.clone(),
169+
..Default::default()
170+
};
171+
172+
match services
173+
.org_sso_configs
174+
.create(oid, create_input, secret_manager.as_ref())
175+
.await
176+
{
177+
Ok(created) => {
178+
let msg = format!("Created SSO config for org {oid} ({})", created.id);
179+
tracing::info!("{msg}");
180+
summary.push(msg);
181+
182+
// Auto-verify domains
183+
for domain in &bootstrap.auto_verify_domains {
184+
if sso_config.allowed_email_domains.contains(domain) {
185+
match services
186+
.domain_verifications
187+
.create_auto_verified(created.id, domain)
188+
.await
189+
{
190+
Ok(_) => {
191+
let msg = format!("Auto-verified domain: {domain}");
192+
tracing::info!("{msg}");
193+
summary.push(msg);
194+
}
195+
Err(e) => {
196+
tracing::warn!("Failed to auto-verify domain {domain}: {e}");
197+
}
198+
}
199+
}
200+
}
201+
}
202+
Err(e) => {
203+
eprintln!("Error creating SSO config: {e}");
204+
std::process::exit(1);
205+
}
206+
}
207+
}
208+
}
209+
210+
// 3. Create API key if configured
211+
if let Some(ref key_config) = bootstrap.initial_api_key {
212+
let oid = if let Some(oid) = org_id {
213+
oid
214+
} else {
215+
eprintln!("Error: initial_api_key requires initial_org to be configured.");
216+
std::process::exit(1);
217+
};
218+
219+
// Check if key already exists (idempotent)
220+
match services
221+
.api_keys
222+
.get_by_name_and_org(oid, &key_config.name)
223+
.await
224+
{
225+
Ok(Some(existing)) => {
226+
let msg = format!(
227+
"API key already exists: {} ({})",
228+
existing.name, existing.id
229+
);
230+
tracing::info!("{msg}");
231+
summary.push(msg);
232+
}
233+
Ok(None) => {
234+
let owner = models::ApiKeyOwner::Organization { org_id: oid };
235+
match services
236+
.api_keys
237+
.create(
238+
models::CreateApiKey {
239+
name: key_config.name.clone(),
240+
owner,
241+
budget_limit_cents: None,
242+
budget_period: None,
243+
expires_at: None,
244+
scopes: None,
245+
allowed_models: None,
246+
ip_allowlist: None,
247+
rate_limit_rpm: None,
248+
rate_limit_tpm: None,
249+
},
250+
&api_key_prefix,
251+
)
252+
.await
253+
{
254+
Ok(created) => {
255+
let msg = format!(
256+
"Created API key: {} ({})",
257+
created.api_key.name, created.api_key.id
258+
);
259+
tracing::info!("{msg}");
260+
summary.push(msg);
261+
// Print the raw key to stdout (only shown once)
262+
println!("{}", created.key);
263+
}
264+
Err(e) => {
265+
eprintln!("Error creating API key: {e}");
266+
std::process::exit(1);
267+
}
268+
}
269+
}
270+
Err(e) => {
271+
eprintln!("Error checking for existing API key: {e}");
272+
std::process::exit(1);
273+
}
274+
}
275+
}
276+
277+
// Print summary
278+
eprintln!();
279+
eprintln!("=== Bootstrap Summary ===");
280+
for line in &summary {
281+
eprintln!(" {line}");
282+
}
283+
if summary.is_empty() {
284+
eprintln!(" No changes made (nothing configured in [auth.bootstrap])");
285+
}
286+
eprintln!("=========================");
287+
}

0 commit comments

Comments
 (0)