py-serverless-kit 0.1.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 (71) hide show
  1. py_serverless_kit/__init__.py +47 -0
  2. py_serverless_kit/__main__.py +3 -0
  3. py_serverless_kit/_version.py +2 -0
  4. py_serverless_kit/cli/__init__.py +0 -0
  5. py_serverless_kit/cli/main.py +491 -0
  6. py_serverless_kit/cli/templates.py +212 -0
  7. py_serverless_kit/config/__init__.py +12 -0
  8. py_serverless_kit/config/loader.py +316 -0
  9. py_serverless_kit/config/schema.py +457 -0
  10. py_serverless_kit/core/__init__.py +0 -0
  11. py_serverless_kit/core/deployer.py +180 -0
  12. py_serverless_kit/core/describe.py +112 -0
  13. py_serverless_kit/core/project.py +147 -0
  14. py_serverless_kit/dashboard/__init__.py +3 -0
  15. py_serverless_kit/dashboard/generator.py +65 -0
  16. py_serverless_kit/dashboard/template.html +248 -0
  17. py_serverless_kit/local/__init__.py +3 -0
  18. py_serverless_kit/local/runner.py +234 -0
  19. py_serverless_kit/local/server.py +200 -0
  20. py_serverless_kit/packaging/__init__.py +3 -0
  21. py_serverless_kit/packaging/builder.py +252 -0
  22. py_serverless_kit/providers/__init__.py +15 -0
  23. py_serverless_kit/providers/alibaba.py +268 -0
  24. py_serverless_kit/providers/aws.py +508 -0
  25. py_serverless_kit/providers/azure.py +417 -0
  26. py_serverless_kit/providers/base.py +310 -0
  27. py_serverless_kit/providers/cloudflare.py +264 -0
  28. py_serverless_kit/providers/digitalocean.py +211 -0
  29. py_serverless_kit/providers/gcp.py +480 -0
  30. py_serverless_kit/providers/huawei.py +292 -0
  31. py_serverless_kit/providers/ibm.py +132 -0
  32. py_serverless_kit/providers/oracle.py +304 -0
  33. py_serverless_kit/providers/registry.py +60 -0
  34. py_serverless_kit/providers/schedule.py +153 -0
  35. py_serverless_kit/providers/tencent.py +275 -0
  36. py_serverless_kit/providers/terraform_doc.py +125 -0
  37. py_serverless_kit/runtime/__init__.py +6 -0
  38. py_serverless_kit/runtime/adapters/__init__.py +18 -0
  39. py_serverless_kit/runtime/adapters/_httpfn.py +91 -0
  40. py_serverless_kit/runtime/adapters/alibaba.py +147 -0
  41. py_serverless_kit/runtime/adapters/aws.py +201 -0
  42. py_serverless_kit/runtime/adapters/azure.py +185 -0
  43. py_serverless_kit/runtime/adapters/base.py +86 -0
  44. py_serverless_kit/runtime/adapters/cloudflare.py +126 -0
  45. py_serverless_kit/runtime/adapters/digitalocean.py +74 -0
  46. py_serverless_kit/runtime/adapters/gcp.py +176 -0
  47. py_serverless_kit/runtime/adapters/huawei.py +173 -0
  48. py_serverless_kit/runtime/adapters/ibm.py +91 -0
  49. py_serverless_kit/runtime/adapters/oracle.py +161 -0
  50. py_serverless_kit/runtime/adapters/tencent.py +151 -0
  51. py_serverless_kit/runtime/batch.py +47 -0
  52. py_serverless_kit/runtime/context.py +45 -0
  53. py_serverless_kit/runtime/dispatch.py +277 -0
  54. py_serverless_kit/runtime/errors.py +83 -0
  55. py_serverless_kit/runtime/event.py +267 -0
  56. py_serverless_kit/runtime/invoke.py +119 -0
  57. py_serverless_kit/runtime/log.py +69 -0
  58. py_serverless_kit/runtime/manifest.py +121 -0
  59. py_serverless_kit/runtime/response.py +177 -0
  60. py_serverless_kit/runtime/routing.py +74 -0
  61. py_serverless_kit/runtime/workflow.py +312 -0
  62. py_serverless_kit/terraform/__init__.py +3 -0
  63. py_serverless_kit/terraform/runner.py +104 -0
  64. py_serverless_kit/workflows/__init__.py +35 -0
  65. py_serverless_kit/workflows/asl.py +170 -0
  66. py_serverless_kit/workflows/gcp.py +328 -0
  67. py_serverless_kit-0.1.0.dist-info/METADATA +300 -0
  68. py_serverless_kit-0.1.0.dist-info/RECORD +71 -0
  69. py_serverless_kit-0.1.0.dist-info/WHEEL +4 -0
  70. py_serverless_kit-0.1.0.dist-info/entry_points.txt +3 -0
  71. py_serverless_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,480 @@
