apache-airflow-providers-edge3 1.0.0rc2__py3-none-any.whl → 1.1.0rc1__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.
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "1.0.0"
32
+ __version__ = "1.1.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.10.0"
@@ -82,10 +82,9 @@ def _make_generic_request(method: str, rest_path: str, data: str | None = None)
82
82
 
83
83
  @cache
84
84
  def jwt_generator() -> JWTGenerator:
85
- clock_grace = conf.getint("core", "internal_api_clock_grace", fallback=30)
86
85
  return JWTGenerator(
87
- secret_key=conf.get("core", "internal_api_secret_key"),
88
- valid_for=clock_grace,
86
+ secret_key=conf.get("api_auth", "jwt_secret"),
87
+ valid_for=conf.getint("api_auth", "jwt_leeway", fallback=30),
89
88
  audience="api",
90
89
  )
91
90
 
@@ -23,6 +23,7 @@ import signal
23
23
  import sys
24
24
  from dataclasses import asdict
25
25
  from datetime import datetime
26
+ from getpass import getuser
26
27
  from http import HTTPStatus
27
28
  from multiprocessing import Process
28
29
  from pathlib import Path
@@ -36,6 +37,8 @@ from requests import HTTPError
36
37
 
37
38
  from airflow import __version__ as airflow_version, settings
38
39
  from airflow.cli.cli_config import ARG_PID, ARG_VERBOSE, ActionCommand, Arg
40
+ from airflow.cli.commands.daemon_utils import run_command_with_daemon_option
41
+ from airflow.cli.simple_table import AirflowConsole
39
42
  from airflow.configuration import conf
40
43
  from airflow.providers.edge3 import __version__ as edge_provider_version
41
44
  from airflow.providers.edge3.cli.api_client import (
@@ -164,7 +167,6 @@ class _EdgeWorkerCli:
164
167
  """Flag if job processing should be completed and no new jobs fetched for maintenance mode. """
165
168
  maintenance_comments: str | None = None
166
169
  """Comments for maintenance mode."""
167
-
168
170
  edge_instance: _EdgeWorkerCli | None = None
169
171
  """Singleton instance of the worker."""
170
172
 
@@ -176,6 +178,7 @@ class _EdgeWorkerCli:
176
178
  concurrency: int,
177
179
  job_poll_interval: int,
178
180
  heartbeat_interval: int,
181
+ daemon: bool = False,
179
182
  ):
180
183
  self.pid_file_path = pid_file_path
181
184
  self.job_poll_interval = job_poll_interval
@@ -184,6 +187,7 @@ class _EdgeWorkerCli:
184
187
  self.queues = queues
185
188
  self.concurrency = concurrency
186
189
  self.free_concurrency = concurrency
190
+ self.daemon = daemon
187
191
 
188
192
  _EdgeWorkerCli.edge_instance = self
189
193
 
@@ -271,6 +275,12 @@ class _EdgeWorkerCli:
271
275
  setproctitle(f"airflow edge worker: {workload.ti.key}")
272
276
 
273
277
  try:
278
+ base_url = conf.get("api", "base_url", fallback="/")
279
+ # If it's a relative URL, use localhost:8080 as the default
280
+ if base_url.startswith("/"):
281
+ base_url = f"http://localhost:8080{base_url}"
282
+ default_execution_api_server = f"{base_url.rstrip('/')}/execution/"
283
+
274
284
  supervise(
275
285
  # This is the "wrong" ti type, but it duck types the same. TODO: Create a protocol for this.
276
286
  # Same like in airflow/executors/local_executor.py:_execute_work()
@@ -278,7 +288,9 @@ class _EdgeWorkerCli:
278
288
  dag_rel_path=workload.dag_rel_path,
279
289
  bundle_info=workload.bundle_info,
280
290
  token=workload.token,
281
- server=conf.get("core", "execution_api_server_url"),
291
+ server=conf.get(
292
+ "core", "execution_api_server_url", fallback=default_execution_api_server
293
+ ),
282
294
  log_path=workload.log_path,
283
295
  )
284
296
  return 0
@@ -332,7 +344,8 @@ class _EdgeWorkerCli:
332
344
  if e.response.status_code == HTTPStatus.NOT_FOUND:
333
345
  raise SystemExit("Error: API endpoint is not ready, please set [edge] api_enabled=True.")
334
346
  raise SystemExit(str(e))
335
- _write_pid_to_pidfile(self.pid_file_path)
347
+ if not self.daemon:
348
+ _write_pid_to_pidfile(self.pid_file_path)
336
349
  signal.signal(signal.SIGINT, _EdgeWorkerCli.signal_handler)
337
350
  signal.signal(SIG_STATUS, _EdgeWorkerCli.signal_handler)
338
351
  signal.signal(signal.SIGTERM, self.shutdown_handler)
@@ -358,7 +371,8 @@ class _EdgeWorkerCli:
358
371
  except EdgeWorkerVersionException:
359
372
  logger.info("Version mismatch of Edge worker and Core. Quitting worker anyway.")
360
373
  finally:
361
- remove_existing_pidfile(self.pid_file_path)
374
+ if not self.daemon:
375
+ remove_existing_pidfile(self.pid_file_path)
362
376
 
363
377
  def loop(self):
364
378
  """Run a loop of scheduling and monitoring tasks."""
@@ -463,6 +477,9 @@ class _EdgeWorkerCli:
463
477
  _EdgeWorkerCli.maintenance_comments = worker_info.maintenance_comments
464
478
  else:
465
479
  _EdgeWorkerCli.maintenance_comments = None
480
+ if worker_info.state == EdgeWorkerState.SHUTDOWN_REQUEST:
481
+ logger.info("Shutdown requested!")
482
+ _EdgeWorkerCli.drain = True
466
483
 
