cornflow 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. cornflow/app.py +4 -2
  2. cornflow/cli/__init__.py +4 -0
  3. cornflow/cli/actions.py +4 -0
  4. cornflow/cli/config.py +4 -0
  5. cornflow/cli/migrations.py +13 -8
  6. cornflow/cli/permissions.py +4 -0
  7. cornflow/cli/roles.py +5 -1
  8. cornflow/cli/schemas.py +5 -0
  9. cornflow/cli/service.py +263 -131
  10. cornflow/cli/tools/api_generator.py +13 -10
  11. cornflow/cli/tools/endpoint_tools.py +191 -196
  12. cornflow/cli/tools/models_tools.py +87 -60
  13. cornflow/cli/tools/schema_generator.py +161 -67
  14. cornflow/cli/tools/schemas_tools.py +4 -5
  15. cornflow/cli/users.py +8 -0
  16. cornflow/cli/views.py +4 -0
  17. cornflow/commands/access.py +14 -3
  18. cornflow/commands/auxiliar.py +106 -0
  19. cornflow/commands/dag.py +3 -2
  20. cornflow/commands/permissions.py +186 -81
  21. cornflow/commands/roles.py +15 -14
  22. cornflow/commands/schemas.py +6 -4
  23. cornflow/commands/users.py +12 -17
  24. cornflow/commands/views.py +171 -41
  25. cornflow/endpoints/dag.py +27 -25
  26. cornflow/endpoints/data_check.py +128 -165
  27. cornflow/endpoints/example_data.py +9 -3
  28. cornflow/endpoints/execution.py +40 -34
  29. cornflow/endpoints/health.py +7 -7
  30. cornflow/endpoints/instance.py +39 -12
  31. cornflow/endpoints/meta_resource.py +4 -5
  32. cornflow/schemas/execution.py +9 -1
  33. cornflow/schemas/health.py +1 -0
  34. cornflow/shared/authentication/auth.py +76 -45
  35. cornflow/shared/const.py +10 -1
  36. cornflow/shared/exceptions.py +3 -1
  37. cornflow/shared/utils_tables.py +36 -8
  38. cornflow/shared/validators.py +1 -1
  39. cornflow/tests/const.py +1 -0
  40. cornflow/tests/custom_test_case.py +4 -4
  41. cornflow/tests/unit/test_alarms.py +1 -2
  42. cornflow/tests/unit/test_cases.py +4 -7
  43. cornflow/tests/unit/test_executions.py +22 -1
  44. cornflow/tests/unit/test_external_role_creation.py +785 -0
  45. cornflow/tests/unit/test_health.py +4 -1
  46. cornflow/tests/unit/test_log_in.py +46 -9
  47. cornflow/tests/unit/test_tables.py +3 -3
  48. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/METADATA +2 -2
  49. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/RECORD +52 -50
  50. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/WHEEL +1 -1
  51. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/entry_points.txt +0 -0
  52. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/top_level.txt +0 -0
