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.
@@ -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 PREFECT_DEBUG_MODE
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("gcloud services enable run.googleapis.com")
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 prefect-cloud-run"
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}/prefect-cloud-run-key.json"
138
- f" --iam-account=prefect-cloud-run@{self._project}.iam.gserviceaccount.com"
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((Path(tmpdir) / "prefect-cloud-run-key.json").read_text())
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:prefect-cloud-run@{self._project}.iam.gserviceaccount.com"'
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:prefect-cloud-run@{self._project}.iam.gserviceaccount.com"'
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, work_pool_name: str, key: dict, client: PrefectClient
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=f"{work_pool_name}-push-pool-credentials",
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=f"{work_pool_name}-push-pool-credentials",
240
+ name=block_document_name,
190
241
  block_type_slug="gcp-credentials",
191
242
  )
192
243
  return block_doc.id
193
244
 
194
- @inject_client
195
- async def provision(
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
- - Create a service account for managing Cloud Run jobs: [blue]prefect-cloud-run[/]
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]prefect-cloud-run[/]
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 [blue]{work_pool_name}-push-pool-credentials[/] to store the service account key
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
- if not Confirm.ask(
230
- "Proceed with infrastructure provisioning?", console=self._console
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=5)
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
- work_pool_name, key, client
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"