467
484
  worker_state_changed = worker_info.state != state
468
485
  except EdgeWorkerVersionException:
@@ -479,10 +496,8 @@ class _EdgeWorkerCli:
479
496
  return
480
497
 
481
498
 
482
- @cli_utils.action_cli(check_db=False)
483
499
  @providers_configuration_loaded
484
- def worker(args):
485
- """Start Airflow Edge Worker."""
500
+ def _launch_worker(args):
486
501
  print(settings.HEADER)
487
502
  print(EDGE_WORKER_HEADER)
488
503
 
@@ -493,14 +508,31 @@ def worker(args):
493
508
  concurrency=args.concurrency,
494
509
  job_poll_interval=conf.getint("edge", "job_poll_interval"),
495
510
  heartbeat_interval=conf.getint("edge", "heartbeat_interval"),
511
+ daemon=args.daemon,
496
512
  )
497
513
  edge_worker.start()
498
514
 
499
515
 
516
+ @cli_utils.action_cli(check_db=False)
517
+ @providers_configuration_loaded
518
+ def worker(args):
519
+ """Start Airflow Edge Worker."""
520
+ umask = args.umask or conf.get("edge", "worker_umask", fallback=settings.DAEMON_UMASK)
521
+
522
+ run_command_with_daemon_option(
523
+ args=args,
524
+ process_name=EDGE_WORKER_PROCESS_NAME,
525
+ callback=lambda: _launch_worker(args),
526
+ should_setup_logging=True,
527
+ pid_file=_pid_file_path(args.pid),
528
+ umask=umask,
529
+ )
530
+
531
+
500
532
  @cli_utils.action_cli(check_db=False)
501
533
  @providers_configuration_loaded
502
534
  def status(args):
503
- """Check for Airflow Edge Worker status."""
535
+ """Check for Airflow Local Edge Worker status."""
504
536
  pid = _get_pid(args.pid)
505
537
 
506
538
  # Send Signal as notification to drop status JSON
@@ -526,7 +558,7 @@ def status(args):
526
558
  @cli_utils.action_cli(check_db=False)
527
559
  @providers_configuration_loaded
528
560
  def maintenance(args):
529
- """Set or Unset maintenance mode of worker."""
561
+ """Set or Unset maintenance mode of local edge worker."""
530
562
  if args.maintenance == "on" and not args.comments:
531
563
  logger.error("Comments are required when setting maintenance mode.")
532
564
  sys.exit(4)
@@ -597,7 +629,7 @@ def maintenance(args):
597
629
  @cli_utils.action_cli(check_db=False)
598
630
  @providers_configuration_loaded
599
631
  def stop(args):
600
- """Stop a running Airflow Edge Worker."""
632
+ """Stop a running local Airflow Edge Worker."""
601
633
  pid = _get_pid(args.pid)
602
634
  # Send SIGINT
603
635
  logger.info("Sending SIGINT to worker pid %i.", pid)
@@ -611,6 +643,109 @@ def stop(args):
611
643
  logger.info("Worker has been shut down.")
612
644
 
613
645
 