cornflow/app.py CHANGED
@@ -51,6 +51,8 @@ def create_app(env_name="development", dataconn=None):
51
51
  """
52
52
  dictConfig(log_config(app_config[env_name].LOG_LEVEL))
53
53
 
54
+ # Note: Explicit CSRF protection is not configured as the application uses
55
+ # JWT for authentication via headers, mitigating standard CSRF vulnerabilities.
54
56
  app = Flask(__name__)
55
57
  app.json.sort_keys = False
56
58
  app.logger.setLevel(app_config[env_name].LOG_LEVEL)
@@ -103,7 +105,7 @@ def create_app(env_name="development", dataconn=None):
103
105
  else:
104
106
  raise ConfigurationError(
105
107
  error="Invalid authentication type",
106
- log_txt="Error while configuring authentication. The authentication type is not valid."
108
+ log_txt="Error while configuring authentication. The authentication type is not valid.",
107
109
  )
108
110
 
109
111
  initialize_errorhandlers(app)
@@ -162,7 +164,7 @@ def create_base_user(username, email, password, verbose):
162
164
  @click.option("-v", "--verbose", is_flag=True, default=False)
163
165
  @with_appcontext
164
166
  def register_roles(verbose):
165
- register_roles_command(verbose)
167
+ register_roles_command(verbose=verbose)
166
168
 
167
169
 
168
170
  @click.command("register_actions")
cornflow/cli/__init__.py CHANGED
@@ -17,6 +17,10 @@ from cornflow.cli.views import views
17
17
 
18
18
  @click.group(name="cornflow", help="Commands in the cornflow cli")
19
19
  def cli():
20
+ """
21
+ This method is empty but it serves as the building block
22
+ for the rest of the commands
23
+ """
20
24
  pass
21
25
 
22
26
 
cornflow/cli/actions.py CHANGED
@@ -7,6 +7,10 @@ from .arguments import verbose
7
7
 
8
8
  @click.group(name="actions", help="Commands to manage the actions")
9
9
  def actions():
10
+ """
11
+ This method is empty but it serves as the building block
12
+ for the rest of the commands
13
+ """
10
14
  pass
11
15
 
12
16
 
cornflow/cli/config.py CHANGED
@@ -7,6 +7,10 @@ from .arguments import path
7
7
 
8
8
  @click.group(name="config", help="Commands to manage the configuration variables")
9
9
  def config():
10
+ """
11
+ This method is empty but it serves as the building block
12
+ for the rest of the commands
13
+ """
10
14
  pass
11
15
 
12
16
 
@@ -3,6 +3,7 @@ import os.path
3
3
 
4
4
  import click
5
5
  from cornflow.shared import db
6
+ from cornflow.shared.const import MIGRATIONS_DEFAULT_PATH
6
7
  from flask_migrate import Migrate, migrate, upgrade, downgrade, init
7
8
 
8
9
  from .utils import get_app
@@ -10,6 +11,10 @@ from .utils import get_app
10
11
 
11
12
  @click.group(name="migrations", help="Commands to manage the migrations")
12
13
  def migrations():
14
+ """
15
+ This method is empty but it serves as the building block
16
+ for the rest of the commands
17
+ """
13
18
  pass
14
19
 
15
20
 
@@ -18,12 +23,12 @@ def migrate_migrations():
18
23
  app = get_app()
19
24
  external = int(os.getenv("EXTERNAL_APP", 0))
20
25
  if external == 0:
21
- path = "./cornflow/migrations"
26
+ path = MIGRATIONS_DEFAULT_PATH
22
27
  else:
23
28
  path = f"./{os.getenv('EXTERNAL_APP_MODULE', 'external_app')}/migrations"
24
29
 
25
30
  with app.app_context():
26
- migration_client = Migrate(app=app, db=db, directory=path)
31
+ Migrate(app=app, db=db, directory=path)
27
32
  migrate()
28
33
 
29
34
 
@@ -35,12 +40,12 @@ def upgrade_migrations(revision="head"):
35
40
  app = get_app()
36
41
  external = int(os.getenv("EXTERNAL_APP", 0))
37
42
  if external == 0:
38
- path = "./cornflow/migrations"
43
+ path = MIGRATIONS_DEFAULT_PATH
39
44
  else:
40
45
  path = f"./{os.getenv('EXTERNAL_APP_MODULE', 'external_app')}/migrations"
41
46
 
42
47
  with app.app_context():
43
- migration_client = Migrate(app=app, db=db, directory=path)
48
+ Migrate(app=app, db=db, directory=path)
44
49
  upgrade(revision=revision)
45
50
 
46
51
 
@@ -52,12 +57,12 @@ def downgrade_migrations(revision="-1"):
52
57
  app = get_app()
53
58
  external = int(os.getenv("EXTERNAL_APP", 0))
54
59
  if external == 0:
55
- path = "./cornflow/migrations"
60
+ path = MIGRATIONS_DEFAULT_PATH
56
61
  else:
57
62
  path = f"./{os.getenv('EXTERNAL_APP_MODULE', 'external_app')}/migrations"
58
63
 
59
64
  with app.app_context():
60
- migration_client = Migrate(app=app, db=db, directory=path)
65
+ Migrate(app=app, db=db, directory=path)
61
66
  downgrade(revision=revision)
62
67
 
63
68
 
@@ -69,10 +74,10 @@ def init_migrations():
69
74
  app = get_app()
70
75
  external = int(os.getenv("EXTERNAL_APP", 0))
71
76
  if external == 0:
72
- path = "./cornflow/migrations"
77
+ path = MIGRATIONS_DEFAULT_PATH
73
78
  else:
74
79
  path = f"./{os.getenv('EXTERNAL_APP_MODULE', 'external_app')}/migrations"
75
80
 
76
81
  with app.app_context():
77
- migration_client = Migrate(app=app, db=db, directory=path)
82
+ Migrate(app=app, db=db, directory=path)
78
83
  init()
@@ -11,6 +11,10 @@ from .utils import get_app
11
11
 
12
12
  @click.group(name="permissions", help="Commands to manage the permissions")
13
13
  def permissions():
14
+ """
15
+ This method is empty but it serves as the building block
16
+ for the rest of the commands
17
+ """
14
18
  pass
15
19
 
16
20
 
cornflow/cli/roles.py CHANGED
@@ -6,6 +6,10 @@ from .utils import get_app
6
6
 
7
7
  @click.group(name="roles", help="Commands to manage the roles")
8
8
  def roles():
9
+ """
10
+ This method is empty but it serves as the building block
11
+ for the rest of the commands
12
+ """
9
13
  pass
10
14
 
11
15
 
@@ -14,4 +18,4 @@ def roles():
14
18
  def init(verbose):
15
19
  app = get_app()
16
20
  with app.app_context():
17
- register_roles_command(verbose)
21
+ register_roles_command(verbose=verbose)
cornflow/cli/schemas.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  File that implements the generate from schema cli command
3
3
  """
