apache-airflow-providers-edge3 1.4.1rc2__py3-none-any.whl → 2.0.0__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.
- airflow/providers/edge3/__init__.py +3 -3
- airflow/providers/edge3/cli/api_client.py +23 -26
- airflow/providers/edge3/cli/worker.py +14 -29
- airflow/providers/edge3/example_dags/integration_test.py +1 -1
- airflow/providers/edge3/example_dags/win_test.py +32 -22
- airflow/providers/edge3/executors/edge_executor.py +7 -63
- airflow/providers/edge3/get_provider_info.py +7 -0
- airflow/providers/edge3/models/edge_worker.py +7 -3
- airflow/providers/edge3/plugins/edge_executor_plugin.py +26 -205
- airflow/providers/edge3/plugins/www/dist/main.umd.cjs +8 -100
- airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +6 -1
- airflow/providers/edge3/plugins/www/openapi-gen/queries/ensureQueryData.ts +6 -1
- airflow/providers/edge3/plugins/www/openapi-gen/queries/prefetch.ts +6 -1
- airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +6 -2
- airflow/providers/edge3/plugins/www/openapi-gen/queries/suspense.ts +6 -1
- airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +5 -0
- airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +18 -3
- airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +24 -0
- airflow/providers/edge3/plugins/www/package.json +26 -24
- airflow/providers/edge3/plugins/www/pnpm-lock.yaml +1469 -1413
- airflow/providers/edge3/plugins/www/src/components/SearchBar.tsx +103 -0
- airflow/providers/edge3/plugins/www/src/components/ui/InputGroup.tsx +57 -0
- airflow/providers/edge3/plugins/www/src/components/ui/Select/Content.tsx +37 -0
- airflow/providers/edge3/plugins/www/src/components/ui/Select/Item.tsx +34 -0
- airflow/providers/edge3/plugins/www/src/components/ui/Select/Root.tsx +24 -0
- airflow/providers/edge3/plugins/www/src/components/ui/Select/Trigger.tsx +54 -0
- airflow/providers/edge3/plugins/www/src/components/ui/Select/ValueText.tsx +51 -0
- airflow/providers/edge3/plugins/www/src/components/ui/Select/index.ts +34 -0
- airflow/providers/edge3/plugins/www/src/components/ui/index.ts +3 -0
- airflow/providers/edge3/plugins/www/src/constants.ts +43 -0
- airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx +184 -95
- airflow/providers/edge3/version_compat.py +0 -2
- airflow/providers/edge3/worker_api/auth.py +11 -35
- airflow/providers/edge3/worker_api/datamodels.py +3 -2
- airflow/providers/edge3/worker_api/routes/health.py +1 -1
- airflow/providers/edge3/worker_api/routes/jobs.py +10 -11
- airflow/providers/edge3/worker_api/routes/logs.py +5 -8
- airflow/providers/edge3/worker_api/routes/ui.py +14 -3
- airflow/providers/edge3/worker_api/routes/worker.py +19 -12
- airflow/providers/edge3/{openapi → worker_api}/v2-edge-generated.yaml +59 -5
- {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/METADATA +16 -14
- {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/RECORD +46 -40
- apache_airflow_providers_edge3-2.0.0.dist-info/licenses/NOTICE +5 -0
- airflow/providers/edge3/openapi/__init__.py +0 -19
- airflow/providers/edge3/openapi/edge_worker_api_v1.yaml +0 -808
- airflow/providers/edge3/worker_api/routes/_v2_compat.py +0 -136
- airflow/providers/edge3/worker_api/routes/_v2_routes.py +0 -237
- {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/entry_points.txt +0 -0
- {airflow/providers/edge3 → apache_airflow_providers_edge3-2.0.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -23,188 +23,35 @@ from typing import TYPE_CHECKING, Any
|
|
|
23
23
|
from airflow.configuration import conf
|
|
24
24
|
from airflow.exceptions import AirflowConfigException
|
|
25
25
|
from airflow.plugins_manager import AirflowPlugin
|
|
26
|
-
from airflow.providers.edge3.version_compat import
|
|
26
|
+
from airflow.providers.edge3.version_compat import AIRFLOW_V_3_1_PLUS
|
|
27
27
|
from airflow.utils.session import NEW_SESSION, provide_session
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from sqlalchemy.orm import Session
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
from airflow.utils.db import DBLocks, create_global_lock
|
|
32
|
+
from airflow.utils.db import DBLocks, create_global_lock
|
|
34
33
|
|
|
35
|
-
@provide_session
|
|
36
|
-
def _get_api_endpoint(session: Session = NEW_SESSION) -> dict[str, Any]:
|
|
37
|
-
# Ensure all required DB modeals are created before starting the API
|
|
38
|
-
with create_global_lock(session=session, lock=DBLocks.MIGRATIONS):
|
|
39
|
-
engine = session.get_bind().engine
|
|
40
|
-
from airflow.providers.edge3.models.edge_job import EdgeJobModel
|
|
41
|
-
from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
|
|
42
|
-
from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
@provide_session
|
|
36
|
+
def _get_api_endpoint(session: Session = NEW_SESSION) -> dict[str, Any]:
|
|
37
|
+
# Ensure all required DB modeals are created before starting the API
|
|
38
|
+
with create_global_lock(session=session, lock=DBLocks.MIGRATIONS):
|
|
39
|
+
engine = session.get_bind().engine
|
|
40
|
+
from airflow.providers.edge3.models.edge_job import EdgeJobModel
|
|
41
|
+
from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
|
|
42
|
+
from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
EdgeJobModel.metadata.create_all(engine)
|
|
45
|
+
EdgeLogsModel.metadata.create_all(engine)
|
|
46
|
+
EdgeWorkerModel.metadata.create_all(engine)
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
"app": create_edge_worker_api_app(),
|
|
52
|
-
"url_prefix": "/edge_worker",
|
|
53
|
-
"name": "Airflow Edge Worker",
|
|
54
|
-
}
|
|
48
|
+
from airflow.providers.edge3.worker_api.app import create_edge_worker_api_app
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
from pathlib import Path
|
|
62
|
-
|
|
63
|
-
from flask import Blueprint, redirect, request, url_for
|
|
64
|
-
from flask_appbuilder import BaseView, expose
|
|
65
|
-
from markupsafe import Markup
|
|
66
|
-
from sqlalchemy import select
|
|
67
|
-
|
|
68
|
-
from airflow.auth.managers.models.resource_details import AccessView
|
|
69
|
-
from airflow.utils.state import State, TaskInstanceState
|
|
70
|
-
from airflow.utils.yaml import safe_load
|
|
71
|
-
from airflow.www.auth import has_access_view
|
|
72
|
-
|
|
73
|
-
def _get_airflow_2_api_endpoint() -> Blueprint:
|
|
74
|
-
from airflow.www.app import csrf
|
|
75
|
-
from airflow.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED
|
|
76
|
-
from airflow.www.extensions.init_views import _CustomErrorRequestBodyValidator, _LazyResolver
|
|
77
|
-
|
|
78
|
-
folder = Path(__file__).parents[1].resolve() # this is airflow/providers/edge3/
|
|
79
|
-
with folder.joinpath("openapi", "edge_worker_api_v1.yaml").open() as f:
|
|
80
|
-
specification = safe_load(f)
|
|
81
|
-
from connexion import FlaskApi
|
|
82
|
-
|
|
83
|
-
bp = FlaskApi(
|
|
84
|
-
specification=specification,
|
|
85
|
-
resolver=_LazyResolver(),
|
|
86
|
-
base_path="/edge_worker/v1",
|
|
87
|
-
strict_validation=True,
|
|
88
|
-
options={"swagger_ui": SWAGGER_ENABLED, "swagger_path": SWAGGER_BUNDLE.__fspath__()},
|
|
89
|
-
validate_responses=True,
|
|
90
|
-
validator_map={"body": _CustomErrorRequestBodyValidator},
|
|
91
|
-
).blueprint
|
|
92
|
-
# Need to exempt CSRF to make API usable
|
|
93
|
-
csrf.exempt(bp)
|
|
94
|
-
return bp
|
|
95
|
-
|
|
96
|
-
def _state_token(state):
|
|
97
|
-
"""Return a formatted string with HTML for a given State."""
|
|
98
|
-
color = State.color(state)
|
|
99
|
-
fg_color = State.color_fg(state)
|
|
100
|
-
return Markup(
|
|
101
|
-
"""
|
|
102
|
-
<span class="label" style="color:{fg_color}; background-color:{color};"
|
|
103
|
-
title="Current State: {state}">{state}</span>
|
|
104
|
-
"""
|
|
105
|
-
).format(color=color, state=state, fg_color=fg_color)
|
|
106
|
-
|
|
107
|
-
def modify_maintenance_comment_on_update(maintenance_comment: str | None, username: str) -> str:
|
|
108
|
-
if maintenance_comment:
|
|
109
|
-
if re.search(
|
|
110
|
-
r"^\[[-\d:\s]+\] - .+ put node into maintenance mode\r?\nComment:.*", maintenance_comment
|
|
111
|
-
):
|
|
112
|
-
return re.sub(
|
|
113
|
-
r"^\[[-\d:\s]+\] - .+ put node into maintenance mode\r?\nComment:",
|
|
114
|
-
f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment:",
|
|
115
|
-
maintenance_comment,
|
|
116
|
-
)
|
|
117
|
-
if re.search(r"^\[[-\d:\s]+\] - .+ updated maintenance mode\r?\nComment:.*", maintenance_comment):
|
|
118
|
-
return re.sub(
|
|
119
|
-
r"^\[[-\d:\s]+\] - .+ updated maintenance mode\r?\nComment:",
|
|
120
|
-
f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment:",
|
|
121
|
-
maintenance_comment,
|
|
122
|
-
)
|
|
123
|
-
return f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment: {maintenance_comment}"
|
|
124
|
-
return (
|
|
125
|
-
f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment:"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
# registers airflow/providers/edge3/plugins/templates as a Jinja template folder
|
|
129
|
-
template_bp = Blueprint(
|
|
130
|
-
"template_blueprint",
|
|
131
|
-
__name__,
|
|
132
|
-
template_folder="templates",
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
class EdgeWorkerJobs(BaseView):
|
|
136
|
-
"""Simple view to show Edge Worker jobs."""
|
|
137
|
-
|
|
138
|
-
default_view = "jobs"
|
|
139
|
-
|
|
140
|
-
@expose("/jobs")
|
|
141
|
-
@has_access_view(AccessView.JOBS)
|
|
142
|
-
@provide_session
|
|
143
|
-
def jobs(self, session: Session = NEW_SESSION):
|
|
144
|
-
from airflow.providers.edge3.models.edge_job import EdgeJobModel
|
|
145
|
-
|
|
146
|
-
jobs = session.scalars(select(EdgeJobModel).order_by(EdgeJobModel.queued_dttm)).all()
|
|
147
|
-
html_states = {
|
|
148
|
-
str(state): _state_token(str(state)) for state in TaskInstanceState.__members__.values()
|
|
149
|
-
}
|
|
150
|
-
return self.render_template("edge_worker_jobs.html", jobs=jobs, html_states=html_states)
|
|
151
|
-
|
|
152
|
-
class EdgeWorkerHosts(BaseView):
|
|
153
|
-
"""Simple view to show Edge Worker status."""
|
|
154
|
-
|
|
155
|
-
default_view = "status"
|
|
156
|
-
|
|
157
|
-
@expose("/status")
|
|
158
|
-
@has_access_view(AccessView.JOBS)
|
|
159
|
-
@provide_session
|
|
160
|
-
def status(self, session: Session = NEW_SESSION):
|
|
161
|
-
from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
|
|
162
|
-
|
|
163
|
-
hosts = session.scalars(select(EdgeWorkerModel).order_by(EdgeWorkerModel.worker_name)).all()
|
|
164
|
-
five_min_ago = datetime.now() - timedelta(minutes=5)
|
|
165
|
-
return self.render_template("edge_worker_hosts.html", hosts=hosts, five_min_ago=five_min_ago)
|
|
166
|
-
|
|
167
|
-
@expose("/status/maintenance/<string:worker_name>/on", methods=["POST"])
|
|
168
|
-
@has_access_view(AccessView.JOBS)
|
|
169
|
-
def worker_to_maintenance(self, worker_name: str):
|
|
170
|
-
from flask_login import current_user
|
|
171
|
-
|
|
172
|
-
from airflow.providers.edge3.models.edge_worker import request_maintenance
|
|
173
|
-
|
|
174
|
-
maintenance_comment = request.form.get("maintenance_comment")
|
|
175
|
-
maintenance_comment = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {current_user.username} put node into maintenance mode\nComment: {maintenance_comment}"
|
|
176
|
-
request_maintenance(worker_name, maintenance_comment)
|
|
177
|
-
return redirect(url_for("EdgeWorkerHosts.status"))
|
|
178
|
-
|
|
179
|
-
@expose("/status/maintenance/<string:worker_name>/off", methods=["POST"])
|
|
180
|
-
@has_access_view(AccessView.JOBS)
|
|
181
|
-
def remove_worker_from_maintenance(self, worker_name: str):
|
|
182
|
-
from airflow.providers.edge3.models.edge_worker import exit_maintenance
|
|
183
|
-
|
|
184
|
-
exit_maintenance(worker_name)
|
|
185
|
-
return redirect(url_for("EdgeWorkerHosts.status"))
|
|
186
|
-
|
|
187
|
-
@expose("/status/maintenance/<string:worker_name>/remove", methods=["POST"])
|
|
188
|
-
@has_access_view(AccessView.JOBS)
|
|
189
|
-
def remove_worker(self, worker_name: str):
|
|
190
|
-
from airflow.providers.edge3.models.edge_worker import remove_worker
|
|
191
|
-
|
|
192
|
-
remove_worker(worker_name)
|
|
193
|
-
return redirect(url_for("EdgeWorkerHosts.status"))
|
|
194
|
-
|
|
195
|
-
@expose("/status/maintenance/<string:worker_name>/change_comment", methods=["POST"])
|
|
196
|
-
@has_access_view(AccessView.JOBS)
|
|
197
|
-
def change_maintenance_comment(self, worker_name: str):
|
|
198
|
-
from flask_login import current_user
|
|
199
|
-
|
|
200
|
-
from airflow.providers.edge3.models.edge_worker import change_maintenance_comment
|
|
201
|
-
|
|
202
|
-
maintenance_comment = request.form.get("maintenance_comment")
|
|
203
|
-
maintenance_comment = modify_maintenance_comment_on_update(
|
|
204
|
-
maintenance_comment, current_user.username
|
|
205
|
-
)
|
|
206
|
-
change_maintenance_comment(worker_name, maintenance_comment)
|
|
207
|
-
return redirect(url_for("EdgeWorkerHosts.status"))
|
|
50
|
+
return {
|
|
51
|
+
"app": create_edge_worker_api_app(),
|
|
52
|
+
"url_prefix": "/edge_worker",
|
|
53
|
+
"name": "Airflow Edge Worker",
|
|
54
|
+
}
|
|
208
55
|
|
|
209
56
|
|
|
210
57
|
# Check if EdgeExecutor is actually loaded
|
|
@@ -213,17 +60,14 @@ try:
|
|
|
213
60
|
except AirflowConfigException:
|
|
214
61
|
EDGE_EXECUTOR_ACTIVE = False
|
|
215
62
|
|
|
216
|
-
# Load the API endpoint only on api-server
|
|
217
|
-
#
|
|
63
|
+
# Load the API endpoint only on api-server
|
|
64
|
+
# TODO(jscheffl): Remove this check when the discussion in
|
|
218
65
|
# https://lists.apache.org/thread/w170czq6r7bslkqp1tk6bjjjo0789wgl
|
|
219
66
|
# resulted in a proper API to selective initialize. Maybe backcompat-shim
|
|
220
67
|
# is also needed to support Airflow-versions prior the rework.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
225
|
-
else:
|
|
226
|
-
RUNNING_ON_APISERVER = "gunicorn" in sys.argv[0] and "airflow-webserver" in sys.argv
|
|
68
|
+
RUNNING_ON_APISERVER = (len(sys.argv) > 1 and sys.argv[1] in ["api-server"]) or (
|
|
69
|
+
len(sys.argv) > 2 and sys.argv[2] == "airflow-core/src/airflow/api_fastapi/main.py"
|
|
70
|
+
)
|
|
227
71
|
|
|
228
72
|
|
|
229
73
|
def _get_base_url_path(path: str) -> str:
|
|
@@ -247,8 +91,9 @@ class EdgeExecutorPlugin(AirflowPlugin):
|
|
|
247
91
|
|
|
248
92
|
name = "edge_executor"
|
|
249
93
|
if EDGE_EXECUTOR_ACTIVE and RUNNING_ON_APISERVER:
|
|
94
|
+
fastapi_apps = [_get_api_endpoint()]
|
|
250
95
|
if AIRFLOW_V_3_1_PLUS:
|
|
251
|
-
|
|
96
|
+
# Airflow 3.0 does not know about react_apps, so we only provide the API endpoint
|
|
252
97
|
react_apps = [
|
|
253
98
|
{
|
|
254
99
|
"name": "Edge Executor",
|
|
@@ -271,27 +116,3 @@ class EdgeExecutorPlugin(AirflowPlugin):
|
|
|
271
116
|
"url_route": "edge_worker_api_docs",
|
|
272
117
|
}
|
|
273
118
|
]
|
|
274
|
-
if AIRFLOW_V_3_0_PLUS:
|
|
275
|
-
# Airflow 3.0 does not know about react_apps, so we only provide the API endpoint
|
|
276
|
-
fastapi_apps = [_get_api_endpoint()]
|
|
277
|
-
else:
|
|
278
|
-
appbuilder_menu_items = [
|
|
279
|
-
{
|
|
280
|
-
"name": "Edge Worker API docs",
|
|
281
|
-
"href": _get_base_url_path("/edge_worker/v1/ui"),
|
|
282
|
-
"category": "Docs",
|
|
283
|
-
}
|
|
284
|
-
]
|
|
285
|
-
appbuilder_views = [
|
|
286
|
-
{
|
|
287
|
-
"name": "Edge Worker Jobs",
|
|
288
|
-
"category": "Admin",
|
|
289
|
-
"view": EdgeWorkerJobs(),
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
"name": "Edge Worker Hosts",
|
|
293
|
-
"category": "Admin",
|
|
294
|
-
"view": EdgeWorkerHosts(),
|
|
295
|
-
},
|
|
296
|
-
]
|
|
297
|
-
flask_blueprints = [_get_airflow_2_api_endpoint(), template_bp]
|