646
+ def _check_valid_db_connection():
647
+ """Check for a valid db connection before executing db dependent cli commands."""
648
+ db_conn = conf.get("database", "sql_alchemy_conn")
649
+ db_default = conf.get_default_value("database", "sql_alchemy_conn")
650
+ if db_conn == db_default:
651
+ raise SystemExit(
652
+ "Error: The database connection is not set. Please set the connection in the configuration file."
653
+ )
654
+
655
+
656
+ def _check_if_registered_edge_host(hostname: str):
657
+ """Check if edge worker is registered with the db before executing dependent cli commands."""
658
+ from airflow.providers.edge3.models.edge_worker import _fetch_edge_hosts_from_db
659
+
660
+ if not _fetch_edge_hosts_from_db(hostname=hostname):
661
+ raise SystemExit(f"Error: Edge Worker {hostname} is unknown!")
662
+
663
+
664
+ @cli_utils.action_cli(check_db=False)
665
+ @providers_configuration_loaded
666
+ def list_edge_workers(args) -> None:
667
+ """Query the db to list all registered edge workers."""
668
+ _check_valid_db_connection()
669
+ from airflow.providers.edge3.models.edge_worker import get_registered_edge_hosts
670
+
671
+ all_hosts_iter = get_registered_edge_hosts(states=args.state)
672
+ # Format and print worker info on the screen
673
+ fields = [
674
+ "worker_name",
675
+ "state",
676
+ "queues",
677
+ "maintenance_comment",
678
+ ]
679
+ all_hosts = [{f: host.__getattribute__(f) for f in fields} for host in all_hosts_iter]
680
+ AirflowConsole().print_as(data=all_hosts, output=args.output)
681
+
682
+
683
+ @cli_utils.action_cli(check_db=False)
684
+ @providers_configuration_loaded
685
+ def put_remote_worker_on_maintenance(args) -> None:
686
+ """Put remote edge worker on maintenance."""
687
+ _check_valid_db_connection()
688
+ _check_if_registered_edge_host(hostname=args.edge_hostname)
689
+ from airflow.providers.edge3.models.edge_worker import request_maintenance
690
+
691
+ request_maintenance(args.edge_hostname, args.comments)
692
+ logger.info("%s has been put on maintenance by %s.", args.edge_hostname, getuser())
693
+
694
+
695
+ @cli_utils.action_cli(check_db=False)
696
+ @providers_configuration_loaded
697
+ def remove_remote_worker_from_maintenance(args) -> None:
698
+ """Remove remote edge worker from maintenance."""
699
+ _check_valid_db_connection()
700
+ _check_if_registered_edge_host(hostname=args.edge_hostname)
701
+ from airflow.providers.edge3.models.edge_worker import exit_maintenance
702
+
703
+ exit_maintenance(args.edge_hostname)
704
+ logger.info("%s has been removed from maintenance by %s.", args.edge_hostname, getuser())
705
+
706
+
707
+ @cli_utils.action_cli(check_db=False)
708
+ @providers_configuration_loaded
709
+ def remote_worker_update_maintenance_comment(args) -> None:
710
+ """Update maintenance comments of the remote edge worker."""
711
+ _check_valid_db_connection()
712
+ _check_if_registered_edge_host(hostname=args.edge_hostname)
713
+ from airflow.providers.edge3.models.edge_worker import change_maintenance_comment
714
+
715
+ try:
716
+ change_maintenance_comment(args.edge_hostname, args.comments)
717
+ logger.info("Maintenance comments updated for %s by %s.", args.edge_hostname, getuser())
718
+ except TypeError:
719
+ raise SystemExit
720
+
721
+
722
+ @cli_utils.action_cli(check_db=False)
723
+ @providers_configuration_loaded
724
+ def remove_remote_worker(args) -> None:
725
+ """Remove remote edge worker entry from db."""
726
+ _check_valid_db_connection()
727
+ _check_if_registered_edge_host(hostname=args.edge_hostname)
728
+ from airflow.providers.edge3.models.edge_worker import remove_worker
729
+
730
+ try:
731
+ remove_worker(args.edge_hostname)
732
+ logger.info("Edge Worker host %s removed by %s.", args.edge_hostname, getuser())
733
+ except TypeError:
734
+ raise SystemExit
735
+
736
+
737
+ @cli_utils.action_cli(check_db=False)
738
+ @providers_configuration_loaded
739
+ def remote_worker_request_shutdown(args) -> None:
740
+ """Initiate the shutdown of the remote edge worker."""
741
+ _check_valid_db_connection()
742
+ _check_if_registered_edge_host(hostname=args.edge_hostname)
743
+ from airflow.providers.edge3.models.edge_worker import request_shutdown
744
+
745
+ request_shutdown(args.edge_hostname)
746
+ logger.info("Requested shutdown of Edge Worker host %s by %s.", args.edge_hostname, getuser())
747
+
748
+
614
749
  ARG_CONCURRENCY = Arg(
615
750
  ("-c", "--concurrency"),
616
751
  type=int,
@@ -625,11 +760,21 @@ ARG_EDGE_HOSTNAME = Arg(
625
760
  ("-H", "--edge-hostname"),
626
761
  help="Set the hostname of worker if you have multiple workers on a single machine",
627
762
  )
763
+ ARG_REQUIRED_EDGE_HOSTNAME = Arg(
764
+ ("-H", "--edge-hostname"),
765
+ help="Set the hostname of worker if you have multiple workers on a single machine",
766
+ required=True,
767
+ )
628
768
  ARG_MAINTENANCE = Arg(("maintenance",), help="Desired maintenance state", choices=("on", "off"))
629
769
  ARG_MAINTENANCE_COMMENT = Arg(
630
770
  ("-c", "--comments"),
631
771
  help="Maintenance comments to report reason. Required if maintenance is turned on.",
632
772
  )
773
+ ARG_REQUIRED_MAINTENANCE_COMMENT = Arg(
774
+ ("-c", "--comments"),
775
+ help="Maintenance comments to report reason. Required if enabling maintenance",
776
+ required=True,
777
+ )
633
778
  ARG_WAIT_MAINT = Arg(
634
779
  ("-w", "--wait"),
635
780
  default=False,
@@ -642,6 +787,36 @@ ARG_WAIT_STOP = Arg(
642
787
  help="Wait until edge worker is shut down.",
643
788
  action="store_true",
644
789
  )
790
+ ARG_OUTPUT = Arg(
791
+ (
792
+ "-o",
793
+ "--output",
794
+ ),
795
+ help="Output format. Allowed values: json, yaml, plain, table (default: table)",
796
+ metavar="(table, json, yaml, plain)",
797
+ choices=("table", "json", "yaml", "plain"),
798
+ default="table",
799
+ )
800
+ ARG_STATE = Arg(
801
+ (
802
+ "-s",
803
+ "--state",
804
+ ),
805
+ nargs="+",
806
+ help="State of the edge worker",
807
+ )
808
+
809
+ ARG_DAEMON = Arg(
810
+ ("-D", "--daemon"), help="Daemonize instead of running in the foreground", action="store_true"
811
+ )
812
+ ARG_UMASK = Arg(
813
+ ("-u", "--umask"),
814
+ help="Set the umask of edge worker in daemon mode",
815
+ )
816
+ ARG_STDERR = Arg(("--stderr",), help="Redirect stderr to this file if run in daemon mode")
817
+ ARG_STDOUT = Arg(("--stdout",), help="Redirect stdout to this file if run in daemon mode")
818
+ ARG_LOG_FILE = Arg(("-l", "--log-file"), help="Location of the log file if run in daemon mode")
819
+
645
820
  EDGE_COMMANDS: list[ActionCommand] = [
646
821
  ActionCommand(
647
822
  name=worker.__name__,
@@ -653,6 +828,11 @@ EDGE_COMMANDS: list[ActionCommand] = [
653
828
  ARG_EDGE_HOSTNAME,
654
829
  ARG_PID,
655
830
  ARG_VERBOSE,
831
+ ARG_DAEMON,
832
+ ARG_STDOUT,
833
+ ARG_STDERR,
834
+ ARG_LOG_FILE,
835
+ ARG_UMASK,
656
836
  ),
657
837
  ),
658
838
  ActionCommand(
@@ -686,4 +866,49 @@ EDGE_COMMANDS: list[ActionCommand] = [
686
866
  ARG_VERBOSE,
687
867
  ),
688
868
  ),
869
+ ActionCommand(
870
+ name="list-workers",
871
+ help=list_edge_workers.__doc__,
872
+ func=list_edge_workers,
873
+ args=(
874
+ ARG_OUTPUT,
875
+ ARG_STATE,
876
+ ),
877
+ ),
878
+ ActionCommand(
879
+ name="remote-edge-worker-request-maintenance",
880
+ help=put_remote_worker_on_maintenance.__doc__,
881
+ func=put_remote_worker_on_maintenance,
882
+ args=(
883
+ ARG_REQUIRED_EDGE_HOSTNAME,
884
+ ARG_REQUIRED_MAINTENANCE_COMMENT,
885
+ ),
886
+ ),
887
+ ActionCommand(
888
+ name="remote-edge-worker-exit-maintenance",
889
+ help=remove_remote_worker_from_maintenance.__doc__,
890
+ func=remove_remote_worker_from_maintenance,
891
+ args=(ARG_REQUIRED_EDGE_HOSTNAME,),
892
+ ),
893
+ ActionCommand(
894
+ name="remote-edge-worker-update-maintenance-comment",
895
+ help=remote_worker_update_maintenance_comment.__doc__,
896
+ func=remote_worker_update_maintenance_comment,
897
+ args=(
898
+ ARG_REQUIRED_EDGE_HOSTNAME,
899
+ ARG_REQUIRED_MAINTENANCE_COMMENT,
900
+ ),
901
+ ),
902
+ ActionCommand(
903
+ name="remove-remote-edge-worker",
904
+ help=remove_remote_worker.__doc__,
905
+ func=remove_remote_worker,
906
+ args=(ARG_REQUIRED_EDGE_HOSTNAME,),
907
+ ),
908
+ ActionCommand(
909
+ name="shutdown-remote-edge-worker",
910
+ help=remote_worker_request_shutdown.__doc__,
911
+ func=remote_worker_request_shutdown,
912
+ args=(ARG_REQUIRED_EDGE_HOSTNAME,),
913
+ ),
689
914
  ]
@@ -26,20 +26,24 @@ from __future__ import annotations
26
26
  from datetime import datetime
27
27
  from time import sleep
28
28
 
29
- from airflow.decorators import task, task_group
30
29
  from airflow.exceptions import AirflowNotFoundException
31
30
  from airflow.hooks.base import BaseHook
32
- from airflow.models.dag import DAG
33
- from airflow.models.variable import Variable
34
- from airflow.providers.common.compat.standard.operators import PythonOperator
35
- from airflow.providers.standard.operators.empty import EmptyOperator
36
- from airflow.sdk import Param
37
31
  from airflow.utils.trigger_rule import TriggerRule
38
32
 
39
33
  try:
40
34
  from airflow.providers.standard.operators.bash import BashOperator
35
+ from airflow.providers.standard.operators.empty import EmptyOperator
36
+ from airflow.providers.standard.operators.python import PythonOperator
37
+ from airflow.sdk import DAG, Param, Variable, task, task_group
41
38
  except ImportError:
39
+ # Airflow 2.10 compat
40
+ from airflow.decorators import task, task_group # type: ignore[no-redef,attr-defined]
41
+ from airflow.models.dag import DAG # type: ignore[no-redef,attr-defined,assignment]
42
+ from airflow.models.param import Param # type: ignore[no-redef,attr-defined]
43
+ from airflow.models.variable import Variable # type: ignore[no-redef,attr-defined]
42
44
  from airflow.operators.bash import BashOperator # type: ignore[no-redef,attr-defined]
45
+ from airflow.operators.empty import EmptyOperator # type: ignore[no-redef,attr-defined]
46
+ from airflow.operators.python import PythonOperator # type: ignore[no-redef,attr-defined]
43
47
 
44
48
  with DAG(
45
49
  dag_id="integration_test",
@@ -53,9 +57,18 @@ with DAG(
53
57
  "mapping_count": Param(
54
58
  4,
55
59
  type="integer",
60
+ minimum=1,
61
+ maximum=1024,
56
62
  title="Mapping Count",
57
63
  description="Amount of tasks that should be mapped",
58
64
  ),
65
+ "minutes_to_run": Param(
66
+ 15,
67
+ type="integer",
68
+ minimum=1,
69
+ title="Minutes to run",
70
+ description="Duration in minutes the long running task should run",
71
+ ),
59
72
  },
60
73
  ) as dag:
61
74
 
@@ -146,9 +159,10 @@ with DAG(
146
159
  [plan_to_fail(), needs_retry()] >> capture_fail()
147
160
 
148
161
  @task
149
- def long_running():
150
- print("This task runs for 15 minutes")
151
- for i in range(15):
162
+ def long_running(**context):
163
+ minutes_to_run: int = context["params"]["minutes_to_run"]
164
+ print(f"This task runs for {minutes_to_run} minute{'s' if minutes_to_run > 1 else ''}.")
165
+ for i in range(minutes_to_run):
152
166
  sleep(60)
153
167
  print(f"Running for {i + 1} minutes now.")
154
168
  print("Long running task completed.")
@@ -177,7 +177,11 @@ class EdgeExecutor(BaseExecutor):
177
177
  .with_for_update(skip_locked=True)
178
178
  .filter(
179
179
  EdgeWorkerModel.state.not_in(
180
- [EdgeWorkerState.UNKNOWN, EdgeWorkerState.OFFLINE, EdgeWorkerState.OFFLINE_MAINTENANCE]
180
+ [
181
+ EdgeWorkerState.UNKNOWN,
182
+ EdgeWorkerState.OFFLINE,
183
+ EdgeWorkerState.OFFLINE_MAINTENANCE,
184
+ ]
181
185
  ),
182
186
  EdgeWorkerModel.last_update < (timezone.utcnow() - timedelta(seconds=heartbeat_interval * 5)),
183
187
  )
@@ -186,7 +190,17 @@ class EdgeExecutor(BaseExecutor):
186
190
 
187
191
  for worker in lifeless_workers:
188
192
  changed = True
189
- worker.state = EdgeWorkerState.UNKNOWN
193
+ # If the worker dies in maintenance mode we want to remember it, so it can start in maintenance mode
194
+ worker.state = (
195
+ EdgeWorkerState.OFFLINE_MAINTENANCE
196
+ if worker.state
197
+ in (
198
+ EdgeWorkerState.MAINTENANCE_MODE,
199
+ EdgeWorkerState.MAINTENANCE_PENDING,
200
+ EdgeWorkerState.MAINTENANCE_REQUEST,
201
+ )
202
+ else EdgeWorkerState.UNKNOWN
203
+ )
190
204
  reset_metrics(worker.worker_name)
191
205
 
192
206
  return changed
@@ -25,7 +25,7 @@ def get_provider_info():
25
25
  return {
26
26
  "package-name": "apache-airflow-providers-edge3",
27
27
  "name": "Edge Executor",
28
- "description": "Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites\n",
28
+ "description": "Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites.\n\nWhen tasks need to be executed on remote sites where the connection need to pass through\nfirewalls or other network restrictions, the Edge Worker can be deployed. The Edge Worker\nis a lightweight process with reduced dependencies. The worker only needs to be able to\ncommunicate with the central Airflow site via HTTPS.\n\nIn the central Airflow site the EdgeExecutor is used to orchestrate the work. The EdgeExecutor\nis a custom executor which is used to schedule tasks on the edge workers. The EdgeExecutor can co-exist\nwith other executors (for example CeleryExecutor or KubernetesExecutor) in the same Airflow site.\n\nAdditional REST API endpoints are provided to distribute tasks and manage the edge workers. The endpoints\nare provided by the API server.\n",
29
29
  "plugins": [
30
30
  {
31
31
  "name": "edge_executor",
@@ -93,6 +93,13 @@ def get_provider_info():
93
93
  "example": None,
94
94
  "default": "524288",
95
95
  },
96
+ "worker_umask": {
97
+ "description": "The default umask to use for edge worker when run in daemon mode\n\nThis controls the file-creation mode mask which determines the initial value of file permission bits\nfor newly created files.\n\nThis value is treated as an octal-integer.\n",
98
+ "version_added": None,
99
+ "type": "string",
100
+ "default": None,
101
+ "example": None,
102
+ },
96
103
  },
97
104
  }
98
105
  },
@@ -18,6 +18,7 @@ from __future__ import annotations
18
18
 
19
19
  import ast
20
20
  import json
21
+ import logging
21
22
  from datetime import datetime
22
23
  from enum import Enum
23
24
  from typing import TYPE_CHECKING
@@ -29,12 +30,15 @@ from airflow.models.base import Base
29
30
  from airflow.stats import Stats
30
31
  from airflow.utils import timezone
31
32
  from airflow.utils.log.logging_mixin import LoggingMixin
33
+ from airflow.utils.providers_configuration_loader import providers_configuration_loaded
32
34
  from airflow.utils.session import NEW_SESSION, provide_session
33
35
  from airflow.utils.sqlalchemy import UtcDateTime
34
36
 
35
37
  if TYPE_CHECKING:
36
38
  from sqlalchemy.orm import Session
37
39
 
40
+ logger = logging.getLogger(__name__)
41
+
38
42
 
39
43
  class EdgeWorkerVersionException(AirflowException):
40
44
  """Signal a version mismatch between core and Edge Site."""
@@ -51,6 +55,8 @@ class EdgeWorkerState(str, Enum):
51
55
  """Edge Worker is actively running a task."""
52
56
  IDLE = "idle"
53
57
  """Edge Worker is active and waiting for a task."""
58
+ SHUTDOWN_REQUEST = "shutdown request"
59
+ """Request to shutdown Edge Worker."""
54
60
  TERMINATING = "terminating"
55
61
  """Edge Worker is completing work and stopping."""
56
62
  OFFLINE = "offline"
@@ -194,6 +200,26 @@ def reset_metrics(worker_name: str) -> None:
194
200
  )
195
201
 
196
202
 
203
+ @providers_configuration_loaded
204
+ @provide_session
205
+ def _fetch_edge_hosts_from_db(
206
+ hostname: str | None = None, states: list | None = None, session: Session = NEW_SESSION
207
+ ) -> list:
208
+ query = select(EdgeWorkerModel)
209
+ if states:
210
+ query = query.where(EdgeWorkerModel.state.in_(states))
211
+ if hostname:
212
+ query = query.where(EdgeWorkerModel.worker_name == hostname)
213
+ query = query.order_by(EdgeWorkerModel.worker_name)
214
+ return session.scalars(query).all()
215
+
216
+
217
+ @providers_configuration_loaded
218
+ @provide_session
219
+ def get_registered_edge_hosts(states: list | None = None, session: Session = NEW_SESSION):
220
+ return _fetch_edge_hosts_from_db(states=states, session=session)
221
+
222
+
197
223
  @provide_session
198
224
  def request_maintenance(
199
225
  worker_name: str, maintenance_comment: str | None, session: Session = NEW_SESSION
@@ -217,7 +243,14 @@ def exit_maintenance(worker_name: str, session: Session = NEW_SESSION) -> None:
217
243
  @provide_session
218
244
  def remove_worker(worker_name: str, session: Session = NEW_SESSION) -> None:
219
245
  """Remove a worker that is offline or just gone from DB."""
220
- session.execute(delete(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name))
246
+ query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name)
247
+ worker: EdgeWorkerModel = session.scalar(query)
248
+ if worker.state in (EdgeWorkerState.OFFLINE, EdgeWorkerState.OFFLINE_MAINTENANCE):
249
+ session.execute(delete(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name))
250
+ else:
251
+ error_message = f"Cannot remove edge worker {worker_name} as it is in {worker.state} state!"
252
+ logger.error(error_message)
253
+ raise TypeError(error_message)
221
254
 
222
255
 
223
256
  @provide_session
@@ -227,4 +260,22 @@ def change_maintenance_comment(
227
260
  """Write maintenance comment in the db."""
228
261
  query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name)
229
262
  worker: EdgeWorkerModel = session.scalar(query)
230
- worker.maintenance_comment = maintenance_comment
263
+ if worker.state in (EdgeWorkerState.MAINTENANCE_MODE, EdgeWorkerState.OFFLINE_MAINTENANCE):
264
+ worker.maintenance_comment = maintenance_comment
265
+ else:
266
+ error_message = f"Cannot change maintenance comment as {worker_name} is not in maintenance!"
267
+ logger.error(error_message)
268
+ raise TypeError(error_message)
269
+
270
+
271
+ @provide_session
272
+ def request_shutdown(worker_name: str, session: Session = NEW_SESSION) -> None:
273
+ """Request to shutdown the edge worker."""
274
+ query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name)
275
+ worker: EdgeWorkerModel = session.scalar(query)
276
+ if worker.state not in (
277
+ EdgeWorkerState.OFFLINE,
278
+ EdgeWorkerState.OFFLINE_MAINTENANCE,
279
+ EdgeWorkerState.UNKNOWN,
280
+ ):
281
+ worker.state = EdgeWorkerState.SHUTDOWN_REQUEST
@@ -32,5 +32,4 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
32
32
  return airflow_version.major, airflow_version.minor, airflow_version.micro
