@@ -148,6 +148,12 @@ func EnableDefaultFlows(rc *eos_io.RuntimeContext, cfg *DefaultFlowsConfig) erro
148148 return fmt .Errorf ("failed to render flow template %q: %w" , flow .Name , err )
149149 }
150150
151+ // CRITICAL: Log rendered YAML preview to verify template interpolation
152+ logger .Debug ("Rendered flow YAML" ,
153+ zap .String ("flow" , flow .Name ),
154+ zap .Int ("yaml_size" , len (rendered )),
155+ zap .String ("yaml_preview" , string (rendered [:min (500 , len (rendered ))]))) // First 500 chars
156+
151157 if cfg .DryRun {
152158 logger .Info ("[dry-run] Would import Authentik flow" ,
153159 zap .String ("flow" , flow .Name ),
@@ -163,13 +169,31 @@ func EnableDefaultFlows(rc *eos_io.RuntimeContext, cfg *DefaultFlowsConfig) erro
163169 }
164170 }
165171
172+ logger .Info ("Importing flow to Authentik" ,
173+ zap .String ("flow" , flow .Name ),
174+ zap .String ("slug" , flow .Slug ))
175+
166176 if err := client .ImportFlow (rc .Ctx , rendered ); err != nil {
167177 return fmt .Errorf ("failed to import flow %q: %w" , flow .Name , err )
168178 }
169179
170- logger .Info ("✓ Flow imported" ,
180+ // CRITICAL: Verify flow actually exists after import
181+ // RATIONALE: Authentik may return 200 OK but not create the flow (validation errors, etc.)
182+ // Use retry to handle eventual consistency (API indexing lag)
183+ // RETRY STRATEGY: 5 attempts with 2s initial delay (2s, 4s, 8s, 16s, 32s = max 62s wait)
184+ verifiedFlow := getFlowWithRetry (rc , client , logger , flow .Slug , 5 , 2 * time .Second )
185+ if verifiedFlow == nil {
186+ logger .Error ("Flow import reported success but flow does not exist" ,
187+ zap .String ("flow_name" , flow .Name ),
188+ zap .String ("slug" , flow .Slug ),
189+ zap .String ("remediation" , "Check Authentik logs and YAML syntax" ))
190+ return fmt .Errorf ("flow import verification failed: flow %q does not exist after import" , flow .Name )
191+ }
192+
193+ logger .Info ("✓ Flow imported and verified" ,
171194 zap .String ("flow" , flow .Name ),
172- zap .String ("slug" , flow .Slug ))
195+ zap .String ("slug" , flow .Slug ),
196+ zap .String ("uuid" , verifiedFlow .PK ))
173197 importedSlugs = append (importedSlugs , flow .Slug )
174198 }
175199
@@ -202,11 +226,12 @@ func EnableDefaultFlows(rc *eos_io.RuntimeContext, cfg *DefaultFlowsConfig) erro
202226 // CRITICAL: Brand API requires flow UUIDs, not slugs
203227 // Lookup each flow to get its PK (UUID) with retry logic for eventual consistency
204228 // RATIONALE: Freshly imported flows may not be immediately queryable due to Authentik indexing
205- authFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-authentication" , appSlug ), 3 , 2 * time .Second )
206- enrollmentFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-enrollment" , appSlug ), 3 , 2 * time .Second )
207- invalidationGlobalFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-invalidation-global" , appSlug ), 3 , 2 * time .Second )
208- recoveryFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-recovery" , appSlug ), 3 , 2 * time .Second )
209- unenrollmentFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-unenrollment" , appSlug ), 3 , 2 * time .Second )
229+ // RETRY STRATEGY: 5 attempts with exponential backoff (1s, 2s, 4s, 8s, 16s = max 31s wait)
230+ authFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-authentication" , appSlug ), 5 , 1 * time .Second )
231+ enrollmentFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-enrollment" , appSlug ), 5 , 1 * time .Second )
232+ invalidationGlobalFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-invalidation-global" , appSlug ), 5 , 1 * time .Second )
233+ recoveryFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-recovery" , appSlug ), 5 , 1 * time .Second )
234+ unenrollmentFlow := getFlowWithRetry (rc , client , logger , fmt .Sprintf ("%s-unenrollment" , appSlug ), 5 , 1 * time .Second )
210235
211236 // Only update brand if we successfully looked up all flow UUIDs
212237 if authFlow != nil && enrollmentFlow != nil && invalidationGlobalFlow != nil && recoveryFlow != nil && unenrollmentFlow != nil {
@@ -963,41 +988,49 @@ entries:
963988 timeout: 30
964989`
965990
966- // getFlowWithRetry attempts to retrieve a flow with retry logic for eventual consistency
967- // RATIONALE: Freshly imported flows may not be immediately queryable in Authentik
968- // This is a timing issue - the flow exists but the index hasn't updated yet
969- func getFlowWithRetry (rc * eos_io.RuntimeContext , client * authentik.APIClient , logger otelzap.LoggerWithCtx , slug string , maxRetries int , retryDelay time.Duration ) * authentik.FlowResponse {
991+ // getFlowWithRetry attempts to retrieve a flow with retry logic and exponential backoff
992+ // RATIONALE: Freshly imported flows may not be immediately queryable in Authentik due to indexing lag
993+ // This is a timing issue - the flow exists but the API index hasn't updated yet
994+ // RETRY STRATEGY: Exponential backoff (1s, 2s, 4s, 8s, 16s) for up to 5 attempts (max 31s wait)
995+ func getFlowWithRetry (rc * eos_io.RuntimeContext , client * authentik.APIClient , logger otelzap.LoggerWithCtx , slug string , maxRetries int , initialDelay time.Duration ) * authentik.FlowResponse {
996+ currentDelay := initialDelay
997+
970998 for attempt := 1 ; attempt <= maxRetries ; attempt ++ {
971999 flow , err := client .GetFlow (rc .Ctx , slug )
9721000 if err != nil {
973- logger .Warn ("Failed to lookup flow UUID" ,
1001+ logger .Warn ("Failed to lookup flow UUID (API error) " ,
9741002 zap .String ("slug" , slug ),
9751003 zap .Int ("attempt" , attempt ),
9761004 zap .Int ("max_retries" , maxRetries ),
9771005 zap .Error (err ))
9781006 } else if flow != nil {
9791007 // Success - flow found
980- logger .Debug ("Found flow UUID" ,
1008+ logger .Debug ("Flow UUID resolved " ,
9811009 zap .String ("slug" , slug ),
9821010 zap .String ("uuid" , flow .PK ),
1011+ zap .String ("flow_name" , flow .Name ),
9831012 zap .Int ("attempt" , attempt ))
9841013 return flow
9851014 }
9861015
9871016 // Flow not found yet (flow == nil, err == nil means "not found" per GetFlow contract)
9881017 if attempt < maxRetries {
989- logger .Debug ("Flow not found yet, waiting before retry " ,
1018+ logger .Debug ("Flow not indexed yet, retrying with exponential backoff " ,
9901019 zap .String ("slug" , slug ),
9911020 zap .Int ("attempt" , attempt ),
9921021 zap .Int ("max_retries" , maxRetries ),
993- zap .Duration ("retry_delay" , retryDelay ))
994- time .Sleep (retryDelay )
1022+ zap .Duration ("retry_delay" , currentDelay ),
1023+ zap .String ("reason" , "Authentik API eventual consistency" ))
1024+ time .Sleep (currentDelay )
1025+ currentDelay *= 2 // Exponential backoff: 1s → 2s → 4s → 8s → 16s
9951026 }
9961027 }
9971028
998- // All retries exhausted
999- logger .Warn ("Flow not found after all retries" ,
1029+ // All retries exhausted - flow may not exist or API is very slow
1030+ logger .Warn ("Flow not found after all retries (may not exist or API indexing is slow) " ,
10001031 zap .String ("slug" , slug ),
1001- zap .Int ("max_retries" , maxRetries ))
1032+ zap .Int ("max_retries" , maxRetries ),
1033+ zap .Duration ("total_wait_time" , initialDelay * (1 << uint (maxRetries )- 1 )), // Sum of geometric series
1034+ zap .String ("remediation" , "Check if flow was actually imported successfully" ))
10021035 return nil
10031036}
0 commit comments