4
+
4
5
  import click
5
6
  from .tools.api_generator import APIGenerator
6
7
  from .tools.schema_generator import SchemaGenerator
@@ -20,6 +21,10 @@ METHOD_OPTIONS = [
20
21
 
21
22
  @click.group(name="schemas", help="Commands to manage the schemas")
22
23
  def schemas():
24
+ """
25
+ This method is empty but it serves as the building block
26
+ for the rest of the commands
27
+ """
23
28
  pass
24
29
 
25
30
 
cornflow/cli/service.py CHANGED
@@ -2,8 +2,20 @@ import os
2
2
  import subprocess
3
3
  import sys
4
4
  import time
5
+ import logging
5
6
  from logging import error
6
7
 
8
+ # Configure a logger for the service module
9
+ logger = logging.getLogger("cornflow.service")
10
+ logger.setLevel(logging.INFO)
11
+
12
+ # Add console handler if not already present
13
+ if not logger.handlers:
14
+ handler = logging.StreamHandler(sys.stdout)
15
+ formatter = logging.Formatter("%(asctime)s [%(name)s] [%(levelname)s] %(message)s")
16
+ handler.setFormatter(formatter)
17
+ logger.addHandler(handler)
18
+ logger.propagate = False
7
19
 
8
20
  import click
9
21
  from .utils import get_db_conn
@@ -29,30 +41,110 @@ from cornflow.shared import db
29
41
  from cryptography.fernet import Fernet
30
42
  from flask_migrate import Migrate, upgrade
31
43
 
44
+ MAIN_WD = "/usr/src/app"
45
+
32
46
 
33
47
  @click.group(name="service", help="Commands to run the cornflow service")
34
48
  def service():
49
+ """
50
+ This method is empty but it serves as the building block
51
+ for the rest of the commands
52
+ """
35
53
  pass
36
54
 
37
55
 
38
56
  @service.command(name="init", help="Initialize the service")
39
57
  def init_cornflow_service():
40
58
  click.echo("Starting the service")
41
- os.chdir("/usr/src/app")
59
+ os.chdir(MAIN_WD)
60
+
61
+ config = _setup_environment_variables()
62
+ _configure_logging(config["cornflow_logging"])
63
+
64
+ external_application = config["external_application"]
65
+ environment = config["environment"]
66
+ cornflow_db_conn = config["cornflow_db_conn"]
67
+ external_app_module = config["external_app_module"]
68
+
69
+ app = None # Initialize app to None
70
+
71
+ if external_application == 0:
72
+ click.echo("Initializing standard Cornflow application")
73
+ app = create_app(environment, cornflow_db_conn)
74
+ with app.app_context():
75
+ _initialize_database(app)
76
+ _create_initial_users(
77
+ config["auth"],
78
+ config["cornflow_admin_user"],
79
+ config["cornflow_admin_email"],
80
+ config["cornflow_admin_pwd"],
81
+ config["cornflow_service_user"],
82
+ config["cornflow_service_email"],
83
+ config["cornflow_service_pwd"],
84
+ )
85
+ _sync_with_airflow(
86
+ config["airflow_url"],
87
+ config["airflow_user"],
88
+ config["airflow_pwd"],
89
+ config["open_deployment"],
90
+ external_app=False,
91
+ )
92
+ _start_application(external_application, environment)
93
+
94
+ elif external_application == 1:
95
+ click.echo(f"Initializing Cornflow with external app: {external_app_module}")
96
+ if not external_app_module:
97
+ sys.exit("FATAL: EXTERNAL_APP is 1 but EXTERNAL_APP_MODULE is not set.")
98
+
99
+ _setup_external_app()
100
+ from importlib import import_module
101
+
102
+ external_app_lib = import_module(external_app_module)
103
+ app = external_app_lib.create_wsgi_app(environment, cornflow_db_conn)
104
+ with app.app_context():
105
+
106
+ _initialize_database(app, external_app_module)
107
+
108
+ _create_initial_users(
109
+ config["auth"],
110
+ config["cornflow_admin_user"],
111
+ config["cornflow_admin_email"],
112
+ config["cornflow_admin_pwd"],
113
+ config["cornflow_service_user"],
114
+ config["cornflow_service_email"],
115
+ config["cornflow_service_pwd"],
116
+ )
117
+
118
+ _sync_with_airflow(
119
+ config["airflow_url"],
120
+ config["airflow_user"],
121
+ config["airflow_pwd"],
122
+ config["open_deployment"],
123
+ external_app=True,
124
+ )
125
+ _start_application(external_application, environment, external_app_module)
126
+
127
+ else:
128
+ # This case should ideally be caught earlier or handled differently
129
+ sys.exit(f"FATAL: Invalid EXTERNAL_APP value: {external_application}")
130
+
131
+
132
+ def _setup_environment_variables():
133
+ """Reads environment variables, sets defaults, and returns config values."""
134
+
42
135
  environment = os.getenv("FLASK_ENV", "development")
43
136
  os.environ["FLASK_ENV"] = environment
44
137
 
45
- ###################################
46
- # Global defaults and back-compat #
47
- ###################################
48
- # Airflow global default conn
138
+ # Airflow details
49
139
  airflow_user = os.getenv("AIRFLOW_USER", "admin")
50
140
  airflow_pwd = os.getenv("AIRFLOW_PWD", "admin")
51
141
  airflow_url = os.getenv("AIRFLOW_URL", "http://webserver:8080")
52
- cornflow_url = os.environ.setdefault("cornflow_url", "http://cornflow:5000")
53
142
  os.environ["AIRFLOW_USER"] = airflow_user
54
143
  os.environ["AIRFLOW_PWD"] = airflow_pwd
55
144
  os.environ["AIRFLOW_URL"] = airflow_url
145
+
146
+ # Cornflow app config
147
+ os.environ.setdefault("cornflow_url", "http://cornflow:5000")
56
148
  os.environ["FLASK_APP"] = "cornflow.app"
57
149
  os.environ["SECRET_KEY"] = os.getenv("FERNET_KEY", Fernet.generate_key().decode())
58
150
 
@@ -74,10 +166,9 @@ def init_cornflow_service():
74
166
  )