33
33
 
34
34
 
35
- AIRFLOW_V_2_10_PLUS = get_base_airflow_version_tuple() >= (2, 10, 0)
36
35
  AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
@@ -47,10 +47,9 @@ if AIRFLOW_V_3_0_PLUS:
47
47
 
48
48
  @cache
49
49
  def jwt_validator() -> JWTValidator:
50
- clock_grace = conf.getint("core", "internal_api_clock_grace", fallback=30)
51
50
  return JWTValidator(
52
- secret_key=conf.get("core", "internal_api_secret_key"),
53
- leeway=clock_grace,
51
+ secret_key=conf.get("api_auth", "jwt_secret"),
52
+ leeway=conf.getint("api_auth", "jwt_leeway", fallback=30),
54
53
  audience="api",
55
54
  )
56
55
 
@@ -114,7 +114,7 @@ _worker_queue_doc = Body(
114
114
 
115
115
 
116
116
  def redefine_state(worker_state: EdgeWorkerState, body_state: EdgeWorkerState) -> EdgeWorkerState:
117
- """Redefine the state of the worker based on maintenance request."""
117
+ """Redefine the state of the worker based on maintenance or shutdown request."""
118
118
  if (
119
119
  worker_state == EdgeWorkerState.MAINTENANCE_REQUEST
120
120
  and body_state
@@ -122,7 +122,12 @@ def redefine_state(worker_state: EdgeWorkerState, body_state: EdgeWorkerState) -
122
122
  EdgeWorkerState.MAINTENANCE_PENDING,
123
123
  EdgeWorkerState.MAINTENANCE_MODE,
124
124
  )
125
- or worker_state == EdgeWorkerState.OFFLINE_MAINTENANCE
125
+ or worker_state
126
+ in (
127
+ EdgeWorkerState.OFFLINE_MAINTENANCE,
128
+ EdgeWorkerState.MAINTENANCE_MODE,
129
+ EdgeWorkerState.MAINTENANCE_PENDING,
130
+ )
126
131
  and body_state == EdgeWorkerState.STARTING
127
132
  ):
