11import { existsSync } from "node:fs" ;
2- import { configString , loadConfig , readJsonFile , runText , writeIfChanged } from "./lib/common" ;
2+ import { configString , loadConfig , readJsonFile , runAllowFailure , runText , writeIfChanged } from "./lib/common" ;
33
44const PREFIX = "terrariumctl idp sync" ;
55const DEFAULT_CONFIG_PATH = process . env . TERRARIUM_CONFIG_PATH ?? "/etc/terrarium/config.yaml" ;
6+ const DEFAULT_ZITADEL_DIR = "/var/lib/terrarium/zitadel" ;
67const DEFAULT_BOOTSTRAP_DIR = "/var/lib/terrarium/zitadel/bootstrap" ;
78const DEFAULT_TF_DIR = "/var/lib/terrarium/zitadel/terraform" ;
89const DEFAULT_OUTPUTS_PATH = "/etc/terrarium/zitadel-apps.json" ;
910const DEFAULT_TOFU_IMAGE = "ghcr.io/opentofu/opentofu:1.10.6" ;
11+ const WAIT_INTERVAL_MS = 5000 ;
12+ const WAIT_ATTEMPTS = 36 ;
1013
1114async function dockerRun ( args : string [ ] ) : Promise < string > {
1215 return await runText ( [ "docker" , ...args ] , PREFIX ) ;
1316}
1417
18+ async function waitForFile ( path : string , label : string ) : Promise < void > {
19+ for ( let attempt = 0 ; attempt < WAIT_ATTEMPTS ; attempt += 1 ) {
20+ if ( existsSync ( path ) ) {
21+ return ;
22+ }
23+ await Bun . sleep ( WAIT_INTERVAL_MS ) ;
24+ }
25+ throw new Error ( `timed out waiting for ${ label } : ${ path } ` ) ;
26+ }
27+
28+ async function waitForApiReady ( stackDir : string ) : Promise < void > {
29+ let lastError = "" ;
30+ for ( let attempt = 0 ; attempt < WAIT_ATTEMPTS ; attempt += 1 ) {
31+ const result = await runAllowFailure (
32+ [
33+ "docker" ,
34+ "compose" ,
35+ "--project-name" ,
36+ "terrarium-zitadel" ,
37+ "-f" ,
38+ `${ stackDir } /docker-compose.yml` ,
39+ "exec" ,
40+ "-T" ,
41+ "zitadel-api" ,
42+ "/app/zitadel" ,
43+ "ready"
44+ ] ,
45+ { cwd : stackDir }
46+ ) ;
47+ if ( result . exitCode === 0 ) {
48+ return ;
49+ }
50+ lastError = result . stderr . trim ( ) || result . stdout . trim ( ) || "container is not ready yet" ;
51+ await Bun . sleep ( WAIT_INTERVAL_MS ) ;
52+ }
53+ throw new Error ( `timed out waiting for ZITADEL API readiness: ${ lastError } ` ) ;
54+ }
55+
1556export async function idpSyncCmd ( configPath = DEFAULT_CONFIG_PATH ) : Promise < void > {
1657 const config = loadConfig ( configPath , PREFIX ) ;
1758 if ( configString ( config , "terrarium_idp_mode" ) !== "zitadel_self_hosted" ) {
1859 return ;
1960 }
2061
2162 const authDomain = configString ( config , "terrarium_auth_domain" ) ;
63+ const zitadelDir = configString ( config , "terrarium_zitadel_dir" , DEFAULT_ZITADEL_DIR ) ;
2264 const bootstrapDir = configString ( config , "terrarium_zitadel_bootstrap_dir" , DEFAULT_BOOTSTRAP_DIR ) ;
2365 const tfDir = configString ( config , "terrarium_zitadel_tf_dir" , DEFAULT_TF_DIR ) ;
2466 const outputsPath = configString ( config , "terrarium_zitadel_outputs_path" , DEFAULT_OUTPUTS_PATH ) ;
@@ -27,13 +69,14 @@ export async function idpSyncCmd(configPath = DEFAULT_CONFIG_PATH): Promise<void
2769 if ( ! authDomain ) {
2870 throw new Error ( "terrarium_auth_domain is empty" ) ;
2971 }
30- if ( ! existsSync ( `${ bootstrapDir } /admin-sa.json` ) ) {
31- throw new Error ( `missing bootstrap machine key: ${ bootstrapDir } /admin-sa.json` ) ;
32- }
3372 if ( ! existsSync ( tfDir ) ) {
3473 throw new Error ( `terraform directory not found: ${ tfDir } ` ) ;
3574 }
3675
76+ await waitForFile ( `${ bootstrapDir } /admin-sa.json` , "bootstrap machine key" ) ;
77+ await waitForFile ( `${ bootstrapDir } /login-client.pat` , "login client PAT" ) ;
78+ await waitForApiReady ( zitadelDir ) ;
79+
3780 const commonArgs = [
3881 "run" ,
3982 "--rm" ,
0 commit comments