75
167
  cornflow_service_pwd = os.getenv("CORNFLOW_SERVICE_PWD", "Service_user1234")
76
168
 
77
- # Cornflow logging and storage config
169
+ # Cornflow logging and deployment config
78
170
  cornflow_logging = os.getenv("CORNFLOW_LOGGING", "console")
79
171
  os.environ["CORNFLOW_LOGGING"] = cornflow_logging
80
-
81
172
  open_deployment = os.getenv("OPEN_DEPLOYMENT", 1)
82
173
  os.environ["OPEN_DEPLOYMENT"] = str(open_deployment)
83
174
  signup_activated = os.getenv("SIGNUP_ACTIVATED", 1)
@@ -88,161 +179,202 @@ def init_cornflow_service():
88
179
  os.environ["DEFAULT_ROLE"] = str(default_role)
89
180
 
90
181
  # Check LDAP parameters for active directory and show message
91
- if os.getenv("AUTH_TYPE") == AUTH_LDAP:
92
- print(
93
- "WARNING: Cornflow will be deployed with LDAP Authorization. Please review your ldap auth configuration."
182
+ if auth == AUTH_LDAP:
183
+ click.echo(
184
+ "WARNING: Cornflow will be deployed with LDAP Authorization. "
185
+ "Please review your ldap auth configuration."
94
186
  )
95
187
 
96
188
  # check database param from docker env
97
- if os.getenv("DATABASE_URL") is None:
189
+ if cornflow_db_conn is None:
98
190
  sys.exit("FATAL: you need to provide a postgres database for Cornflow")
99
191
 
100
- # set logrotate config file
192
+ external_application = int(os.getenv("EXTERNAL_APP", 0))
193
+ external_app_module = os.getenv("EXTERNAL_APP_MODULE")
194
+
195
+ return {
196
+ "environment": environment,
197
+ "auth": auth,
198
+ "airflow_user": airflow_user,
199
+ "airflow_pwd": airflow_pwd,
200
+ "airflow_url": airflow_url,
201
+ "cornflow_db_conn": cornflow_db_conn,
202
+ "cornflow_admin_user": cornflow_admin_user,
203
+ "cornflow_admin_email": cornflow_admin_email,
204
+ "cornflow_admin_pwd": cornflow_admin_pwd,
205
+ "cornflow_service_user": cornflow_service_user,
206
+ "cornflow_service_email": cornflow_service_email,
207
+ "cornflow_service_pwd": cornflow_service_pwd,
208
+ "cornflow_logging": cornflow_logging,
209
+ "open_deployment": open_deployment,
210
+ "external_application": external_application,
211
+ "external_app_module": external_app_module,
212
+ }
213
+
214
+
215
+ def _configure_logging(cornflow_logging):
216
+ """Configures log rotation if logging to file."""
101
217
  if cornflow_logging == "file":
102
218
  try:
103
- conf = "/usr/src/app/log/*.log {\n\
104
- rotate 30\n \
105
- daily\n\
106
- compress\n\
107
- size 20M\n\
108
- postrotate\n\
109
- kill -HUP \$(cat /usr/src/app/gunicorn.pid)\n \
110
- endscript}"
219
+ conf = f"""/usr/src/app/log/*.log {{
220
+ rotate 30
221
+ daily
222
+ compress
223
+ size 20M
224
+ postrotate
225
+ kill -HUP $(cat {MAIN_WD}/gunicorn.pid)
226
+ endscript}}"""
111
227
  logrotate = subprocess.run(
112
- f"cat > /etc/logrotate.d/cornflow <<EOF\n {conf} \nEOF", shell=True
228
+ f"cat > /etc/logrotate.d/cornflow <<EOF\n {conf} \nEOF",
229
+ shell=True,
230
+ capture_output=True,
231
+ text=True,
113
232
  )