128
133
  return EdgeWorkerState.MAINTENANCE_REQUEST
@@ -133,6 +138,14 @@ def redefine_state(worker_state: EdgeWorkerState, body_state: EdgeWorkerState) -
133
138
  if body_state == EdgeWorkerState.MAINTENANCE_MODE:
134
139
  return EdgeWorkerState.IDLE
135
140
 
141
+ if worker_state == EdgeWorkerState.SHUTDOWN_REQUEST:
142
+ if body_state not in (
143
+ EdgeWorkerState.OFFLINE_MAINTENANCE,
144
+ EdgeWorkerState.OFFLINE,
145
+ EdgeWorkerState.UNKNOWN,
146
+ ):
147
+ return EdgeWorkerState.SHUTDOWN_REQUEST
148
+
136
149
  return body_state
137
150
 
138
151
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apache-airflow-providers-edge3
3
- Version: 1.0.0rc2
3
+ Version: 1.1.0rc1
4
4
  Summary: Provider package apache-airflow-providers-edge3 for Apache Airflow
5
5
  Keywords: airflow-provider,edge3,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -20,18 +20,17 @@ Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Topic :: System :: Monitoring
23
- Requires-Dist: apache-airflow>=2.10.0rc0
23
+ Requires-Dist: apache-airflow>=2.10.0rc1
24
+ Requires-Dist: apache-airflow-providers-fab>=1.5.3rc1
24
25
  Requires-Dist: pydantic>=2.11.0
