prefect-client 2.14.10__py3-none-any.whl → 2.14.11__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.
- prefect/_internal/pydantic/v2_schema.py +9 -2
- prefect/client/orchestration.py +50 -2
- prefect/client/schemas/objects.py +16 -1
- prefect/deployments/runner.py +29 -2
- prefect/engine.py +128 -16
- prefect/flows.py +16 -0
- prefect/infrastructure/kubernetes.py +64 -0
- prefect/infrastructure/provisioners/cloud_run.py +199 -33
- prefect/infrastructure/provisioners/container_instance.py +398 -115
- prefect/infrastructure/provisioners/ecs.py +483 -48
- prefect/input/__init__.py +11 -0
- prefect/input/actions.py +88 -0
- prefect/input/run_input.py +107 -0
- prefect/runner/runner.py +5 -0
- prefect/runner/server.py +92 -8
- prefect/runner/utils.py +92 -0
- prefect/settings.py +34 -9
- prefect/utilities/dockerutils.py +31 -0
- prefect/utilities/processutils.py +5 -2
- prefect/utilities/validation.py +63 -0
- prefect/workers/utilities.py +0 -1
- {prefect_client-2.14.10.dist-info → prefect_client-2.14.11.dist-info}/METADATA +1 -1
- {prefect_client-2.14.10.dist-info → prefect_client-2.14.11.dist-info}/RECORD +26 -21
- {prefect_client-2.14.10.dist-info → prefect_client-2.14.11.dist-info}/LICENSE +0 -0
- {prefect_client-2.14.10.dist-info → prefect_client-2.14.11.dist-info}/WHEEL +0 -0
- {prefect_client-2.14.10.dist-info → prefect_client-2.14.11.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ import enum
|
|
3
3
|
import json
|
4
4
|
import math
|
5
5
|
import os
|
6
|
+
import shlex
|
6
7
|
import time
|
7
8
|
from contextlib import contextmanager
|
8
9
|
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Tuple, Union
|
@@ -359,6 +360,69 @@ class KubernetesJob(Infrastructure):
|
|
359
360
|
def preview(self):
|
360
361
|
return yaml.dump(self.build_job())
|
361
362
|
|
363
|
+
def get_corresponding_worker_type(self):
|
364
|
+
return "kubernetes"
|
365
|
+
|
366
|
+
async def generate_work_pool_base_job_template(self):
|
367
|
+
from prefect.workers.utilities import (
|
368
|
+
get_default_base_job_template_for_infrastructure_type,
|
369
|
+
)
|
370
|
+
|
371
|
+
base_job_template = await get_default_base_job_template_for_infrastructure_type(
|
372
|
+
self.get_corresponding_worker_type()
|
373
|
+
)
|
374
|
+
assert (
|
375
|
+
base_job_template is not None
|
376
|
+
), "Failed to retrieve default base job template."
|
377
|
+
for key, value in self.dict(exclude_unset=True, exclude_defaults=True).items():
|
378
|
+
if key == "command":
|
379
|
+
base_job_template["variables"]["properties"]["command"]["default"] = (
|
380
|
+
shlex.join(value)
|
381
|
+
)
|
382
|
+
elif key in [
|
383
|
+
"type",
|
384
|
+
"block_type_slug",
|
385
|
+
"_block_document_id",
|
386
|
+
"_block_document_name",
|
387
|
+
"_is_anonymous",
|
388
|
+
"job",
|
389
|
+
"customizations",
|
390
|
+
]:
|
391
|
+
continue
|
392
|
+
elif key == "image_pull_policy":
|
393
|
+
base_job_template["variables"]["properties"]["image_pull_policy"][
|
394
|
+
"default"
|
395
|
+
] = value.value
|
396
|
+
elif key == "cluster_config":
|
397
|
+
base_job_template["variables"]["properties"]["cluster_config"][
|
398
|
+
"default"
|
399
|
+
] = {
|
400
|
+
"$ref": {
|
401
|
+
"block_document_id": str(self.cluster_config._block_document_id)
|
402
|
+
}
|
403
|
+
}
|
404
|
+
elif key in base_job_template["variables"]["properties"]:
|
405
|
+
base_job_template["variables"]["properties"][key]["default"] = value
|
406
|
+
else:
|
407
|
+
self.logger.warning(
|
408
|
+
f"Variable {key!r} is not supported by Kubernetes work pools."
|
409
|
+
" Skipping."
|
410
|
+
)
|
411
|
+
|
412
|
+
custom_job_manifest = self.dict(exclude_unset=True, exclude_defaults=True).get(
|
413
|
+
"job"
|
414
|
+
)
|
415
|
+
if custom_job_manifest:
|
416
|
+
job_manifest = self.build_job()
|
417
|
+
else:
|
418
|
+
job_manifest = copy.deepcopy(
|
419
|
+
base_job_template["job_configuration"]["job_manifest"]
|
420
|
+
)
|
421
|
+
job_manifest = self.customizations.apply(job_manifest)
|
422
|
+
base_job_template["job_configuration"]["job_manifest"] = job_manifest
|
423
|
+
|
424
|
+
return base_job_template
|
425
|
+
|
362
426
|
def build_job(self) -> KubernetesManifest:
|
363
427
|
"""Builds the Kubernetes Job Manifest"""
|
364
428
|
job_manifest = copy.copy(self.job)
|
@@ -14,13 +14,18 @@ from rich.panel import Panel
|
|
14
14
|
from rich.pretty import Pretty
|
15
15
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
16
16
|
from rich.prompt import Confirm
|
17
|
+
from rich.syntax import Syntax
|
17
18
|
|
18
|
-
from prefect.cli._prompts import prompt_select_from_table
|
19
|
+
from prefect.cli._prompts import prompt, prompt_select_from_table
|
19
20
|
from prefect.client.orchestration import PrefectClient, ServerType
|
20
21
|
from prefect.client.schemas.actions import BlockDocumentCreate
|
21
22
|
from prefect.client.utilities import inject_client
|
22
23
|
from prefect.exceptions import ObjectAlreadyExists
|
23
|
-
from prefect.settings import
|
24
|
+
from prefect.settings import (
|
25
|
+
PREFECT_DEBUG_MODE,
|
26
|
+
PREFECT_DEFAULT_DOCKER_BUILD_NAMESPACE,
|
27
|
+
update_current_profile,
|
28
|
+
)
|
24
29
|
|
25
30
|
|
26
31
|
class CloudRunPushProvisioner:
|
@@ -28,6 +33,9 @@ class CloudRunPushProvisioner:
|
|
28
33
|
self._console = Console()
|
29
34
|
self._project = None
|
30
35
|
self._region = None
|
36
|
+
self._service_account_name = "prefect-cloud-run"
|
37
|
+
self._credentials_block_name = None
|
38
|
+
self._image_repository_name = "prefect-images"
|
31
39
|
|
32
40
|
@property
|
33
41
|
def console(self):
|
@@ -107,7 +115,9 @@ class CloudRunPushProvisioner:
|
|
107
115
|
|
108
116
|
async def _enable_cloud_run_api(self):
|
109
117
|
try:
|
110
|
-
await self._run_command(
|
118
|
+
await self._run_command(
|
119
|
+
f"gcloud services enable run.googleapis.com --project={self._project}"
|
120
|
+
)
|
111
121
|
|
112
122
|
except subprocess.CalledProcessError as e:
|
113
123
|
raise RuntimeError(
|
@@ -118,7 +128,7 @@ class CloudRunPushProvisioner:
|
|
118
128
|
async def _create_service_account(self):
|
119
129
|
try:
|
120
130
|
await self._run_command(
|
121
|
-
"gcloud iam service-accounts create
|
131
|
+
f"gcloud iam service-accounts create {self._service_account_name}"
|
122
132
|
' --display-name "Prefect Cloud Run Service Account"'
|
123
133
|
)
|
124
134
|
except subprocess.CalledProcessError as e:
|
@@ -134,27 +144,29 @@ class CloudRunPushProvisioner:
|
|
134
144
|
try:
|
135
145
|
await self._run_command(
|
136
146
|
"gcloud iam service-accounts keys create"
|
137
|
-
f" {tmpdir}/
|
138
|
-
f" --iam-account=
|
147
|
+
f" {tmpdir}/{self._service_account_name}-key.json"
|
148
|
+
f" --iam-account={self._service_account_name}@{self._project}.iam.gserviceaccount.com"
|
139
149
|
)
|
140
150
|
except subprocess.CalledProcessError as e:
|
141
151
|
raise RuntimeError(
|
142
152
|
"Error creating service account key. Please ensure you have the"
|
143
153
|
" necessary permissions."
|
144
154
|
) from e
|
145
|
-
key = json.loads(
|
155
|
+
key = json.loads(
|
156
|
+
(Path(tmpdir) / f"{self._service_account_name}-key.json").read_text()
|
157
|
+
)
|
146
158
|
return key
|
147
159
|
|
148
160
|
async def _assign_roles(self):
|
149
161
|
try:
|
150
162
|
await self._run_command(
|
151
163
|
"gcloud projects add-iam-policy-binding"
|
152
|
-
f' {self._project} --member="serviceAccount:
|
164
|
+
f' {self._project} --member="serviceAccount:{self._service_account_name}@{self._project}.iam.gserviceaccount.com"'
|
153
165
|
' --role="roles/iam.serviceAccountUser"'
|
154
166
|
)
|
155
167
|
await self._run_command(
|
156
168
|
"gcloud projects add-iam-policy-binding"
|
157
|
-
f' {self._project} --member="serviceAccount:
|
169
|
+
f' {self._project} --member="serviceAccount:{self._service_account_name}@{self._project}.iam.gserviceaccount.com"'
|
158
170
|
' --role="roles/run.developer"'
|
159
171
|
)
|
160
172
|
except subprocess.CalledProcessError as e:
|
@@ -163,8 +175,47 @@ class CloudRunPushProvisioner:
|
|
163
175
|
" necessary permissions."
|
164
176
|
) from e
|
165
177
|
|
178
|
+
async def _enable_artifact_registry_api(self):
|
179
|
+
try:
|
180
|
+
await self._run_command(
|
181
|
+
"gcloud services enable artifactregistry.googleapis.com"
|
182
|
+
f" --project={self._project}"
|
183
|
+
)
|
184
|
+
except subprocess.CalledProcessError as e:
|
185
|
+
raise RuntimeError(
|
186
|
+
"Error enabling Artifact Registry API. Please ensure you have the"
|
187
|
+
" necessary permissions."
|
188
|
+
) from e
|
189
|
+
|
190
|
+
async def _create_artifact_registry_repository(self, repository_name: str):
|
191
|
+
try:
|
192
|
+
await self._run_command(
|
193
|
+
"gcloud artifacts repositories create"
|
194
|
+
f" {repository_name} --repository-format=docker"
|
195
|
+
f" --location={self._region} --project={self._project}"
|
196
|
+
)
|
197
|
+
except subprocess.CalledProcessError as e:
|
198
|
+
if "already exists" not in e.output.decode("utf-8"):
|
199
|
+
return
|
200
|
+
raise RuntimeError(
|
201
|
+
"Error creating Artifact Registry repository. Please ensure you have"
|
202
|
+
" the necessary permissions."
|
203
|
+
) from e
|
204
|
+
|
205
|
+
async def _login_to_artifact_registry(self):
|
206
|
+
try:
|
207
|
+
await self._run_command(
|
208
|
+
f"gcloud auth configure-docker {self._region}-docker.pkg.dev"
|
209
|
+
f" --project={self._project}"
|
210
|
+
)
|
211
|
+
except subprocess.CalledProcessError as e:
|
212
|
+
raise RuntimeError(
|
213
|
+
"Error logging into Artifact Registry. Please ensure you have the"
|
214
|
+
" necessary permissions."
|
215
|
+
) from e
|
216
|
+
|
166
217
|
async def _create_gcp_credentials_block(
|
167
|
-
self,
|
218
|
+
self, block_document_name: str, key: dict, client: PrefectClient
|
168
219
|
) -> UUID:
|
169
220
|
credentials_block_type = await client.read_block_type_by_slug("gcp-credentials")
|
170
221
|
|
@@ -177,7 +228,7 @@ class CloudRunPushProvisioner:
|
|
177
228
|
try:
|
178
229
|
block_doc = await client.create_block_document(
|
179
230
|
block_document=BlockDocumentCreate(
|
180
|
-
name=
|
231
|
+
name=block_document_name,
|
181
232
|
data={"service_account_info": key},
|
182
233
|
block_type_id=credentials_block_type.id,
|
183
234
|
block_schema_id=credentials_block_schema.id,
|
@@ -186,24 +237,13 @@ class CloudRunPushProvisioner:
|
|
186
237
|
return block_doc.id
|
187
238
|
except ObjectAlreadyExists:
|
188
239
|
block_doc = await client.read_block_document_by_name(
|
189
|
-
name=
|
240
|
+
name=block_document_name,
|
190
241
|
block_type_slug="gcp-credentials",
|
191
242
|
)
|
192
243
|
return block_doc.id
|
193
244
|
|
194
|
-
|
195
|
-
|
196
|
-
self,
|
197
|
-
work_pool_name: str,
|
198
|
-
base_job_template: dict,
|
199
|
-
client: Optional[PrefectClient] = None,
|
200
|
-
) -> Dict[str, Any]:
|
201
|
-
assert client, "Client injection failed"
|
202
|
-
await self._verify_gcloud_ready()
|
203
|
-
self._project = await self._get_project()
|
204
|
-
self._region = await self._get_default_region()
|
205
|
-
|
206
|
-
table = Panel(
|
245
|
+
async def _create_provision_table(self, work_pool_name: str, client: PrefectClient):
|
246
|
+
return Panel(
|
207
247
|
dedent(
|
208
248
|
f"""\
|
209
249
|
Provisioning infrastructure for your work pool [blue]{work_pool_name}[/] will require:
|
@@ -211,32 +251,120 @@ class CloudRunPushProvisioner:
|
|
211
251
|
Updates in GCP project [blue]{self._project}[/] in region [blue]{self._region}[/]
|
212
252
|
|
213
253
|
- Activate the Cloud Run API for your project
|
214
|
-
-
|
254
|
+
- Activate the Artifact Registry API for your project
|
255
|
+
- Create an Artifact Registry repository named [blue]{self._image_repository_name}[/]
|
256
|
+
- Create a service account for managing Cloud Run jobs: [blue]{self._service_account_name}[/]
|
215
257
|
- Service account will be granted the following roles:
|
216
258
|
- Service Account User
|
217
259
|
- Cloud Run Developer
|
218
|
-
- Create a key for service account [blue]
|
260
|
+
- Create a key for service account: [blue]{self._service_account_name}[/]
|
219
261
|
|
220
262
|
Updates in Prefect {"workspace" if client.server_type == ServerType.CLOUD else "server"}
|
221
263
|
|
222
|
-
- Create GCP credentials block
|
223
|
-
|
264
|
+
- Create GCP credentials block to store the service account key: [blue]{self._credentials_block_name}[/]
|
265
|
+
"""
|
224
266
|
),
|
225
267
|
expand=False,
|
226
268
|
)
|
269
|
+
|
270
|
+
async def _customize_resource_names(
|
271
|
+
self, work_pool_name: str, client: PrefectClient
|
272
|
+
) -> bool:
|
273
|
+
self._service_account_name = prompt(
|
274
|
+
"Please enter a name for the service account",
|
275
|
+
default=self._service_account_name,
|
276
|
+
)
|
277
|
+
self._credentials_block_name = prompt(
|
278
|
+
"Please enter a name for the GCP credentials block",
|
279
|
+
default=self._credentials_block_name,
|
280
|
+
)
|
281
|
+
self._image_repository_name = prompt(
|
282
|
+
"Please enter a name for the Artifact Registry repository",
|
283
|
+
default=self._image_repository_name,
|
284
|
+
)
|
285
|
+
table = await self._create_provision_table(work_pool_name, client)
|
286
|
+
self._console.print(table)
|
287
|
+
|
288
|
+
return Confirm.ask(
|
289
|
+
"Proceed with infrastructure provisioning?", console=self._console
|
290
|
+
)
|
291
|
+
|
292
|
+
@inject_client
|
293
|
+
async def provision(
|
294
|
+
self,
|
295
|
+
work_pool_name: str,
|
296
|
+
base_job_template: dict,
|
297
|
+
client: Optional[PrefectClient] = None,
|
298
|
+
) -> Dict[str, Any]:
|
299
|
+
assert client, "Client injection failed"
|
300
|
+
await self._verify_gcloud_ready()
|
301
|
+
self._project = await self._get_project()
|
302
|
+
self._region = await self._get_default_region()
|
303
|
+
self._credentials_block_name = f"{work_pool_name}-push-pool-credentials"
|
304
|
+
|
305
|
+
table = await self._create_provision_table(work_pool_name, client)
|
227
306
|
self._console.print(table)
|
228
307
|
if self._console.is_interactive:
|
229
|
-
|
230
|
-
|
308
|
+
chosen_option = prompt_select_from_table(
|
309
|
+
self._console,
|
310
|
+
"Proceed with infrastructure provisioning with default resource names?",
|
311
|
+
[
|
312
|
+
{"header": "Options:", "key": "option"},
|
313
|
+
],
|
314
|
+
[
|
315
|
+
{
|
316
|
+
"option": (
|
317
|
+
"Yes, proceed with infrastructure provisioning with default"
|
318
|
+
" resource names"
|
319
|
+
)
|
320
|
+
},
|
321
|
+
{"option": "Customize resource names"},
|
322
|
+
{"option": "Do not proceed with infrastructure provisioning"},
|
323
|
+
],
|
324
|
+
)
|
325
|
+
if chosen_option["option"] == "Customize resource names":
|
326
|
+
if not await self._customize_resource_names(work_pool_name, client):
|
327
|
+
return base_job_template
|
328
|
+
|
329
|
+
elif (
|
330
|
+
chosen_option["option"]
|
331
|
+
== "Do not proceed with infrastructure provisioning"
|
231
332
|
):
|
232
333
|
return base_job_template
|
334
|
+
elif (
|
335
|
+
chosen_option["option"]
|
336
|
+
!= "Yes, proceed with infrastructure provisioning with default"
|
337
|
+
" resource names"
|
338
|
+
):
|
339
|
+
# basically, we should never hit this. i'm concerned that we might change
|
340
|
+
# the options in the future and forget to update this check
|
341
|
+
raise ValueError(f"Invalid option selected: {chosen_option['option']}")
|
233
342
|
|
234
343
|
with Progress(console=self._console) as progress:
|
235
|
-
task = progress.add_task("Provisioning Infrastructure", total=
|
344
|
+
task = progress.add_task("Provisioning Infrastructure", total=9)
|
236
345
|
progress.console.print("Activating Cloud Run API")
|
237
346
|
await self._enable_cloud_run_api()
|
238
347
|
progress.advance(task)
|
239
348
|
|
349
|
+
progress.console.print("Activating Artifact Registry API")
|
350
|
+
await self._enable_artifact_registry_api()
|
351
|
+
progress.advance(task)
|
352
|
+
|
353
|
+
progress.console.print("Creating Artifact Registry repository")
|
354
|
+
await self._create_artifact_registry_repository(self._image_repository_name)
|
355
|
+
progress.advance(task)
|
356
|
+
|
357
|
+
progress.console.print("Configuring authentication to Artifact Registry")
|
358
|
+
await self._login_to_artifact_registry()
|
359
|
+
progress.advance(task)
|
360
|
+
|
361
|
+
progress.console.print("Setting default Docker build namespace")
|
362
|
+
default_docker_build_namespace = f"{self._region}-docker.pkg.dev/{self._project}/{self._image_repository_name}"
|
363
|
+
update_current_profile(
|
364
|
+
{PREFECT_DEFAULT_DOCKER_BUILD_NAMESPACE: default_docker_build_namespace}
|
365
|
+
)
|
366
|
+
progress.advance(task)
|
367
|
+
|
240
368
|
progress.console.print("Creating service account")
|
241
369
|
await self._create_service_account()
|
242
370
|
progress.advance(task)
|
@@ -251,7 +379,7 @@ class CloudRunPushProvisioner:
|
|
251
379
|
|
252
380
|
progress.console.print("Creating GCP credentials block")
|
253
381
|
block_doc_id = await self._create_gcp_credentials_block(
|
254
|
-
|
382
|
+
self._credentials_block_name, key, client
|
255
383
|
)
|
256
384
|
base_job_template_copy = deepcopy(base_job_template)
|
257
385
|
base_job_template_copy["variables"]["properties"]["credentials"][
|
@@ -259,6 +387,44 @@ class CloudRunPushProvisioner:
|
|
259
387
|
] = {"$ref": {"block_document_id": str(block_doc_id)}}
|
260
388
|
progress.advance(task)
|
261
389
|
|
390
|
+
self._console.print(
|
391
|
+
dedent(
|
392
|
+
f"""\
|
393
|
+
Your default Docker build namespace has been set to [blue]{default_docker_build_namespace!r}[/].
|
394
|
+
Use any image name to build and push to this registry by default:
|
395
|
+
"""
|
396
|
+
),
|
397
|
+
Panel(
|
398
|
+
Syntax(
|
399
|
+
dedent(
|
400
|
+
f"""\
|
401
|
+
from prefect import flow
|
402
|
+
from prefect.deployments import DeploymentImage
|
403
|
+
|
404
|
+
|
405
|
+
@flow(log_prints=True)
|
406
|
+
def my_flow(name: str = "world"):
|
407
|
+
print(f"Hello {{name}}! I'm a flow running in Cloud Run!")
|
408
|
+
|
409
|
+
|
410
|
+
if __name__ == "__main__":
|
411
|
+
my_flow.deploy(
|
412
|
+
name="my-deployment",
|
413
|
+
work_pool_name="{work_pool_name}",
|
414
|
+
image=DeploymentImage(
|
415
|
+
name="my-image:latest",
|
416
|
+
platform="linux/amd64",
|
417
|
+
)
|
418
|
+
)"""
|
419
|
+
),
|
420
|
+
"python",
|
421
|
+
background_color="default",
|
422
|
+
),
|
423
|
+
title="example_deploy_script.py",
|
424
|
+
expand=False,
|
425
|
+
),
|
426
|
+
)
|
427
|
+
|
262
428
|
self._console.print(
|
263
429
|
(
|
264
430
|
f"Infrastructure successfully provisioned for '{work_pool_name}' work"
|