1
+ """GCP: Cloud Functions (2nd gen) + Cloud Scheduler + Pub/Sub + Cloud Storage + Cloud Workflows.
2
+
3
+ Cloud Functions gen2 allows a single ``event_trigger`` per function and an
4
+ event-triggered function cannot serve plain HTTP. To keep "change only the
5
+ provider in YAML" true, each *logical* function is deployed as one or more
6
+ Cloud Functions sharing the same bundle and handler (``PSKIT_FUNCTION`` is the
7
+ logical name in all of them):
8
+
9
+ * ``<name>`` - HTTP deployment (entry ``pskit_http``). Created when the
10
+ function has ``http`` events, no event triggers at all, is a workflow task (Cloud
11
+ Workflows needs a URL), or has schedules but no Pub/Sub trigger to publish to.
12
+ * ``<name>-<kind><i>`` - one per event trigger (entry ``pskit_event``), e.g.
13
+ ``worker-queue0``, ``worker-storage0``, ``worker-topic0``. A storage event listing
14
+ both ``created`` and ``deleted`` becomes two deployments. When a function has
15
+ exactly one trigger and needs no HTTP deployment it is simply named ``<name>``.
16
+
17
+ Triggers:
18
+
19
+ * ``queue`` / ``topic`` -> Pub/Sub ``messagePublished`` (GCP has no push queue like SQS,
20
+ so a queue is a Pub/Sub topic too).
21
+ * ``storage`` -> Cloud Storage ``finalized`` / ``deleted``. Prefix / suffix
22
+ filters are not supported by Eventarc, so the runtime applies them.
23
+ * ``schedule`` -> Cloud Scheduler: POSTs the ``__pskit`` envelope to the HTTP
24
+ deployment with an OIDC token, or - when there is no HTTP deployment - publishes the
25
+ envelope to the function's first Pub/Sub topic.
26
+
27
+ A dedicated *invoker* service account is created and granted ``roles/run.invoker`` on
28
+ every deployment; Cloud Scheduler, Eventarc and Cloud Workflows all authenticate with it.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import base64
34
+ import json
35
+ import re
36
+ from dataclasses import dataclass, field
37
+ from typing import Any, Dict, List, Optional, Set
38
+
39
+ from ..config.schema import FunctionConfig, ServiceConfig
40
+ from ..runtime.adapters.base import envelope
41
+ from ..workflows import definition_dict
42
+ from ..workflows.gcp import compile_gcp
43
+ from . import schedule as sched
44
+ from .base import HTTP, QUEUE, SCHEDULE, STORAGE, TOPIC, WORKFLOW_NATIVE, Issue, Provider, RenderContext, stable_suffix
45
+ from .terraform_doc import TerraformDoc, escape, ref, tf_id
46
+
47
+ FN = "google_cloudfunctions2_function"
48
+ PUBSUB_EVENT = "google.cloud.pubsub.topic.v1.messagePublished"
49
+ STORAGE_EVENTS = {
50
+ "created": "google.cloud.storage.object.v1.finalized",
51
+ "deleted": "google.cloud.storage.object.v1.deleted",
52
+ }
53
+ EVENT_MAX_TIMEOUT = 540
54
+ TRIGGERS = ("queue", "topic", "storage")
55
+
56
+
57
+ def _label(text: str) -> str:
58
+ value = re.sub(r"[^a-z0-9_-]", "_", str(text).lower())[:63]
59
+ return value
60
+
61
+
62
+ def labels(tags: Dict[str, str]) -> Dict[str, str]:
63
+ out = {}
64
+ for key, value in tags.items():
65
+ k = _label(key)
66
+ if not k or not k[0].isalpha():
67
+ k = "k" + k
68
+ out[k[:63]] = _label(value)
69
+ return out
70
+
71
+
72
+ def _sa_id(prefix: str, suffix: str) -> str:
73
+ base = re.sub(r"[^a-z0-9-]", "-", prefix.lower())
74
+ base = base[: 30 - len(suffix) - 1].strip("-")
75
+ if not base or not base[0].isalpha():
76
+ base = "p" + base
77
+ return f"{base}-{suffix}"[:30]
78
+
79
+
80
+ @dataclass
81
+ class Deployment:
82
+ """One Cloud Function backing (part of) a logical function."""
83
+
84
+ key: str # logical name or "<name>-<kind><i>"; also the terraform label
85
+ kind: str # "http" | "queue" | "topic" | "storage"
86
+ config: Any = None # trigger event config
87
+ action: Optional[str] = None # storage: created | deleted
88
+ schedules: List[Any] = field(default_factory=list)
89
+ pubsub_schedules: bool = False # schedules publish to this deployment's topic
90
+
91
+
92
+ def plan_deployments(name: str, fn: FunctionConfig, needs_http: bool = False) -> List[Deployment]:
93
+ """Split a logical function into Cloud Functions (see module docstring)."""
94
+ triggers: List[Deployment] = []
95
+ counters: Dict[str, int] = {}
96
+ for ev in fn.events:
97
+ if ev.type not in TRIGGERS:
98
+ continue
99
+ actions = list(ev.config.events) if ev.type == "storage" else [None]
100
+ for action in actions:
101
+ i = counters.get(ev.type, 0)
102
+ counters[ev.type] = i + 1
103
+ triggers.append(Deployment(f"{name}-{ev.type}{i}", ev.type, ev.config, action))
104
+ schedules = fn.events_of("schedule")
105
+ pubsub = next((d for d in triggers if d.kind in ("queue", "topic")), None)
106
+ want_http = bool(fn.events_of("http")) or not triggers or needs_http or bool(schedules and pubsub is None)
107
+ out: List[Deployment] = []
108
+ if want_http:
109
+ out.append(Deployment(name, "http", schedules=schedules))
110
+ elif schedules and pubsub is not None:
111
+ pubsub.schedules = schedules
112
+ pubsub.pubsub_schedules = True
113
+ if len(triggers) == 1 and not want_http:
114
+ triggers[0].key = name
115
+ return out + triggers
116
+
117
+
118
+ def task_functions(config: ServiceConfig) -> Set[str]:
119
+ return {f for wf in config.workflows.values() for f in wf.task_functions()}
120
+
121
+
122
+ class GcpProvider(Provider):
123
+ name = "gcp"
124
+ display_name = "Google Cloud Functions"
125
+ tier = "stable"
126
+ terraform_source = "hashicorp/google"
127
+ terraform_version = ">= 5.20"
128
+ terraform_local_name = "google"
129
+ default_region = "us-central1"
130
+ capabilities = frozenset({HTTP, SCHEDULE, QUEUE, STORAGE, TOPIC, WORKFLOW_NATIVE})
131
+ max_name_length = 63
132
+ name_pattern = r"[^a-z0-9-]"
133
+ runtimes = {f"python3.{v}": f"python3{v}" for v in (9, 10, 11, 12, 13)}
134
+ vendor_dependencies = False
135
+ extra_requirements = ["functions-framework>=3.5"]
136
+ max_timeout = 3600
137
+ notes = "Cloud Functions gen2 (Cloud Run). Requires options.project. Queues map to Pub/Sub topics."
138
+
139
+ # ------------------------------------------------------------------ bundle
140
+ def shim_files(self, config: ServiceConfig) -> Dict[str, str]:
141
+ return {
142
+ "main.py": (
143
+ "# Generated by py-serverless-kit. Entry points: pskit_http, pskit_event\n"
144
+ "import functions_framework\n\n"
145
+ "from py_serverless_kit.runtime.adapters import gcp as _pskit\n\n\n"
146
+ "@functions_framework.http\n"
147
+ "def pskit_http(request):\n"
148
+ " return _pskit.http(request)\n\n\n"
149
+ "@functions_framework.cloud_event\n"
150
+ "def pskit_event(cloud_event):\n"
151
+ " _pskit.cloud_event(cloud_event)\n"
152
+ )
153
+ }
154
+
155
+ def provider_block(self, ctx: RenderContext) -> Dict[str, Any]:
156
+ block: Dict[str, Any] = {"region": ctx.region}
157
+ if ctx.opt("project"):
158
+ block["project"] = ctx.opt("project")
159
+ return block
160
+
161
+ def extra_validation(self, config: ServiceConfig) -> List[Issue]:
162
+ issues: List[Issue] = []
163
+ if not config.provider.options.get("project"):
164
+ issues.append(Issue("error", "GCP needs provider.options.project (the GCP project id)", "provider.options"))
165
+ if config.provider.iam.statements:
166
+ issues.append(
167
+ Issue(
168
+ "warning",
169
+ "iam.statements are AWS-style and ignored on GCP; grant roles to options.service_account instead",
170
+ "provider.iam",
171
+ )
172
+ )
173
+ for name, fn in config.functions.items():
174
+ where = f"functions.{name}"
175
+ triggers = [e for e in fn.events if e.type in TRIGGERS]
176
+ for ev in fn.events_of("queue"):
177
+ if ev.dead_letter:
178
+ issues.append(
179
+ Issue(
180
+ "warning",
181
+ "queue.dead_letter is not applied on GCP (Eventarc manages the subscription)",
182
+ where,
183
+ )
184
+ )
185
+ if ev.fifo:
186
+ issues.append(
187
+ Issue("warning", "queue.fifo is not supported by Pub/Sub triggers and is ignored", where)
188
+ )
189
+ for ev in fn.events_of("http"):
190
+ if ev.auth in ("jwt", "key"):
191
+ issues.append(
192
+ Issue("error", f"http auth {ev.auth!r} is not supported on GCP; use none or iam", where)
193
+ )
194
+ if triggers and config.timeout_of(fn) > EVENT_MAX_TIMEOUT:
195
+ issues.append(
196
+ Issue("error", f"event-triggered functions are limited to {EVENT_MAX_TIMEOUT}s on GCP", where)
197
+ )
198
+ return issues
199
+
200
+ # ------------------------------------------------------------------ render
201
+ def render_resources(self, doc: TerraformDoc, ctx: RenderContext) -> None:
202
+ cfg = ctx.config
203
+ project = ctx.opt("project", "")
204
+ region = ctx.region
205
+
206
+ src_bucket = doc.resource(
207
+ "google_storage_bucket",
208
+ "source",
209
+ {
210
+ "name": f"{ctx.prefix[:45]}-src-{stable_suffix(project, ctx.prefix, length=8)}",
211
+ "location": region.upper(),
212
+ "uniform_bucket_level_access": True,
213
+ "force_destroy": True,
214
+ "labels": labels(ctx.tags()),
215
+ },
216
+ )
217
+ src_obj = doc.resource(
218
+ "google_storage_bucket_object",
219
+ "source",
220
+ {
221
+ "name": f"source-{ctx.artifact.sha256[:16]}.zip",
222
+ "bucket": ref(src_bucket, "name"),
223
+ "source": "${path.module}/artifact.zip",
224
+ },
225
+ )
226
+ invoker = doc.resource(
227
+ "google_service_account",
228
+ "invoker",
229
+ {
230
+ "account_id": _sa_id(ctx.prefix, "inv"),
231
+ "display_name": f"pskit invoker for {ctx.prefix}",
232
+ },
233
+ )
234
+ invoker_email = ref(invoker, "email")
235
+ has_storage = any(fn.events_of("storage") for fn in cfg.functions.values())
236
+ tasks = task_functions(cfg)
237
+ plans = {name: plan_deployments(name, fn, name in tasks) for name, fn in ctx.function_items()}
238
+ has_trigger = any(d.kind != "http" for deps in plans.values() for d in deps)
239
+ depends: List[str] = []
240
+ if has_storage:
241
+ gcs = doc.data("google_storage_project_service_account", "gcs", {"project": project} if project else {})
242
+ depends.append(
243
+ doc.resource(
244
+ "google_project_iam_member",
245
+ "gcs-pubsub",
246
+ {
247
+ "project": project,
248
+ "role": "roles/pubsub.publisher",
249
+ "member": "serviceAccount:" + ref(gcs, "email_address"),
250
+ },
251
+ )
252
+ )
253
+ if has_trigger:
254
+ depends.append(
255
+ doc.resource(
256
+ "google_project_iam_member",
257
+ "invoker-eventarc",
258
+ {
259
+ "project": project,
260
+ "role": "roles/eventarc.eventReceiver",
261
+ "member": "serviceAccount:" + invoker_email,
262
+ },
263
+ )
264
+ )
265
+
266
+ fn_out: Dict[str, Dict[str, Any]] = {}
267
+ urls: Dict[str, str] = {}
268
+ for name, fn in ctx.function_items():
269
+ deployments = plans[name]
270
+ for ev in fn.events_of("topic"):
271
+ if ev.filter:
272
+ doc.warnings.append(
273
+ f"GCP: topic filter on {name!r} ({ev.name}) is not applied by Pub/Sub push "
274
+ "triggers; filter the records in your handler"
275
+ )
276
+ if len(deployments) > 1:
277
+ doc.warnings.append(
278
+ f"GCP: function {name!r} is deployed as {len(deployments)} Cloud Functions: "
279
+ + ", ".join(ctx.physical_name(d.key) for d in deployments)
280
+ )
281
+ ids: List[str] = []
282
+ out: Dict[str, Any] = {}
283
+ for dep in deployments:
284
+ res = self._function(doc, ctx, name, fn, dep, src_bucket, src_obj, invoker_email, depends)
285
+ ids.append(ref(res, "name"))
286
+ service = ref(res, "service_config[0].service")
287
+ doc.resource(
288
+ "google_cloud_run_service_iam_member",
289
+ f"{dep.key}-invoker",
290
+ {
291
+ "location": region,
292
+ "service": service,
293
+ "role": "roles/run.invoker",
294
+ "member": "serviceAccount:" + invoker_email,
295
+ },
296
+ )
297
+ if dep.kind == "http":
298
+ urls[name] = ref(res, "url")
299
+ if any(ev.auth == "none" for ev in fn.events_of("http")):
300
+ doc.resource(
301
+ "google_cloud_run_service_iam_member",
302
+ f"{dep.key}-public",
303
+ {"location": region, "service": service, "role": "roles/run.invoker", "member": "allUsers"},
304
+ )
305
+ if fn.events_of("http"):
306
+ out["url"] = ref(res, "url")
307
+ out["base_url"] = ref(res, "url")
308
+ for i, ev in enumerate(dep.schedules):
309
+ self._schedule(doc, ctx, name, fn, dep, i, ev, ref(res, "url"), invoker_email)
310
+ primary = deployments[0]
311
+ out["id"] = ids[0]
312
+ out["console"] = (
313
+ f"https://console.cloud.google.com/functions/details/{region}/"
314
+ f"{ctx.physical_name(primary.key)}?project={project}"
315
+ )
316
+ if len(ids) > 1:
317
+ out["extra"] = ids[1:]
318
+ fn_out[name] = out
319
+
320
+ wf_out = self._workflows(doc, ctx, urls, invoker_email)
321
+ doc.output(
322
+ "pskit",
323
+ {"functions": fn_out, "workflows": wf_out, "endpoint": None},
324
+ description="py-serverless-kit deployment summary",
325
+ )
326
+
327
+ # ------------------------------------------------------------------ pieces
328
+ def _topic(self, doc: TerraformDoc, ctx: RenderContext, name: str, existing: bool) -> str:
329
+ label = f"t-{name}"
330
+ if existing:
331
+ return doc.data("google_pubsub_topic", label, {"name": name})
332
+ if not doc.has_resource("google_pubsub_topic", label):
333
+ doc.resource("google_pubsub_topic", label, {"name": name, "labels": labels(ctx.tags())})
334
+ return f"google_pubsub_topic.{tf_id(label)}"
335
+
336
+ def _function(
337
+ self,
338
+ doc: TerraformDoc,
339
+ ctx: RenderContext,
340
+ name: str,
341
+ fn: FunctionConfig,
342
+ dep: Deployment,
343
+ src_bucket: str,
344
+ src_obj: str,
345
+ invoker_email: str,
346
+ depends: List[str],
347
+ ) -> str:
348
+ cfg = ctx.config
349
+ env = ctx.environment(name, fn) # PSKIT_FUNCTION stays the logical name
350
+ env["PSKIT_GCP_PROJECT"] = ctx.opt("project", "")
351
+ service_config: Dict[str, Any] = {
352
+ "available_memory": f"{cfg.memory_of(fn)}M",
353
+ "timeout_seconds": cfg.timeout_of(fn),
354
+ "ingress_settings": "ALLOW_ALL",
355
+ "all_traffic_on_latest_revision": True,
356
+ }
357
+ if fn.concurrency:
358
+ service_config["max_instance_count"] = fn.concurrency
359
+ if ctx.opt("service_account"):
360
+ service_config["service_account_email"] = ctx.opt("service_account")
361
+ body: Dict[str, Any] = {
362
+ "name": ctx.physical_name(dep.key),
363
+ "location": ctx.region,
364
+ "description": fn.description[:256],
365
+ "labels": labels(dict(ctx.tags(fn), **{"pskit-function": name})),
366
+ "build_config": {
367
+ "runtime": self.runtime_id(cfg.runtime_of(fn)),
368
+ "entry_point": "pskit_http" if dep.kind == "http" else "pskit_event",
369
+ "source": {"storage_source": {"bucket": ref(src_bucket, "name"), "object": ref(src_obj, "name")}},
370
+ },
371
+ "service_config": service_config,
372
+ }
373
+ if dep.kind != "http":
374
+ ev = dep.config
375
+ et: Dict[str, Any] = {
376
+ "trigger_region": ctx.region,
377
+ "retry_policy": "RETRY_POLICY_RETRY",
378
+ "service_account_email": invoker_email,
379
+ }
380
+ if dep.kind in ("queue", "topic"):
381
+ env["PSKIT_TRIGGER_KIND"] = dep.kind
382
+ et["event_type"] = PUBSUB_EVENT
383
+ et["pubsub_topic"] = ref(self._topic(doc, ctx, ev.name, ev.existing), "id")
384
+ else:
385
+ if ev.prefix:
386
+ env["PSKIT_STORAGE_PREFIX"] = ev.prefix
387
+ if ev.suffix:
388
+ env["PSKIT_STORAGE_SUFFIX"] = ev.suffix
389
+ bucket_name = ev.bucket
390
+ if not ev.existing:
391
+ label = f"b-{ev.bucket}"
392
+ if not doc.has_resource("google_storage_bucket", label):
393
+ doc.resource(
394
+ "google_storage_bucket",
395
+ label,
396
+ {
397
+ "name": ev.bucket,
398
+ "location": ctx.region.upper(),
399
+ "uniform_bucket_level_access": True,
400
+ "labels": labels(ctx.tags()),
401
+ },
402
+ )
403
+ bucket_name = ref("google_storage_bucket", tf_id(label), "name")
404
+ et["event_type"] = STORAGE_EVENTS[dep.action or "created"]
405
+ et["event_filters"] = [{"attribute": "bucket", "value": bucket_name}]
406
+ body["event_trigger"] = et
407
+ service_config["environment_variables"] = {k: escape(v) for k, v in env.items()}
408
+ if depends:
409
+ body["depends_on"] = list(depends)
410
+ body.update(fn.options)
411
+ return doc.resource(FN, dep.key, body)
412
+
413
+ def _schedule(
414
+ self,
415
+ doc: TerraformDoc,
416
+ ctx: RenderContext,
417
+ name: str,
418
+ fn: FunctionConfig,
419
+ dep: Deployment,
420
+ i: int,
421
+ ev: Any,
422
+ url: str,
423
+ invoker_email: str,
424
+ ) -> None:
425
+ payload = base64.b64encode(
426
+ json.dumps(envelope("schedule", ev.input, source="gcp.scheduler")).encode("utf-8")
427
+ ).decode("ascii")
428
+ body: Dict[str, Any] = {
429
+ "name": ctx.physical_name(f"{name}-{ev.name or i}"),
430
+ "region": ctx.region,
431
+ "schedule": sched.to_cron5(ev),
432
+ "time_zone": ev.timezone,
433
+ "paused": not ev.enabled,
434
+ }
435
+ if dep.pubsub_schedules: # no HTTP deployment: publish the envelope to the function's topic
436
+ topic = self._topic(doc, ctx, dep.config.name, dep.config.existing)
437
+ body["pubsub_target"] = {"topic_name": ref(topic, "id"), "data": payload}
438
+ else:
439
+ body["attempt_deadline"] = f"{min(1800, max(15, ctx.config.timeout_of(fn)))}s"
440
+ body["http_target"] = {
441
+ "http_method": "POST",
442
+ "uri": url,
443
+ "body": payload,
444
+ "headers": {"Content-Type": "application/json"},
445
+ "oidc_token": {"service_account_email": invoker_email, "audience": url},
446
+ }
447
+ doc.resource("google_cloud_scheduler_job", f"{name}-{i}", body)
448
+
449
+ def _workflows(
450
+ self, doc: TerraformDoc, ctx: RenderContext, urls: Dict[str, str], invoker_email: str
451
+ ) -> Dict[str, Any]:
452
+ out: Dict[str, Any] = {}
453
+ project = ctx.opt("project", "")
454
+ for name, wf in sorted(ctx.config.workflows.items()):
455
+ if ctx.workflow_engines.get(name) != "native":
456
+ out[name] = {"id": ref(FN, tf_id(f"{name}-orchestrator"), "name")}
457
+ continue
458
+ placeholders = {f: f"__PSKIT_URL_{i}__" for i, f in enumerate(sorted(urls))}
459
+ source, warnings = compile_gcp(definition_dict(wf), placeholders.__getitem__, name)
460
+ doc.warnings.extend(warnings)
461
+ text = escape(json.dumps(source, indent=2))
462
+ for f, token in placeholders.items():
463
+ text = text.replace(token, urls[f])
464
+ res = doc.resource(
465
+ "google_workflows_workflow",
466
+ f"wf-{name}",
467
+ {
468
+ "name": ctx.physical_name(name),
469
+ "region": ctx.region,
470
+ "description": wf.description[:256],
471
+ "service_account": invoker_email,
472
+ "labels": labels(ctx.tags()),
473
+ "source_contents": text,
474
+ },
475
+ )
476
+ out[name] = {
477
+ "id": ref(res, "id"),
478
+ "console": f"https://console.cloud.google.com/workflows/workflow/{ctx.region}/{ctx.physical_name(name)}?project={project}",
479
+ }
480
+ return out