25
26
  Requires-Dist: retryhttp>=1.2.0,!=1.3.0
26
- Requires-Dist: apache-airflow-providers-fab ; extra == "fab"
27
27
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
28
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-edge3/1.0.0/changelog.html
29
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-edge3/1.0.0
28
+ Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-edge3/1.1.0/changelog.html
29
+ Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-edge3/1.1.0
30
30
  Project-URL: Mastodon, https://fosstodon.org/@airflow
31
31
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
32
32
  Project-URL: Source Code, https://github.com/apache/airflow
33
33
  Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
34
- Provides-Extra: fab
35
34
 
36
35
 
37
36
  .. Licensed to the Apache Software Foundation (ASF) under one
@@ -58,10 +57,22 @@ Provides-Extra: fab
58
57
 
59
58
  Package ``apache-airflow-providers-edge3``
60
59
 
61
- Release: ``1.0.0``
60
+ Release: ``1.1.0``
62
61
 
63
62
 
64
- Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites
63
+ Handle edge workers on remote sites via HTTP(s) connection and orchestrates work over distributed sites.
64
+
65
+ When tasks need to be executed on remote sites where the connection need to pass through
66
+ firewalls or other network restrictions, the Edge Worker can be deployed. The Edge Worker
67
+ is a lightweight process with reduced dependencies. The worker only needs to be able to
68
+ communicate with the central Airflow site via HTTPS.
69
+
70
+ In the central Airflow site the EdgeExecutor is used to orchestrate the work. The EdgeExecutor
71
+ is a custom executor which is used to schedule tasks on the edge workers. The EdgeExecutor can co-exist
72
+ with other executors (for example CeleryExecutor or KubernetesExecutor) in the same Airflow site.
73
+
74
+ Additional REST API endpoints are provided to distribute tasks and manage the edge workers. The endpoints
75
+ are provided by the API server.
65
76
 
