apache-airflow-providers-edge3 1.5.0__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 +9 -28
- 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/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 -8
- 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 +17 -15
- airflow/providers/edge3/plugins/www/pnpm-lock.yaml +1194 -1244
- 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.5.0.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/METADATA +13 -13
- {apache_airflow_providers_edge3-1.5.0.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/RECORD +45 -40
- 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.5.0.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_edge3-1.5.0.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/entry_points.txt +0 -0
- {apache_airflow_providers_edge3-1.5.0.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {apache_airflow_providers_edge3-1.5.0.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/licenses/NOTICE +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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
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.")]
|
|
@@ -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.
|
|
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)
|
|
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.
|
|
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
|