114
- out_logrotate = logrotate.stdout
115
- print(out_logrotate)
233
+ if logrotate.returncode != 0:
234
+ error(f"Error configuring logrotate: {logrotate.stderr}")
235
+ else:
236
+ logger.info(logrotate.stdout)
237
+ except Exception as e:
238
+ error(f"Exception during logrotate configuration: {e}")
116
239
 
117
- except error:
118
- print(error)
119
240
 
120
- external_application = int(os.getenv("EXTERNAL_APP", 0))
121
- if external_application == 0:
122
- os.environ["GUNICORN_WORKING_DIR"] = "/usr/src/app"
123
- elif external_application == 1:
124
- os.environ["GUNICORN_WORKING_DIR"] = "/usr/src/app"
125
- else:
126
- raise Exception("No external application found")
241
+ def _initialize_database(app, external_app_module=None):
242
+ """Initializes the database and runs migrations."""
243
+ with app.app_context():
244
+ if external_app_module:
245
+ from importlib import import_module
127
246
 
128
- if external_application == 0:
129
- click.echo("Starting cornflow")
130
- app = create_app(environment, cornflow_db_conn)
131
- with app.app_context():
132
- path = f"{os.path.dirname(cornflow.__file__)}/migrations"
133
- Migrate(app=app, db=db, directory=path)
134
- upgrade()
135
- access_init_command(verbose=False)
136
- if auth == AUTH_DB or auth == AUTH_OID:
137
- # create cornflow admin user
138
- create_user_with_role(
139
- cornflow_admin_user,
140
- cornflow_admin_email,
141
- cornflow_admin_pwd,
142
- "admin",
143
- ADMIN_ROLE,
144
- verbose=True,
145
- )
146
- # create cornflow service user
147
- create_user_with_role(
148
- cornflow_service_user,
149
- cornflow_service_email,
150
- cornflow_service_pwd,
151
- "serviceuser",
152
- SERVICE_ROLE,
153
- verbose=True,
154
- )
155
- register_deployed_dags_command(
156
- airflow_url, airflow_user, airflow_pwd, verbose=True
157
- )
158
- register_dag_permissions_command(open_deployment, verbose=True)
159
- update_schemas_command(airflow_url, airflow_user, airflow_pwd, verbose=True)
247
+ external_app_lib = import_module(external_app_module)
248
+ migrations_path = f"{os.path.dirname(external_app_lib.__file__)}/migrations"
249
+ else:
250
+ migrations_path = f"{os.path.dirname(cornflow.__file__)}/migrations"
251
+
252
+ Migrate(app=app, db=db, directory=migrations_path)
253
+ upgrade()
254
+ logger.info("----------------Migrations applied----------------")
255
+ access_init_command(verbose=False)
160
256
 