66
77
 
67
78
  Provider package
@@ -71,7 +82,7 @@ This is a provider package for ``edge3`` provider. All classes for this provider
71
82
  are in ``airflow.providers.edge3`` python package.
72
83
 
73
84
  You can find package information and changelog for the provider
74
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-edge3/1.0.0/>`_.
85
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-edge3/1.1.0/>`_.
75
86
 
76
87
  Installation
77
88
  ------------
@@ -85,13 +96,14 @@ The package supports the following python versions: 3.9,3.10,3.11,3.12
85
96
  Requirements
86
97
  ------------
87
98
 
88
- ================== ===================
89
- PIP package Version required
90
- ================== ===================
91
- ``apache-airflow`` ``>=2.10.0``
92
- ``pydantic`` ``>=2.11.0``
93
- ``retryhttp`` ``>=1.2.0,!=1.3.0``
94
- ================== ===================
99
+ ================================ ===================
100
+ PIP package Version required
101
+ ================================ ===================
102
+ ``apache-airflow`` ``>=2.10.0``
103
+ ``apache-airflow-providers-fab`` ``>=1.5.3``
104
+ ``pydantic`` ``>=2.11.0``
105
+ ``retryhttp`` ``>=1.2.0,!=1.3.0``
106
+ ================================ ===================
95
107
 
96
108
  Cross provider package dependencies
97
109
  -----------------------------------
@@ -113,5 +125,5 @@ Dependent package
113
125
  ============================================================================================== =======
114
126
 
115
127
  The changelog for the provider package can be found in the
116
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-edge3/1.0.0/changelog.html>`_.
128
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-edge3/1.1.0/changelog.html>`_.
117
129
 
@@ -1,21 +1,21 @@
1
1
  airflow/providers/edge3/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850
2
- airflow/providers/edge3/__init__.py,sha256=hb8OUkltNOLHQS7pnEmS9LOc9NJyajbwwZY_51qn6kk,1494
3
- airflow/providers/edge3/get_provider_info.py,sha256=T8CMBInzQT7TRJFEFf3tbDK6otJ210gXj4qOaOLyfnE,5548
4
- airflow/providers/edge3/version_compat.py,sha256=aHg90_DtgoSnQvILFICexMyNlHlALBdaeWqkX3dFDug,1605
2
+ airflow/providers/edge3/__init__.py,sha256=NrV-3Uc00pr80NbPVUZWK9JYceO_jK26xp9BmvwFGnU,1494
3
+ airflow/providers/edge3/get_provider_info.py,sha256=Ek27-dB4UALHUFYoYjtoQIGq0p7zeHcEgmELHvpVmCU,6836
4
+ airflow/providers/edge3/version_compat.py,sha256=j5PCtXvZ71aBjixu-EFTNtVDPsngzzs7os0ZQDgFVDk,1536
5
5
  airflow/providers/edge3/cli/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
6
- airflow/providers/edge3/cli/api_client.py,sha256=J5l99mwwKXC7Ub_kO6fe_56A8Ue3ag8mIYxD_qPLfuQ,7497
6
+ airflow/providers/edge3/cli/api_client.py,sha256=334KHVB4eMSzRpQ5emS56o-RTUJQprxf5Q3xQldCHDQ,7440
7
7
  airflow/providers/edge3/cli/dataclasses.py,sha256=JUuvvmzSVWvG9uOEfzLIiXrTZ-HbESvu50jkPpVIYVw,2895
8
- airflow/providers/edge3/cli/edge_command.py,sha256=xWNcgvUJyHqT51_Q-6bVPjCd1lrI7KpxSrFfejPvgjA,27466
8
+ airflow/providers/edge3/cli/edge_command.py,sha256=dRKC1VPAUIvJnzvPlmcT98oBGlG0_MK7TBTNvjDEI2k,35658
9
9
  airflow/providers/edge3/example_dags/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
10
- airflow/providers/edge3/example_dags/integration_test.py,sha256=pXf6rmS4crHKfCBT73LvPmhJS8Rmri8pUlfIvuLja7E,5240
10
+ airflow/providers/edge3/example_dags/integration_test.py,sha256=kevxwDePjTRqLWJGV-nKEQQWb7qb-y2oedm3mdqi6-8,6127
11
11
  airflow/providers/edge3/example_dags/win_notepad.py,sha256=2evbqiupi5Ko4tyRkIEC5TPbc2c0wyR2YpsDYsBLMMM,2828
12
12
  airflow/providers/edge3/example_dags/win_test.py,sha256=GegWqjvbsSdbsA_f3S9_FRYftVO0pggXwQQggB9Vvz4,13220
