1- pub mod webserver_config;
1+ //! Builds the `webserver_config.py` Flask configuration file from the resolved
2+ //! authentication/authorization config plus user-provided config overrides.
23
3- use std:: collections:: BTreeMap ;
4+ use std:: { collections:: BTreeMap , io :: Write } ;
45
56use indoc:: formatdoc;
67use snafu:: { ResultExt , Snafu } ;
78use stackable_operator:: {
89 commons:: tls_verification:: TlsVerification ,
910 crd:: authentication:: { ldap, oidc} ,
11+ v2:: flask_config_writer,
1012} ;
1113
12- use crate :: crd:: {
13- AirflowConfigOptions ,
14- authentication:: {
15- AirflowAuthenticationClassResolved , AirflowClientAuthenticationDetailsResolved ,
16- DEFAULT_OIDC_PROVIDER , FlaskRolesSyncMoment ,
14+ use crate :: {
15+ controller:: ValidatedCluster ,
16+ crd:: {
17+ AirflowConfigOptions ,
18+ authentication:: {
19+ AirflowAuthenticationClassResolved , AirflowClientAuthenticationDetailsResolved ,
20+ DEFAULT_OIDC_PROVIDER , FlaskRolesSyncMoment ,
21+ } ,
22+ authorization:: AirflowAuthorizationResolved ,
1723 } ,
18- authorization:: AirflowAuthorizationResolved ,
1924} ;
2025
21- pub const PYTHON_IMPORTS : & [ & str ] = & [
26+ const PYTHON_IMPORTS : & [ & str ] = & [
2227 "import os" ,
2328 "from flask_appbuilder.const import (AUTH_DB, AUTH_LDAP, AUTH_OAUTH, AUTH_REMOTE_USER)" ,
2429 "basedir = os.path.abspath(os.path.dirname(__file__))" ,
2530 "WTF_CSRF_ENABLED = True" ,
2631] ;
2732
33+ /// Marks arbitrary Python code to prepend verbatim to the generated file.
34+ const CONFIG_OVERRIDE_FILE_HEADER_KEY : & str = "FILE_HEADER" ;
35+ /// Marks arbitrary Python code to append verbatim to the generated file.
36+ const CONFIG_OVERRIDE_FILE_FOOTER_KEY : & str = "FILE_FOOTER" ;
37+
2838type Result < T , E = Error > = std:: result:: Result < T , E > ;
2939
3040#[ derive( Snafu , Debug ) ]
@@ -37,6 +47,57 @@ pub enum Error {
3747
3848 #[ snafu( display( "invalid well-known OIDC configuration URL" ) ) ]
3949 InvalidWellKnownConfigUrl { source : oidc:: v1alpha1:: Error } ,
50+
51+ #[ snafu( display( "failed to write the webserver config file" ) ) ]
52+ WriteConfigFile {
53+ source : flask_config_writer:: FlaskAppConfigWriterError ,
54+ } ,
55+
56+ #[ snafu( display( "failed to write the header/footer to the webserver config file" ) ) ]
57+ WriteHeaderFooter { source : std:: io:: Error } ,
58+ }
59+
60+ /// Renders the `webserver_config.py` contents: operator defaults (derived from the
61+ /// resolved authentication/authorization config) with the user's `config_overrides`
62+ /// applied last, wrapped by the optional `FILE_HEADER`/`FILE_FOOTER` Python blocks.
63+ pub fn build (
64+ validated_cluster : & ValidatedCluster ,
65+ config_file_overrides : & BTreeMap < String , String > ,
66+ ) -> Result < String , Error > {
67+ // this will call default values from AirflowClientAuthenticationDetails
68+ let mut config = build_airflow_config (
69+ & validated_cluster. cluster_config . authentication_config ,
70+ & validated_cluster. cluster_config . authorization_config ,
71+ & validated_cluster. image . product_version ,
72+ ) ?;
73+
74+ let mut file_config = config_file_overrides. clone ( ) ;
75+
76+ // now add any overrides, replacing any defaults
77+ config. append ( & mut file_config) ;
78+
79+ let mut config_file = Vec :: new ( ) ;
80+
81+ // By removing the keys from `config`, we avoid pasting the Python code into a Python variable as well
82+ // (which would be bad)
83+ if let Some ( header) = config. remove ( CONFIG_OVERRIDE_FILE_HEADER_KEY ) {
84+ writeln ! ( config_file, "{}" , header) . context ( WriteHeaderFooterSnafu ) ?;
85+ }
86+
87+ let temp_file_footer: Option < String > = config. remove ( CONFIG_OVERRIDE_FILE_FOOTER_KEY ) ;
88+
89+ flask_config_writer:: write :: < AirflowConfigOptions , _ , _ > (
90+ & mut config_file,
91+ config. iter ( ) ,
92+ PYTHON_IMPORTS ,
93+ )
94+ . context ( WriteConfigFileSnafu ) ?;
95+
96+ if let Some ( footer) = temp_file_footer {
97+ writeln ! ( config_file, "{}" , footer) . context ( WriteHeaderFooterSnafu ) ?;
98+ }
99+
100+ Ok ( String :: from_utf8 ( config_file) . expect ( "the Flask config writer only emits valid UTF-8" ) )
40101}
41102
42103/// Builds the operator-default `webserver_config.py` entries derived from the resolved
@@ -322,15 +383,13 @@ mod tests {
322383 shared:: time:: Duration ,
323384 } ;
324385
325- use crate :: {
326- config:: build_airflow_config,
327- crd:: {
328- authentication:: {
329- AirflowAuthenticationClassResolved , AirflowClientAuthenticationDetailsResolved ,
330- FlaskRolesSyncMoment , default_user_registration,
331- } ,
332- authorization:: { AirflowAuthorizationResolved , OpaConfigResolved } ,
386+ use super :: build_airflow_config;
387+ use crate :: crd:: {
388+ authentication:: {
389+ AirflowAuthenticationClassResolved , AirflowClientAuthenticationDetailsResolved ,
390+ FlaskRolesSyncMoment , default_user_registration,
333391 } ,
392+ authorization:: { AirflowAuthorizationResolved , OpaConfigResolved } ,
334393 } ;
335394
336395 const TEST_AIRFLOW_VERSION : & str = "3.0.6" ;
0 commit comments