161
- # execute gunicorn application
162
- os.system(
163
- "/usr/local/bin/gunicorn -c python:cornflow.gunicorn \"cornflow.app:create_app('$FLASK_ENV')\""
257
+
258
+ def _create_initial_users(
259
+ auth,
260
+ admin_user,
261
+ admin_email,
262
+ admin_pwd,
263
+ service_user,
264
+ service_email,
265
+ service_pwd,
266
+ ):
267
+ """Creates the initial admin and service users if using DB or OID auth."""
268
+ if auth == AUTH_DB or auth == AUTH_OID:
269
+ # create cornflow admin user
270
+ create_user_with_role(
271
+ admin_user,
272
+ admin_email,
273
+ admin_pwd,
274
+ "admin",
275
+ ADMIN_ROLE,
276
+ verbose=True,
277
+ )
278
+ # create cornflow service user
279
+ create_user_with_role(
280
+ service_user,
281
+ service_email,
282
+ service_pwd,
283
+ "serviceuser",
284
+ SERVICE_ROLE,
285
+ verbose=True,
164
286
  )
165
287
 
166
- elif external_application == 1:
167
- click.echo(f"Starting cornflow + {os.getenv('EXTERNAL_APP_MODULE')}")
168
- os.chdir("/usr/src/app")
169
288
 
170
- if register_key():
171
- prefix = "CUSTOM_SSH_"
172
- env_variables = {}
173
- for key, value in os.environ.items():
174
- if key.startswith(prefix):
175
- env_variables[key] = value
289
+ def _sync_with_airflow(
290
+ airflow_url, airflow_user, airflow_pwd, open_deployment, external_app=False
291
+ ):
292
+ """Syncs DAGs, permissions, and schemas with Airflow."""
293
+ register_deployed_dags_command(airflow_url, airflow_user, airflow_pwd, verbose=True)
294
+ register_dag_permissions_command(open_deployment, verbose=True)
295
+ update_schemas_command(airflow_url, airflow_user, airflow_pwd, verbose=True)
296
+ if external_app:
297
+ update_dag_registry_command(
298
+ airflow_url, airflow_user, airflow_pwd, verbose=True
299
+ )
176
300
 
177
- for _, value in env_variables.items():
178
- register_ssh_host(value)
179
301
 
180
- os.system("$(command -v pip) install --user -r requirements.txt")
181
- time.sleep(5)
182
- sys.path.append("/usr/src/app")
302
+ def _setup_external_app():
303
+ """Performs setup steps specific to external applications."""
183
304
 
184
- from importlib import import_module
305
+ os.chdir(MAIN_WD)
185
306
 
186
- external_app = import_module(os.getenv("EXTERNAL_APP_MODULE"))
187
- app = external_app.create_wsgi_app(environment, cornflow_db_conn)
188
- with app.app_context():
189
- path = f"{os.path.dirname(external_app.__file__)}/migrations"
190
- migrate = Migrate(app=app, db=db, directory=path)
191
- upgrade()
192
- access_init_command(verbose=False)
193
- if auth == AUTH_DB or auth == AUTH_OID:
194
- # create cornflow admin user
195
- create_user_with_role(
196
- cornflow_admin_user,
197
- cornflow_admin_email,
198
- cornflow_admin_pwd,
199
- "admin",
200
- ADMIN_ROLE,
201
- verbose=True,
202
- )
203
- # create cornflow service user
204
- create_user_with_role(
205
- cornflow_service_user,
206
- cornflow_service_email,
207
- cornflow_service_pwd,
208
- "serviceuser",
209
- SERVICE_ROLE,
210
- verbose=True,
211
- )
212
- register_deployed_dags_command(
213
- airflow_url, airflow_user, airflow_pwd, verbose=True
214
- )
215
- register_dag_permissions_command(open_deployment, verbose=True)
216
- update_schemas_command(airflow_url, airflow_user, airflow_pwd, verbose=True)
217
- update_dag_registry_command(
218
- airflow_url, airflow_user, airflow_pwd, verbose=True
219
- )
307
+ if _register_key():
220
308
 
221
- os.system(
222
- f"/usr/local/bin/gunicorn -c python:cornflow.gunicorn "
223
- f"\"$EXTERNAL_APP_MODULE:create_wsgi_app('$FLASK_ENV')\""
309
+ prefix = "CUSTOM_SSH_"
310
+ env_variables = {
311
+ key: value for key, value in os.environ.items() if key.startswith(prefix)
312
+ }
313
+ for _, value in env_variables.items():
314
+ _register_ssh_host(value)
315
+ else:
316
+ logger.info(
317
+ "************************ NO SSH KEY TO REGISTER ************************"
224
318
  )
225
319
 
320
+ # Install requirements for the external app
321
+ pip_install_cmd = "$(command -v pip) install --user -r requirements.txt"
322
+ click.echo(f"Running: {pip_install_cmd}")
323
+ result = subprocess.run(pip_install_cmd, shell=True, capture_output=True, text=True)
324
+ if result.returncode != 0:
325
+ error(f"Error installing requirements: {result.stderr}")
226
326
  else:
227
- raise Exception("No external application found")
327
+ logger.info(result.stdout)
328
+ time.sleep(5) # Consider if this sleep is truly necessary
329
+ sys.path.append(MAIN_WD)
330
+
331
+ # Add .local path to sys.path so pip --user packages can be found
332
+ local_lib_path = os.path.expanduser("~/.local/lib/python3.12/site-packages")
333
+ if local_lib_path not in sys.path:
334
+ sys.path.insert(0, local_lib_path)
228
335
 
229
336
 
230
- def register_ssh_host(host):
337
+ def _start_application(external_application, environment, external_app_module=None):
338
+ """Starts the Gunicorn server."""
339
+ if external_application == 0:
340
+ os.environ["GUNICORN_WORKING_DIR"] = MAIN_WD
341
+ gunicorn_cmd = (
342
+ "/usr/local/bin/gunicorn -c python:cornflow.gunicorn "
343
+ f"\"cornflow.app:create_app('{environment}')\""
344
+ )
345
+ elif external_application == 1:
346
+ os.environ["GUNICORN_WORKING_DIR"] = MAIN_WD
347
+ if not external_app_module:
348
+ raise ValueError(
349
+ "EXTERNAL_APP_MODULE must be set for external applications"
350
+ )
351
+ gunicorn_cmd = (
352
+ "/usr/local/bin/gunicorn -c python:cornflow.gunicorn "
353
+ f"\"{external_app_module}:create_wsgi_app('{environment}')\""
354
+ )
355
+ else:
356
+ raise ValueError(f"Invalid EXTERNAL_APP value: {external_application}")
357
+
358
+ click.echo(f"Starting application with Gunicorn: {gunicorn_cmd}")
359
+ os.system(gunicorn_cmd)
360
+
361
+
362
+ def _register_ssh_host(host):
231
363
  if host is not None:
232
- add_host = f"ssh-keyscan {host} >> /usr/src/app/.ssh/known_hosts"
233
- config_ssh_host = f"echo Host {host} >> /usr/src/app/.ssh/config"
234
- config_ssh_key = 'echo " IdentityFile /usr/src/app/.ssh/id_rsa" >> /usr/src/app/.ssh/config'
364
+ add_host = f"ssh-keyscan {host} >> {MAIN_WD}/.ssh/known_hosts"
365
+ config_ssh_host = f"echo Host {host} >> {MAIN_WD}/.ssh/config"
366
+ config_ssh_key = (
367
+ 'echo " IdentityFile {MAIN_WD}/.ssh/id_rsa" >> {MAIN_WD}/.ssh/config'
368
+ )
235
369
  os.system(add_host)
236
370
  os.system(config_ssh_host)
237
371
  os.system(config_ssh_key)
238
372
 
239
373
 
240
- def register_key():
241
- if os.path.isfile("/usr/src/app/.ssh/id_rsa"):
242
- add_key = (
243
- "chmod 0600 /usr/src/app/.ssh/id_rsa && ssh-add /usr/src/app/.ssh/id_rsa"
244
- )
374
+ def _register_key():
375
+ if os.path.isfile(f"{MAIN_WD}/.ssh/id_rsa"):
376
+ add_key = f"chmod 0600 {MAIN_WD}/.ssh/id_rsa && ssh-add {MAIN_WD}/.ssh/id_rsa"
245
377
  os.system(add_key)
246
378
  return True
247
379
  else:
248
- return False
380
+ return False