13
13
  airflow/providers/edge3/executors/__init__.py,sha256=y830gGSKCvjOcLwLuCDp84NCrHWWB9RSSH1qvJpFhyY,923
14
- airflow/providers/edge3/executors/edge_executor.py,sha256=d05OpRKd82mSkKeybNZPXw1yiRdZAKYLZGh15eIgaiM,15254
14
+ airflow/providers/edge3/executors/edge_executor.py,sha256=Z7q1Ph_SvAuT1cR4YYcEmKYt4Yi711q65VrzofQ5t1A,15791
15
15
  airflow/providers/edge3/models/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
16
16
  airflow/providers/edge3/models/edge_job.py,sha256=rdl9cH1bBrc7id8zkZ7uxsCJNPLG-8o9cnspGwBPBcQ,3167
17
17
  airflow/providers/edge3/models/edge_logs.py,sha256=bNstp7gR54O2vbxzz4NTL0erbifFbGUjZ-YOM0I4sqk,2768
18
- airflow/providers/edge3/models/edge_worker.py,sha256=MsWzvJRTdvCRLZAMDy-nYYkMOl6Ji6AUeNfSQOeO6do,8587
18
+ airflow/providers/edge3/models/edge_worker.py,sha256=7U74k24xw36X8gQkrac3yWbtFBHbopvmRIWsjpFY8Og,10693
19
19
  airflow/providers/edge3/openapi/__init__.py,sha256=0O-WvmDx8GeKSoECpHYrbe0hW-LgjlKny3VqTCpBQeQ,927
20
20
  airflow/providers/edge3/openapi/edge_worker_api_v1.yaml,sha256=GAE2IdOXmcUueNy5KFkLBgNpoWnOjnHT9TrW5NZEWpI,24938
21
21
  airflow/providers/edge3/plugins/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
@@ -24,7 +24,7 @@ airflow/providers/edge3/plugins/templates/edge_worker_hosts.html,sha256=0_P2yfZw
24
24
  airflow/providers/edge3/plugins/templates/edge_worker_jobs.html,sha256=bZ-6ysmIy6j4eR_TPHiqbgb3qpNMKCcEEB-SpxuxNgc,2831
25
25
  airflow/providers/edge3/worker_api/__init__.py,sha256=nnPvxWGTEKZ9YyB1Yd7P9IvDOenK01LVHm22Owwxj3g,839
26
26
  airflow/providers/edge3/worker_api/app.py,sha256=Dda2VjkzgBtbQbSWSVEAoqd22RlqvBMyiPau65uKkv4,2006
27
- airflow/providers/edge3/worker_api/auth.py,sha256=4UFKc6wQPt9AqoMXMxt1rXljEy8xofgaIhzIdLR54gc,4936
27
+ airflow/providers/edge3/worker_api/auth.py,sha256=XVTfL-c0JYUhpVkKdqqaxZACZXqvnPp_3W6q2TK0Bjc,4883
28
28
  airflow/providers/edge3/worker_api/datamodels.py,sha256=FAiXqnrSN8zH4YE2fUMjXfXcH9cHlhRh4uZvvr936Ys,6696
29
29
  airflow/providers/edge3/worker_api/routes/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
30
30
  airflow/providers/edge3/worker_api/routes/_v2_compat.py,sha256=Q4b2Io0yoK5V_hbgk6fiFviTeT6CFbFMOGYRZgLEeR4,4543
@@ -32,8 +32,8 @@ airflow/providers/edge3/worker_api/routes/_v2_routes.py,sha256=-WAofvXJpOYpTyh98
32
32
  airflow/providers/edge3/worker_api/routes/health.py,sha256=XxqIppnRA138Q6mAHCdyL2JvoeeganUiI-TXyXSPTGo,1075
33
33
  airflow/providers/edge3/worker_api/routes/jobs.py,sha256=UK1w6nXEUadOLwE9abZ4jHH4KtbvXcwaAF0EnwSa3y4,5733
34
34
  airflow/providers/edge3/worker_api/routes/logs.py,sha256=uk0SZ5hAimj3sAcq1FYCDu0AXYNeTeyjZDGBvw-986E,4945
35
- airflow/providers/edge3/worker_api/routes/worker.py,sha256=jb6m9eE9J1fyejf-DuEtGPzGrynyUmf5qkZxLeN-DQo,8554
36
- apache_airflow_providers_edge3-1.0.0rc2.dist-info/entry_points.txt,sha256=7WUIGfd3o9NvvbK5trbZxNXTgYGc6pqg74wZPigbx5o,206
37
- apache_airflow_providers_edge3-1.0.0rc2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
38
- apache_airflow_providers_edge3-1.0.0rc2.dist-info/METADATA,sha256=ZeuPt1_xbWSvlUWlVMcAM0aR5VrVCqfdyHeZBNLsFW0,4986
39
- apache_airflow_providers_edge3-1.0.0rc2.dist-info/RECORD,,
35
+ airflow/providers/edge3/worker_api/routes/worker.py,sha256=BGARu1RZ74lW9X-ltuMYbbVXczm_MZdqHaai2MhDWtY,8969
36
+ apache_airflow_providers_edge3-1.1.0rc1.dist-info/entry_points.txt,sha256=7WUIGfd3o9NvvbK5trbZxNXTgYGc6pqg74wZPigbx5o,206
37
+ apache_airflow_providers_edge3-1.1.0rc1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
38
+ apache_airflow_providers_edge3-1.1.0rc1.dist-info/METADATA,sha256=3kJ3dEUkqjhppUJvNuD0I4r7MOg0pZ1kjmWcOrOARS0,5876
39
+ apache_airflow_providers_edge3-1.1.0rc1.dist-info/RECORD,,