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.
- py_serverless_kit/__init__.py +47 -0
- py_serverless_kit/__main__.py +3 -0
- py_serverless_kit/_version.py +2 -0
- py_serverless_kit/cli/__init__.py +0 -0
- py_serverless_kit/cli/main.py +491 -0
- py_serverless_kit/cli/templates.py +212 -0
- py_serverless_kit/config/__init__.py +12 -0
- py_serverless_kit/config/loader.py +316 -0
- py_serverless_kit/config/schema.py +457 -0
- py_serverless_kit/core/__init__.py +0 -0
- py_serverless_kit/core/deployer.py +180 -0
- py_serverless_kit/core/describe.py +112 -0
- py_serverless_kit/core/project.py +147 -0
- py_serverless_kit/dashboard/__init__.py +3 -0
- py_serverless_kit/dashboard/generator.py +65 -0
- py_serverless_kit/dashboard/template.html +248 -0
- py_serverless_kit/local/__init__.py +3 -0
- py_serverless_kit/local/runner.py +234 -0
- py_serverless_kit/local/server.py +200 -0
- py_serverless_kit/packaging/__init__.py +3 -0
- py_serverless_kit/packaging/builder.py +252 -0
- py_serverless_kit/providers/__init__.py +15 -0
- py_serverless_kit/providers/alibaba.py +268 -0
- py_serverless_kit/providers/aws.py +508 -0
- py_serverless_kit/providers/azure.py +417 -0
- py_serverless_kit/providers/base.py +310 -0
- py_serverless_kit/providers/cloudflare.py +264 -0
- py_serverless_kit/providers/digitalocean.py +211 -0
- py_serverless_kit/providers/gcp.py +480 -0
- py_serverless_kit/providers/huawei.py +292 -0
- py_serverless_kit/providers/ibm.py +132 -0
- py_serverless_kit/providers/oracle.py +304 -0
- py_serverless_kit/providers/registry.py +60 -0
- py_serverless_kit/providers/schedule.py +153 -0
- py_serverless_kit/providers/tencent.py +275 -0
- py_serverless_kit/providers/terraform_doc.py +125 -0
- py_serverless_kit/runtime/__init__.py +6 -0
- py_serverless_kit/runtime/adapters/__init__.py +18 -0
- py_serverless_kit/runtime/adapters/_httpfn.py +91 -0
- py_serverless_kit/runtime/adapters/alibaba.py +147 -0
- py_serverless_kit/runtime/adapters/aws.py +201 -0
- py_serverless_kit/runtime/adapters/azure.py +185 -0
- py_serverless_kit/runtime/adapters/base.py +86 -0
- py_serverless_kit/runtime/adapters/cloudflare.py +126 -0
- py_serverless_kit/runtime/adapters/digitalocean.py +74 -0
- py_serverless_kit/runtime/adapters/gcp.py +176 -0
- py_serverless_kit/runtime/adapters/huawei.py +173 -0
- py_serverless_kit/runtime/adapters/ibm.py +91 -0
- py_serverless_kit/runtime/adapters/oracle.py +161 -0
- py_serverless_kit/runtime/adapters/tencent.py +151 -0
- py_serverless_kit/runtime/batch.py +47 -0
- py_serverless_kit/runtime/context.py +45 -0
- py_serverless_kit/runtime/dispatch.py +277 -0
- py_serverless_kit/runtime/errors.py +83 -0
- py_serverless_kit/runtime/event.py +267 -0
- py_serverless_kit/runtime/invoke.py +119 -0
- py_serverless_kit/runtime/log.py +69 -0
- py_serverless_kit/runtime/manifest.py +121 -0
- py_serverless_kit/runtime/response.py +177 -0
- py_serverless_kit/runtime/routing.py +74 -0
- py_serverless_kit/runtime/workflow.py +312 -0
- py_serverless_kit/terraform/__init__.py +3 -0
- py_serverless_kit/terraform/runner.py +104 -0
- py_serverless_kit/workflows/__init__.py +35 -0
- py_serverless_kit/workflows/asl.py +170 -0
- py_serverless_kit/workflows/gcp.py +328 -0
- py_serverless_kit-0.1.0.dist-info/METADATA +300 -0
- py_serverless_kit-0.1.0.dist-info/RECORD +71 -0
- py_serverless_kit-0.1.0.dist-info/WHEEL +4 -0
- py_serverless_kit-0.1.0.dist-info/entry_points.txt +3 -0
- 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
|