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.
Files changed (50) hide show
  1. airflow/providers/edge3/__init__.py +3 -3
  2. airflow/providers/edge3/cli/api_client.py +23 -26
  3. airflow/providers/edge3/cli/worker.py +14 -29
  4. airflow/providers/edge3/example_dags/integration_test.py +1 -1
  5. airflow/providers/edge3/example_dags/win_test.py +32 -22
  6. airflow/providers/edge3/executors/edge_executor.py +7 -63
  7. airflow/providers/edge3/get_provider_info.py +7 -0
  8. airflow/providers/edge3/models/edge_worker.py +7 -3
  9. airflow/providers/edge3/plugins/edge_executor_plugin.py +26 -205
  10. airflow/providers/edge3/plugins/www/dist/main.umd.cjs +8 -100
  11. airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +6 -1
  12. airflow/providers/edge3/plugins/www/openapi-gen/queries/ensureQueryData.ts +6 -1
  13. airflow/providers/edge3/plugins/www/openapi-gen/queries/prefetch.ts +6 -1
  14. airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +6 -2
  15. airflow/providers/edge3/plugins/www/openapi-gen/queries/suspense.ts +6 -1
  16. airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +5 -0
  17. airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +18 -3
  18. airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +24 -0
  19. airflow/providers/edge3/plugins/www/package.json +26 -24
  20. airflow/providers/edge3/plugins/www/pnpm-lock.yaml +1469 -1413
  21. airflow/providers/edge3/plugins/www/src/components/SearchBar.tsx +103 -0
  22. airflow/providers/edge3/plugins/www/src/components/ui/InputGroup.tsx +57 -0
  23. airflow/providers/edge3/plugins/www/src/components/ui/Select/Content.tsx +37 -0
  24. airflow/providers/edge3/plugins/www/src/components/ui/Select/Item.tsx +34 -0
  25. airflow/providers/edge3/plugins/www/src/components/ui/Select/Root.tsx +24 -0
  26. airflow/providers/edge3/plugins/www/src/components/ui/Select/Trigger.tsx +54 -0
  27. airflow/providers/edge3/plugins/www/src/components/ui/Select/ValueText.tsx +51 -0
  28. airflow/providers/edge3/plugins/www/src/components/ui/Select/index.ts +34 -0
  29. airflow/providers/edge3/plugins/www/src/components/ui/index.ts +3 -0
  30. airflow/providers/edge3/plugins/www/src/constants.ts +43 -0
  31. airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx +184 -95
  32. airflow/providers/edge3/version_compat.py +0 -2
  33. airflow/providers/edge3/worker_api/auth.py +11 -35
  34. airflow/providers/edge3/worker_api/datamodels.py +3 -2
  35. airflow/providers/edge3/worker_api/routes/health.py +1 -1
  36. airflow/providers/edge3/worker_api/routes/jobs.py +10 -11
  37. airflow/providers/edge3/worker_api/routes/logs.py +5 -8
  38. airflow/providers/edge3/worker_api/routes/ui.py +14 -3
  39. airflow/providers/edge3/worker_api/routes/worker.py +19 -12
  40. airflow/providers/edge3/{openapi → worker_api}/v2-edge-generated.yaml +59 -5
  41. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/METADATA +16 -14
  42. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/RECORD +46 -40
  43. apache_airflow_providers_edge3-2.0.0.dist-info/licenses/NOTICE +5 -0
  44. airflow/providers/edge3/openapi/__init__.py +0 -19
  45. airflow/providers/edge3/openapi/edge_worker_api_v1.yaml +0 -808
  46. airflow/providers/edge3/worker_api/routes/_v2_compat.py +0 -136
  47. airflow/providers/edge3/worker_api/routes/_v2_routes.py +0 -237
  48. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/WHEEL +0 -0
  49. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/entry_points.txt +0 -0
  50. {airflow/providers/edge3 → apache_airflow_providers_edge3-2.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -16,22 +16,58 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { Box, Code, Link, List, Table, Text } from "@chakra-ui/react";
19
+ import { Box, Code, HStack, Link, List, Table, Text, type SelectValueChangeDetails } from "@chakra-ui/react";
20
+ import { useState, useCallback } from "react";
20
21
  import { useUiServiceWorker } from "openapi/queries";
21
22
  import { LuExternalLink } from "react-icons/lu";
22
23
  import TimeAgo from "react-timeago";
23
24
 
24
25
  import { ErrorAlert } from "src/components/ErrorAlert";
26
+ import { SearchBar } from "src/components/SearchBar";
25
27
  import { WorkerOperations } from "src/components/WorkerOperations";
26
28
  import { WorkerStateBadge } from "src/components/WorkerStateBadge";
27
- import { ScrollToAnchor } from "src/components/ui";
29
+ import { ScrollToAnchor, Select } from "src/components/ui";
30
+ import { workerStateOptions } from "src/constants";
28
31
  import { autoRefreshInterval } from "src/utils";
32
+ import type { EdgeWorkerState } from "openapi/requests/types.gen";
29
33
 
30
34
  export const WorkerPage = () => {
31
- const { data, error, refetch } = useUiServiceWorker(undefined, {
32
- enabled: true,
33
- refetchInterval: autoRefreshInterval,
34
- });
35
+ const [workerNamePattern, setWorkerNamePattern] = useState("");
36
+ const [queueNamePattern, setQueueNamePattern] = useState("");
37
+ const [filteredState, setFilteredState] = useState<string[]>([]);
38
+
39
+ const hasFilteredState = filteredState.length > 0;
40
+
41
+ const { data, error, refetch } = useUiServiceWorker(
42
+ {
43
+ queueNamePattern: queueNamePattern || undefined,
44
+ state: hasFilteredState ? (filteredState as EdgeWorkerState[]) : undefined,
45
+ workerNamePattern: workerNamePattern || undefined,
46
+ },
47
+ undefined,
48
+ {
49
+ enabled: true,
50
+ refetchInterval: autoRefreshInterval,
51
+ },
52
+ );
53
+
54
+ const handleWorkerSearchChange = (value: string) => {
55
+ setWorkerNamePattern(value);
56
+ };
57
+
58
+ const handleQueueSearchChange = (value: string) => {
59
+ setQueueNamePattern(value);
60
+ };
61
+
62
+ const handleStateChange = useCallback(({ value }: SelectValueChangeDetails<string>) => {
63
+ const [val, ...rest] = value;
64
+
65
+ if ((val === undefined || val === "all") && rest.length === 0) {
66
+ setFilteredState([]);
67
+ } else {
68
+ setFilteredState(value.filter((state) => state !== "all"));
69
+ }
70
+ }, []);
35
71
 
36
72
  // TODO to make it proper
37
73
  // Use DataTable as component from Airflow-Core UI
@@ -40,96 +76,149 @@ export const WorkerPage = () => {
40
76
  // Add links with filter to see jobs on worker
41
77
  // Add time zone support for time display
42
78
  // Translation?
43
- if (data?.workers && data.workers.length > 0)
44
- return (
45
- <Box p={2}>
46
- <Table.Root size="sm" interactive stickyHeader striped>
47
- <Table.Header>
48
- <Table.Row>
49
- <Table.ColumnHeader>Worker Name</Table.ColumnHeader>
50
- <Table.ColumnHeader>State</Table.ColumnHeader>
51
- <Table.ColumnHeader>Queues</Table.ColumnHeader>
52
- <Table.ColumnHeader>First Online</Table.ColumnHeader>
53
- <Table.ColumnHeader>Last Heartbeat</Table.ColumnHeader>
54
- <Table.ColumnHeader>Active Jobs</Table.ColumnHeader>
55
- <Table.ColumnHeader>System Information</Table.ColumnHeader>
56
- <Table.ColumnHeader>Operations</Table.ColumnHeader>
57
- </Table.Row>
58
- </Table.Header>
59
- <Table.Body>
60
- {data.workers.map((worker) => (
61
- <Table.Row key={worker.worker_name} id={worker.worker_name}>
62
- <Table.Cell>{worker.worker_name}</Table.Cell>
63
- <Table.Cell>
64
- <WorkerStateBadge state={worker.state}>{worker.state}</WorkerStateBadge>
65
- </Table.Cell>
66
- <Table.Cell>
67
- {worker.queues ? (
68
- <List.Root>
69
- {worker.queues.map((queue) => (
70
- <List.Item key={queue}>{queue}</List.Item>
71
- ))}
72
- </List.Root>
73
- ) : (
74
- "(all queues)"
75
- )}
76
- </Table.Cell>
77
- <Table.Cell>
78
- {worker.first_online ? <TimeAgo date={worker.first_online} live={false} /> : undefined}
79
- </Table.Cell>
80
- <Table.Cell>
81
- {worker.last_heartbeat ? <TimeAgo date={worker.last_heartbeat} live={false} /> : undefined}
82
- </Table.Cell>
83
- <Table.Cell>{worker.jobs_active}</Table.Cell>
84
- <Table.Cell>
85
- {worker.sysinfo ? (
86
- <List.Root>
87
- {Object.entries(worker.sysinfo).map(([key, value]) => (
88
- <List.Item key={key}>
89
- {key}: {value}
90
- </List.Item>
91
- ))}
92
- </List.Root>
93
- ) : (
94
- "N/A"
95
- )}
96
- </Table.Cell>
97
- <Table.Cell>
98
- <WorkerOperations worker={worker} onOperations={refetch} />
99
- </Table.Cell>
100
- </Table.Row>
101
- ))}
102
- </Table.Body>
103
- </Table.Root>
104
- <ScrollToAnchor />
105
- </Box>
106
- );
107
- if (data) {
108
- return (
109
- <Text as="div" pl={4} pt={1}>
110
- No known workers. Start one via <Code>airflow edge worker [...]</Code>. See{" "}
111
- <Link
112
- target="_blank"
113
- variant="underline"
114
- color="fg.info"
115
- href="https://airflow.apache.org/docs/apache-airflow-providers-edge3/stable/deployment.html"
79
+
80
+ return (
81
+ <Box p={2}>
82
+ <HStack gap={4} mb={4}>
83
+ <SearchBar
84
+ buttonProps={{ disabled: true }}
85
+ defaultValue={workerNamePattern}
86
+ hideAdvanced
87
+ hotkeyDisabled
88
+ onChange={handleWorkerSearchChange}
89
+ placeHolder="Search workers"
90
+ />
91
+ <SearchBar
92
+ buttonProps={{ disabled: true }}
93
+ defaultValue={queueNamePattern}
94
+ hideAdvanced
95
+ hotkeyDisabled
96
+ onChange={handleQueueSearchChange}
97
+ placeHolder="Search queues"
98
+ />
99
+ <Select.Root
100
+ collection={workerStateOptions}
101
+ maxW="450px"
102
+ multiple
103
+ onValueChange={handleStateChange}
104
+ value={hasFilteredState ? filteredState : ["all"]}
116
105
  >
117
- Edge Worker Deployment docs <LuExternalLink />
118
- </Link>{" "}
119
- how to deploy a new worker.
120
- </Text>
121
- );
122
- }
123
- if (error) {
124
- return (
125
- <Text as="div" pl={4} pt={1}>
106
+ <Select.Trigger
107
+ {...(hasFilteredState ? { clearable: true } : {})}
108
+ colorPalette="brand"
109
+ isActive={Boolean(filteredState)}
110
+ >
111
+ <Select.ValueText>
112
+ {() =>
113
+ hasFilteredState ? (
114
+ <HStack flexWrap="wrap" fontSize="sm" gap="4px" paddingY="8px">
115
+ {filteredState.map((state) => (
116
+ <WorkerStateBadge key={state} state={state as EdgeWorkerState}>
117
+ {state}
118
+ </WorkerStateBadge>
119
+ ))}
120
+ </HStack>
121
+ ) : (
122
+ "All States"
123
+ )
124
+ }
125
+ </Select.ValueText>
126
+ </Select.Trigger>
127
+ <Select.Content>
128
+ {workerStateOptions.items.map((option) => (
129
+ <Select.Item item={option} key={option.label}>
130
+ {option.value === "all" ? (
131
+ option.label
132
+ ) : (
133
+ <WorkerStateBadge state={option.value as EdgeWorkerState}>{option.label}</WorkerStateBadge>
134
+ )}
135
+ </Select.Item>
136
+ ))}
137
+ </Select.Content>
138
+ </Select.Root>
139
+ </HStack>
140
+ {error ? (
126
141
  <ErrorAlert error={error} />
127
- </Text>
128
- );
129
- }
130
- return (
131
- <Text as="div" pl={4} pt={1}>
132
- Loading...
133
- </Text>
142
+ ) : !data ? (
143
+ <Text as="div" pl={2} pt={1}>
144
+ Loading...
145
+ </Text>
146
+ ) : data.workers && data.workers.length > 0 ? (
147
+ <>
148
+ <Table.Root size="sm" interactive stickyHeader striped>
149
+ <Table.Header>
150
+ <Table.Row>
151
+ <Table.ColumnHeader>Worker Name</Table.ColumnHeader>
152
+ <Table.ColumnHeader>State</Table.ColumnHeader>
153
+ <Table.ColumnHeader>Queues</Table.ColumnHeader>
154
+ <Table.ColumnHeader>First Online</Table.ColumnHeader>
155
+ <Table.ColumnHeader>Last Heartbeat</Table.ColumnHeader>
156
+ <Table.ColumnHeader>Active Jobs</Table.ColumnHeader>
157
+ <Table.ColumnHeader>System Information</Table.ColumnHeader>
158
+ <Table.ColumnHeader>Operations</Table.ColumnHeader>
159
+ </Table.Row>
160
+ </Table.Header>
161
+ <Table.Body>
162
+ {data.workers.map((worker) => (
163
+ <Table.Row key={worker.worker_name} id={worker.worker_name}>
164
+ <Table.Cell>{worker.worker_name}</Table.Cell>
165
+ <Table.Cell>
166
+ <WorkerStateBadge state={worker.state}>{worker.state}</WorkerStateBadge>
167
+ </Table.Cell>
168
+ <Table.Cell>
169
+ {worker.queues ? (
170
+ <List.Root>
171
+ {worker.queues.map((queue) => (
172
+ <List.Item key={queue}>{queue}</List.Item>
173
+ ))}
174
+ </List.Root>
175
+ ) : (
176
+ "(all queues)"
177
+ )}
178
+ </Table.Cell>
179
+ <Table.Cell>
180
+ {worker.first_online ? <TimeAgo date={worker.first_online} live={false} /> : undefined}
181
+ </Table.Cell>
182
+ <Table.Cell>
183
+ {worker.last_heartbeat ? <TimeAgo date={worker.last_heartbeat} live={false} /> : undefined}
184
+ </Table.Cell>
185
+ <Table.Cell>{worker.jobs_active}</Table.Cell>
186
+ <Table.Cell>
187
+ {worker.sysinfo ? (
188
+ <List.Root>
189
+ {Object.entries(worker.sysinfo).map(([key, value]) => (
190
+ <List.Item key={key}>
191
+ {key}: {value}
192
+ </List.Item>
193
+ ))}
194
+ </List.Root>
195
+ ) : (
196
+ "N/A"
197
+ )}
198
+ </Table.Cell>
199
+ <Table.Cell>
200
+ <WorkerOperations worker={worker} onOperations={refetch} />
201
+ </Table.Cell>
202
+ </Table.Row>
203
+ ))}
204
+ </Table.Body>
205
+ </Table.Root>
206
+ <ScrollToAnchor />
207
+ </>
208
+ ) : (
209
+ <Text as="div" pl={2} pt={1}>
210
+ No known workers. Start one via <Code>airflow edge worker [...]</Code>. See{" "}
211
+ <Link
212
+ target="_blank"
213
+ variant="underline"
214
+ color="fg.info"
215
+ href="https://airflow.apache.org/docs/apache-airflow-providers-edge3/stable/deployment.html"
216
+ >
217
+ Edge Worker Deployment docs <LuExternalLink />
218
+ </Link>{" "}
219
+ how to deploy a new worker.
220
+ </Text>
221
+ )}
222
+ </Box>
134
223
  );
135
224
  };
@@ -32,10 +32,8 @@ 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_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
36
35
  AIRFLOW_V_3_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 0)
37
36
 
38
37
  __all__ = [
39
- "AIRFLOW_V_3_0_PLUS",
40
38
  "AIRFLOW_V_3_1_PLUS",
41
39
  ]
@@ -20,6 +20,7 @@ import logging
20
20
  from functools import cache
21
21
  from uuid import uuid4
22
22
 
23
+ from fastapi import Header, HTTPException, Request, status
23
24
  from itsdangerous import BadSignature
24
25
  from jwt import (
25
26
  ExpiredSignatureError,
@@ -29,49 +30,24 @@ from jwt import (
29
30
  InvalidSignatureError,
30
31
  )
31
32
 
33
+ from airflow.api_fastapi.auth.tokens import JWTValidator
32
34
  from airflow.configuration import conf
33
- from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
34
35
  from airflow.providers.edge3.worker_api.datamodels import JsonRpcRequestBase # noqa: TCH001
35
- from airflow.providers.edge3.worker_api.routes._v2_compat import (
36
- Header,
37
- HTTPException,
38
- Request,
39
- status,
40
- )
41
36
 
42
37
  log = logging.getLogger(__name__)
43
38
 
44
39
 
45
- if AIRFLOW_V_3_0_PLUS:
46
- from airflow.api_fastapi.auth.tokens import JWTValidator
47
-
48
- @cache
49
- def jwt_validator() -> JWTValidator:
50
- return JWTValidator(
51
- secret_key=conf.get("api_auth", "jwt_secret"),
52
- leeway=conf.getint("api_auth", "jwt_leeway", fallback=30),
53
- audience="api",
54
- )
40
+ @cache
41
+ def jwt_validator() -> JWTValidator:
42
+ return JWTValidator(
43
+ secret_key=conf.get("api_auth", "jwt_secret"),
44
+ leeway=conf.getint("api_auth", "jwt_leeway", fallback=30),
45
+ audience="api",
46
+ )
55
47
 
56
- def jwt_validate(authorization: str) -> dict:
57
- return jwt_validator().validated_claims(authorization)
58
-
59
- else:
60
- # Airflow 2.10 compatibility
61
- from airflow.utils.jwt_signer import JWTSigner
62
-
63
- @cache
64
- def jwt_signer() -> JWTSigner:
65
- clock_grace = conf.getint("core", "internal_api_clock_grace", fallback=30)
66
- return JWTSigner(
67
- secret_key=conf.get("core", "internal_api_secret_key"),
68
- expiration_time_in_seconds=clock_grace,
69
- leeway_in_seconds=clock_grace,
70
- audience="api",
71
- )
72
48
 
73
- def jwt_validate(authorization: str) -> dict:
74
- return jwt_signer().verify_token(authorization)
49
+ def jwt_validate(authorization: str) -> dict:
50
+ return jwt_validator().validated_claims(authorization)
75
51
 
76
52
 
77
53
  def _forbidden_response(message: str):
@@ -22,11 +22,12 @@ from typing import (
22
22
  Any,
23
23
  )
24
24
 
25
+ from fastapi import Path
25
26
  from pydantic import BaseModel, Field
26
27
 
28
+ from airflow.executors.workloads import ExecuteTask # noqa: TCH001
27
29
  from airflow.models.taskinstancekey import TaskInstanceKey
28
30
  from airflow.providers.edge3.models.edge_worker import EdgeWorkerState # noqa: TCH001
29
- from airflow.providers.edge3.worker_api.routes._v2_compat import ExecuteTask, Path
30
31
 
31
32
 
32
33
  class WorkerApiDocs:
@@ -93,7 +94,7 @@ class EdgeJobFetched(EdgeJobBase):
93
94
  ExecuteTask,
94
95
  Field(
95
96
  title="Command",
96
- description="Command line to use to execute the job in Airflow 2. Task definition in Airflow 3",
97
+ description="Command line to use to execute the job in Airflow",
97
98
  ),
98
99
  ]
99
100
  concurrency_slots: Annotated[int, Field(description="Number of concurrency slots the job requires.")]
@@ -17,7 +17,7 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from airflow.providers.edge3.worker_api.routes._v2_compat import AirflowRouter
20
+ from airflow.api_fastapi.common.router import AirflowRouter
21
21
 
22
22
  health_router = AirflowRouter(tags=["Monitor"])
23
23
 
@@ -19,9 +19,14 @@ from __future__ import annotations
19
19
 
20
20
  from typing import Annotated
21
21
 
22
+ from fastapi import Body, Depends, status
22
23
  from sqlalchemy import select, update
23
24
 
24
- from airflow.providers.common.compat.sdk import timezone
25
+ from airflow.api_fastapi.common.db.common import SessionDep # noqa: TC001
26
+ from airflow.api_fastapi.common.router import AirflowRouter
27
+ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
28
+ from airflow.executors.workloads import ExecuteTask
29
+ from airflow.providers.common.compat.sdk import Stats, timezone
25
30
  from airflow.providers.edge3.models.edge_job import EdgeJobModel
26
31
  from airflow.providers.edge3.worker_api.auth import jwt_token_authorization_rest
27
32
  from airflow.providers.edge3.worker_api.datamodels import (
@@ -29,21 +34,15 @@ from airflow.providers.edge3.worker_api.datamodels import (
29
34
  WorkerApiDocs,
30
35
  WorkerQueuesBody,
31
36
  )
32
- from airflow.providers.edge3.worker_api.routes._v2_compat import (
33
- AirflowRouter,
34
- Body,
35
- Depends,
36
- SessionDep,
37
- create_openapi_http_exception_doc,
38
- parse_command,
39
- status,
40
- )
41
- from airflow.stats import Stats
42
37
  from airflow.utils.state import TaskInstanceState
43
38
 
44
39
  jobs_router = AirflowRouter(tags=["Jobs"], prefix="/jobs")
45
40
 
46
41
 
42
+ def parse_command(command: str) -> ExecuteTask:
43
+ return ExecuteTask.model_validate_json(command)
44
+
45
+
47
46
  @jobs_router.post(
48
47
  "/fetch/{worker_name}",
49
48
  dependencies=[Depends(jwt_token_authorization_rest)],
@@ -21,20 +21,17 @@ from functools import cache
21
21
  from pathlib import Path
22
22
  from typing import TYPE_CHECKING, Annotated
23
23
 
24
+ from fastapi import Body, Depends, status
25
+
26
+ from airflow.api_fastapi.common.db.common import SessionDep # noqa: TC001
27
+ from airflow.api_fastapi.common.router import AirflowRouter
28
+ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
24
29
  from airflow.configuration import conf
25
30
  from airflow.models.taskinstance import TaskInstance
26
31
  from airflow.models.taskinstancekey import TaskInstanceKey
27
32
  from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
28
33
  from airflow.providers.edge3.worker_api.auth import jwt_token_authorization_rest
29
34
  from airflow.providers.edge3.worker_api.datamodels import PushLogsBody, WorkerApiDocs
30
- from airflow.providers.edge3.worker_api.routes._v2_compat import (
31
- AirflowRouter,
32
- Body,
33
- Depends,
34
- SessionDep,
35
- create_openapi_http_exception_doc,
36
- status,
37
- )
38
35
  from airflow.utils.log.file_task_handler import FileTaskHandler
39
36
  from airflow.utils.session import NEW_SESSION, provide_session
40
37
 
@@ -18,9 +18,9 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  from datetime import datetime
21
- from typing import TYPE_CHECKING
21
+ from typing import TYPE_CHECKING, Annotated
22
22
 
23
- from fastapi import Depends, HTTPException, status
23
+ from fastapi import Depends, HTTPException, Query, status
24
24
  from sqlalchemy import select
25
25
 
26
26
  from airflow.api_fastapi.auth.managers.models.resource_details import AccessView
@@ -30,6 +30,7 @@ from airflow.api_fastapi.core_api.security import GetUserDep, requires_access_vi
30
30
  from airflow.providers.edge3.models.edge_job import EdgeJobModel
31
31
  from airflow.providers.edge3.models.edge_worker import (
32
32
  EdgeWorkerModel,
33
+ EdgeWorkerState,
33
34
  add_worker_queues,
34
35
  change_maintenance_comment,
35
36
  exit_maintenance,
@@ -61,9 +62,19 @@ ui_router = AirflowRouter(tags=["UI"])
61
62
  )
62
63
  def worker(
63
64
  session: SessionDep,
65
+ worker_name_pattern: str | None = None,
66
+ queue_name_pattern: str | None = None,
67
+ state: Annotated[list[EdgeWorkerState] | None, Query()] = None,
64
68
  ) -> WorkerCollectionResponse:
65
69
  """Return Edge Workers."""
66
- query = select(EdgeWorkerModel).order_by(EdgeWorkerModel.worker_name)
70
+ query = select(EdgeWorkerModel)
71
+ if worker_name_pattern:
72
+ query = query.where(EdgeWorkerModel.worker_name.ilike(f"%{worker_name_pattern}%"))
73
+ if queue_name_pattern:
74
+ query = query.where(EdgeWorkerModel._queues.ilike(f"%'{queue_name_pattern}%"))
75
+ if state:
76
+ query = query.where(EdgeWorkerModel.state.in_(state))
77
+ query = query.order_by(EdgeWorkerModel.worker_name)
67
78
  workers: ScalarResult[EdgeWorkerModel] = session.scalars(query)
68
79
 
69
80
  result = [
@@ -20,9 +20,13 @@ from __future__ import annotations
20
20
  import json
21
21
  from typing import Annotated
22
22
 
23
+ from fastapi import Body, Depends, HTTPException, Path, status
23
24
  from sqlalchemy import select
24
25
 
25
- from airflow.providers.common.compat.sdk import timezone
26
+ from airflow.api_fastapi.common.db.common import SessionDep # noqa: TC001
27
+ from airflow.api_fastapi.common.router import AirflowRouter
28
+ from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
29
+ from airflow.providers.common.compat.sdk import Stats, timezone
26
30
  from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, EdgeWorkerState, set_metrics
27
31
  from airflow.providers.edge3.worker_api.auth import jwt_token_authorization_rest
28
32
  from airflow.providers.edge3.worker_api.datamodels import (
@@ -31,17 +35,6 @@ from airflow.providers.edge3.worker_api.datamodels import (
31
35
  WorkerSetStateReturn,
32
36
  WorkerStateBody,
33
37
  )
34
- from airflow.providers.edge3.worker_api.routes._v2_compat import (
35
- AirflowRouter,
36
- Body,
37
- Depends,
38
- HTTPException,
39
- Path,
40
- SessionDep,
41
- create_openapi_http_exception_doc,
42
- status,
43
- )
44
- from airflow.stats import Stats
45
38
 
46
39
  worker_router = AirflowRouter(
47
40
  tags=["Worker"],
@@ -50,6 +43,7 @@ worker_router = AirflowRouter(
50
43
  [
51
44
  status.HTTP_400_BAD_REQUEST,
52
45
  status.HTTP_403_FORBIDDEN,
46
+ status.HTTP_409_CONFLICT,
53
47
  ]
54
48
  ),
55
49
  )
@@ -175,6 +169,19 @@ def register(
175
169
  worker: EdgeWorkerModel | None = session.scalar(query)
176
170
  if not worker:
177
171
  worker = EdgeWorkerModel(worker_name=worker_name, state=body.state, queues=body.queues)
172
+ else:
173
+ # Prevent duplicate workers unless the existing worker is in offline or unknown state
174
+ allowed_states_for_reuse = {
175
+ EdgeWorkerState.OFFLINE,
176
+ EdgeWorkerState.UNKNOWN,
177
+ EdgeWorkerState.OFFLINE_MAINTENANCE,
178
+ }
179
+ if worker.state not in allowed_states_for_reuse:
180
+ raise HTTPException(
181
+ status.HTTP_409_CONFLICT,
182
+ f"Worker '{worker_name}' is already active in state '{worker.state}'. "
183
+ f"Cannot start a duplicate worker with the same name.",
184
+ )
178
185
  worker.state = redefine_state(worker.state, body.state)
179
186
  worker.maintenance_comment = redefine_maintenance_comments(
180
187
  worker.maintenance_comment, body.maintenance_comments