metaflow 2.14.3__py2.py3-none-any.whl → 2.15.1__py2.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.
@@ -0,0 +1,620 @@
1
+ # Tilt configuration for running Metaflow on a local Kubernetes stack
2
+ #
3
+ # Usage:
4
+ # Start the development environment:
5
+ # $ tilt up
6
+ # Stop and clean up:
7
+ # $ tilt down
8
+
9
+ # TODO:
10
+ # 1. move away from temporary images
11
+ # 2. introduce kueue and jobsets
12
+ # 3. lock versions
13
+
14
+ version_settings(constraint='>=0.22.2')
15
+ allow_k8s_contexts('minikube')
16
+
17
+ components = {
18
+ "metadata-service": ["postgresql"],
19
+ "ui": ["postgresql", "minio"],
20
+ "minio": [],
21
+ "postgresql": [],
22
+ "argo-workflows": [],
23
+ "argo-events": ["argo-workflows"],
24
+ }
25
+
26
+ if os.getenv("SERVICES", "").strip():
27
+ requested_components = os.getenv("SERVICES", "").split(",")
28
+ else:
29
+ requested_components = list(components.keys())
30
+
31
+ metaflow_config = {}
32
+ metaflow_config["METAFLOW_KUBERNETES_NAMESPACE"] = "default"
33
+
34
+ aws_config = []
35
+
36
+ def write_config_files():
37
+ metaflow_json = encode_json(metaflow_config)
38
+ cmd = '''cat > .devtools/config_local.json <<EOF
39
+ %s
40
+ EOF
41
+ ''' % (metaflow_json)
42
+ if aws_config and aws_config.strip():
43
+ cmd += '''cat > .devtools/aws_config <<EOF
44
+ %s
45
+ EOF
46
+ ''' % (aws_config.strip())
47
+ return cmd
48
+
49
+ load('ext://helm_resource', 'helm_resource', 'helm_repo')
50
+ load('ext://helm_remote', 'helm_remote')
51
+
52
+
53
+ def resolve(component, resolved=None):
54
+ if resolved == None:
55
+ resolved = []
56
+ if component in resolved:
57
+ return resolved
58
+ if component in components:
59
+ for dep in components[component]:
60
+ resolve(dep, resolved)
61
+ resolved.append(component)
62
+ return resolved
63
+
64
+ valid_components = []
65
+ for component in components.keys():
66
+ if component not in valid_components:
67
+ valid_components.append(component)
68
+ for deps in components.values():
69
+ for dep in deps:
70
+ if dep not in valid_components:
71
+ valid_components.append(dep)
72
+
73
+ enabled_components = []
74
+ for component in requested_components:
75
+ if component not in valid_components:
76
+ fail("Unknown component: " + component)
77
+ for result in resolve(component):
78
+ if result not in enabled_components:
79
+ enabled_components.append(result)
80
+
81
+ # Print a friendly summary when running `tilt up`.
82
+ if config.tilt_subcommand == 'up':
83
+ print("\n📦 Components to install:")
84
+ for component in enabled_components:
85
+ print("• " + component)
86
+ if component in components and components[component]:
87
+ print(" ↳ requires: " + ", ".join(components[component]))
88
+
89
+ config_resources = []
90
+
91
+ #################################################
92
+ # MINIO
93
+ #################################################
94
+ if "minio" in enabled_components:
95
+ helm_remote(
96
+ 'minio',
97
+ repo_name='minio-s3',
98
+ repo_url='https://charts.min.io/',
99
+ set=[
100
+ 'rootUser=rootuser',
101
+ 'rootPassword=rootpass123',
102
+ # TODO: perturb the bucket name to avoid conflicts
103
+ 'buckets[0].name=metaflow-test',
104
+ 'buckets[0].policy=none',
105
+ 'buckets[0].purge=false',
106
+ 'mode=standalone',
107
+ 'replicas=1',
108
+ 'persistence.enabled=false',
109
+ 'resources.requests.memory=128Mi',
110
+ 'resources.requests.cpu=50m',
111
+ 'resources.limits.memory=256Mi',
112
+ 'resources.limits.cpu=100m',
113
+ ]
114
+ )
115
+
116
+ k8s_resource(
117
+ 'minio',
118
+ port_forwards=[
119
+ '9000:9000',
120
+ '9001:9001'
121
+ ],
122
+ links=[
123
+ link('http://localhost:9000', 'MinIO API'),
124
+ link('http://localhost:9001/login', 'MinIO Console (rootuser/rootpass123)')
125
+ ],
126
+ labels=['minio'],
127
+ )
128
+
129
+ k8s_resource(
130
+ "minio-post-job",
131
+ labels=['minio'],
132
+ )
133
+
134
+ k8s_yaml(encode_yaml({
135
+ 'apiVersion': 'v1',
136
+ 'kind': 'Secret',
137
+ 'metadata': {'name': 'minio-secret'},
138
+ 'type': 'Opaque',
139
+ 'stringData': {
140
+ 'AWS_ACCESS_KEY_ID': 'rootuser',
141
+ 'AWS_SECRET_ACCESS_KEY': 'rootpass123',
142
+ 'AWS_ENDPOINT_URL_S3': 'http://minio.default.svc.cluster.local:9000',
143
+ }
144
+ }))
145
+
146
+ metaflow_config["METAFLOW_DEFAULT_DATASTORE"] = "s3"
147
+ metaflow_config["METAFLOW_DATASTORE_SYSROOT_S3"] = "s3://metaflow-test/metaflow"
148
+ metaflow_config["METAFLOW_KUBERNETES_SECRETS"] = "minio-secret"
149
+
150
+ aws_config = """[default]
151
+ aws_access_key_id = rootuser
152
+ aws_secret_access_key = rootpass123
153
+ endpoint_url = http://localhost:9000
154
+ """
155
+ config_resources.append('minio')
156
+
157
+ #################################################
158
+ # POSTGRESQL
159
+ #################################################
160
+ if "postgresql" in enabled_components:
161
+ helm_remote(
162
+ 'postgresql',
163
+ version='12.5.6',
164
+ repo_name='postgresql',
165
+ repo_url='https://charts.bitnami.com/bitnami',
166
+ set=[
167
+ 'auth.username=metaflow',
168
+ 'auth.password=metaflow123',
169
+ 'auth.database=metaflow',
170
+ 'primary.persistence.enabled=false',
171
+ 'primary.resources.requests.memory=128Mi',
172
+ 'primary.resources.requests.cpu=50m',
173
+ 'primary.resources.limits.memory=256Mi',
174
+ 'primary.resources.limits.cpu=100m',
175
+ 'primary.terminationGracePeriodSeconds=1',
176
+ 'primary.podSecurityContext.enabled=false',
177
+ 'primary.containerSecurityContext.enabled=false',
178
+ 'volumePermissions.enabled=false',
179
+ 'shmVolume.enabled=false',
180
+ 'primary.extraVolumes=null',
181
+ 'primary.extraVolumeMounts=null'
182
+ ]
183
+ )
184
+
185
+ k8s_resource(
186
+ 'postgresql',
187
+ port_forwards=['5432:5432'],
188
+ links=[
189
+ link('postgresql://metaflow:metaflow@localhost:5432/metaflow', 'PostgreSQL Connection')
190
+ ],
191
+ labels=['postgresql'],
192
+ resource_deps=components['postgresql'],
193
+ )
194
+
195
+ config_resources.append('postgresql')
196
+
197
+ #################################################
198
+ # ARGO WORKFLOWS
199
+ #################################################
200
+ if "argo-workflows" in enabled_components:
201
+ helm_remote(
202
+ 'argo-workflows',
203
+ repo_name='argo',
204
+ repo_url='https://argoproj.github.io/argo-helm',
205
+ set=[
206
+ 'server.extraArgs[0]=--auth-mode=server',
207
+ 'workflow.serviceAccount.create=true',
208
+ 'workflow.rbac.create=true',
209
+ 'server.livenessProbe.initialDelaySeconds=1',
210
+ 'server.readinessProbe.initialDelaySeconds=1',
211
+ 'server.resources.requests.memory=128Mi',
212
+ 'server.resources.requests.cpu=50m',
213
+ 'server.resources.limits.memory=256Mi',
214
+ 'server.resources.limits.cpu=100m',
215
+ 'controller.resources.requests.memory=128Mi',
216
+ 'controller.resources.requests.cpu=50m',
217
+ 'controller.resources.limits.memory=256Mi',
218
+ 'controller.resources.limits.cpu=100m'
219
+ ]
220
+ )
221
+
222
+ k8s_yaml(encode_yaml({
223
+ 'apiVersion': 'rbac.authorization.k8s.io/v1',
224
+ 'kind': 'Role',
225
+ 'metadata': {
226
+ 'name': 'argo-workflowtaskresults-role',
227
+ 'namespace': 'default'
228
+ },
229
+ 'rules': [{
230
+ 'apiGroups': ['argoproj.io'],
231
+ 'resources': ['workflowtaskresults'],
232
+ 'verbs': ['create', 'patch', 'get', 'list']
233
+ }]
234
+ }))
235
+
236
+ k8s_yaml(encode_yaml({
237
+ 'apiVersion': 'rbac.authorization.k8s.io/v1',
238
+ 'kind': 'RoleBinding',
239
+ 'metadata': {
240
+ 'name': 'default-argo-workflowtaskresults-binding',
241
+ 'namespace': 'default'
242
+ },
243
+ 'subjects': [{
244
+ 'kind': 'ServiceAccount',
245
+ 'name': 'default',
246
+ 'namespace': 'default'
247
+ }],
248
+ 'roleRef': {
249
+ 'kind': 'Role',
250
+ 'name': 'argo-workflowtaskresults-role',
251
+ 'apiGroup': 'rbac.authorization.k8s.io'
252
+ }
253
+ }))
254
+
255
+ k8s_resource(
256
+ workload='argo-workflows-server',
257
+ port_forwards=['2746:2746'],
258
+ links=[
259
+ link('http://localhost:2746', 'Argo Workflows UI')
260
+ ],
261
+ labels=['argo-workflows'],
262
+ resource_deps=components['argo-workflows']
263
+ )
264
+
265
+ k8s_resource(
266
+ workload='argo-workflows-workflow-controller',
267
+ labels=['argo-workflows'],
268
+ resource_deps=components['argo-workflows']
269
+ )
270
+
271
+ config_resources.append('argo-workflows-workflow-controller')
272
+ config_resources.append('argo-workflows-server')
273
+
274
+ #################################################
275
+ # ARGO EVENTS
276
+ #################################################
277
+ if "argo-events" in enabled_components:
278
+ helm_remote(
279
+ 'argo-events',
280
+ repo_name='argo',
281
+ repo_url='https://argoproj.github.io/argo-helm',
282
+ set=[
283
+ 'crds.install=true',
284
+ 'controller.metrics.enabled=true',
285
+ 'controller.livenessProbe.initialDelaySeconds=1',
286
+ 'controller.readinessProbe.initialDelaySeconds=1',
287
+ 'controller.resources.requests.memory=64Mi',
288
+ 'controller.resources.requests.cpu=25m',
289
+ 'controller.resources.limits.memory=128Mi',
290
+ 'controller.resources.limits.cpu=50m',
291
+ 'configs.jetstream.streamConfig.maxAge=72h',
292
+ 'configs.jetstream.streamConfig.replicas=1',
293
+ 'controller.rbac.enabled=true',
294
+ 'controller.rbac.namespaced=false',
295
+ 'controller.serviceAccount.create=true',
296
+ 'controller.serviceAccount.name=argo-events-events-controller-sa',
297
+ 'configs.jetstream.versions[0].configReloaderImage=natsio/nats-server-config-reloader:latest',
298
+ 'configs.jetstream.versions[0].metricsExporterImage=natsio/prometheus-nats-exporter:latest',
299
+ 'configs.jetstream.versions[0].natsImage=nats:latest',
300
+ 'configs.jetstream.versions[0].startCommand=/nats-server',
301
+ 'configs.jetstream.versions[0].version=latest',
302
+ 'configs.jetstream.versions[1].configReloaderImage=natsio/nats-server-config-reloader:latest',
303
+ 'configs.jetstream.versions[1].metricsExporterImage=natsio/prometheus-nats-exporter:latest',
304
+ 'configs.jetstream.versions[1].natsImage=nats:2.9.15',
305
+ 'configs.jetstream.versions[1].startCommand=/nats-server',
306
+ 'configs.jetstream.versions[1].version=2.9.15',
307
+ ]
308
+ )
309
+
310
+ k8s_yaml(encode_yaml({
311
+ 'apiVersion': 'v1',
312
+ 'kind': 'ServiceAccount',
313
+ 'metadata': {
314
+ 'name': 'operate-workflow-sa',
315
+ 'namespace': 'default'
316
+ }
317
+ }))
318
+
319
+ k8s_yaml(encode_yaml({
320
+ 'apiVersion': 'rbac.authorization.k8s.io/v1',
321
+ 'kind': 'Role',
322
+ 'metadata': {
323
+ 'name': 'operate-workflow-role',
324
+ 'namespace': 'default'
325
+ },
326
+ 'rules': [{
327
+ 'apiGroups': ['argoproj.io'],
328
+ 'resources': [
329
+ 'workflows',
330
+ 'workflowtemplates',
331
+ 'cronworkflows',
332
+ 'clusterworkflowtemplates'
333
+ ],
334
+ 'verbs': ['*']
335
+ }]
336
+ }))
337
+
338
+ k8s_yaml(encode_yaml({
339
+ 'apiVersion': 'rbac.authorization.k8s.io/v1',
340
+ 'kind': 'RoleBinding',
341
+ 'metadata': {
342
+ 'name': 'operate-workflow-role-binding',
343
+ 'namespace': 'default'
344
+ },
345
+ 'roleRef': {
346
+ 'apiGroup': 'rbac.authorization.k8s.io',
347
+ 'kind': 'Role',
348
+ 'name': 'operate-workflow-role'
349
+ },
350
+ 'subjects': [{
351
+ 'kind': 'ServiceAccount',
352
+ 'name': 'operate-workflow-sa'
353
+ }]
354
+ }))
355
+
356
+ k8s_yaml(encode_yaml({
357
+ 'apiVersion': 'rbac.authorization.k8s.io/v1',
358
+ 'kind': 'Role',
359
+ 'metadata': {
360
+ 'name': 'view-events-role',
361
+ 'namespace': 'default'
362
+ },
363
+ 'rules': [{
364
+ 'apiGroups': ['argoproj.io'],
365
+ 'resources': [
366
+ 'eventsources',
367
+ 'eventbuses',
368
+ 'sensors'
369
+ ],
370
+ 'verbs': [
371
+ 'get',
372
+ 'list',
373
+ 'watch'
374
+ ]
375
+ }]
376
+ }))
377
+
378
+ k8s_yaml(encode_yaml({
379
+ 'apiVersion': 'rbac.authorization.k8s.io/v1',
380
+ 'kind': 'RoleBinding',
381
+ 'metadata': {
382
+ 'name': 'view-events-role-binding',
383
+ 'namespace': 'default'
384
+ },
385
+ 'roleRef': {
386
+ 'apiGroup': 'rbac.authorization.k8s.io',
387
+ 'kind': 'Role',
388
+ 'name': 'view-events-role'
389
+ },
390
+ 'subjects': [{
391
+ 'kind': 'ServiceAccount',
392
+ 'name': 'argo-workflows',
393
+ 'namespace': 'default'
394
+ }]
395
+ }))
396
+
397
+ k8s_yaml(encode_yaml({
398
+ 'apiVersion': 'argoproj.io/v1alpha1',
399
+ 'kind': 'EventBus',
400
+ 'metadata': {
401
+ 'name': 'default',
402
+ 'namespace': 'default'
403
+ },
404
+ 'spec': {
405
+ 'jetstream': {
406
+ 'version': '2.9.15',
407
+ 'replicas': 3,
408
+ 'containerTemplate': {
409
+ 'resources': {
410
+ 'limits': {
411
+ 'cpu': '100m',
412
+ 'memory': '128Mi'
413
+ },
414
+ 'requests': {
415
+ 'cpu': '100m',
416
+ 'memory': '128Mi'
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }))
423
+
424
+ k8s_yaml(encode_yaml({
425
+ 'apiVersion': 'argoproj.io/v1alpha1',
426
+ 'kind': 'EventSource',
427
+ 'metadata': {
428
+ 'name': 'argo-events-webhook',
429
+ 'namespace': 'default'
430
+ },
431
+ 'spec': {
432
+ 'template': {
433
+ 'container': {
434
+ 'resources': {
435
+ 'requests': {
436
+ 'cpu': '25m',
437
+ 'memory': '50Mi'
438
+ },
439
+ 'limits': {
440
+ 'cpu': '25m',
441
+ 'memory': '50Mi'
442
+ }
443
+ }
444
+ }
445
+ },
446
+ 'service': {
447
+ 'ports': [
448
+ {
449
+ 'port': 12000,
450
+ 'targetPort': 12000
451
+ }
452
+ ]
453
+ },
454
+ 'webhook': {
455
+ 'metaflow-event': {
456
+ 'port': '12000',
457
+ 'endpoint': '/metaflow-event',
458
+ 'method': 'POST'
459
+ }
460
+ }
461
+ }
462
+ }))
463
+
464
+ # Create a custom service and port-forward it because tilt :/
465
+ k8s_yaml(encode_yaml(
466
+ {
467
+ 'apiVersion': 'v1',
468
+ 'kind': 'Service',
469
+ 'metadata': {
470
+ 'name': 'argo-events-webhook-eventsource-svc-tilt',
471
+ 'namespace': 'default',
472
+ },
473
+ 'spec': {
474
+ 'ports': [{
475
+ 'port': 12000,
476
+ 'protocol': 'TCP',
477
+ 'targetPort': 12000
478
+ }],
479
+ 'selector': {
480
+ 'controller': 'eventsource-controller',
481
+ 'eventsource-name': 'argo-events-webhook',
482
+ 'owner-name': 'argo-events-webhook'
483
+ },
484
+ 'type': 'ClusterIP'
485
+ }
486
+ }
487
+ ))
488
+
489
+ local_resource(
490
+ name='argo-events-webhook-eventsource-svc',
491
+ serve_cmd='while ! kubectl get service/argo-events-webhook-eventsource-svc-tilt >/dev/null 2>&1 || ! kubectl get pods -l eventsource-name=argo-events-webhook -o jsonpath="{.items[*].status.phase}" | grep -q "Running"; do sleep 5; done && kubectl port-forward service/argo-events-webhook-eventsource-svc-tilt 12000:12000',
492
+ links=[
493
+ link('http://localhost:12000/metaflow-event', 'Argo Events Webhook'),
494
+ ],
495
+ labels=['argo-events']
496
+ )
497
+
498
+ k8s_resource(
499
+ 'argo-events-controller-manager',
500
+ labels=['argo-events'],
501
+ )
502
+
503
+ metaflow_config["METAFLOW_ARGO_EVENTS_EVENT"] = "metaflow-event"
504
+ metaflow_config["METAFLOW_ARGO_EVENTS_EVENT_BUS"] = "default"
505
+ metaflow_config["METAFLOW_ARGO_EVENTS_EVENT_SOURCE"] = "argo-events-webhook"
506
+ metaflow_config["METAFLOW_ARGO_EVENTS_SERVICE_ACCOUNT"] = "operate-workflow-sa"
507
+ metaflow_config["METAFLOW_ARGO_EVENTS_WEBHOOK_AUTH"] = "service"
508
+ metaflow_config["METAFLOW_ARGO_EVENTS_WEBHOOK_URL"] = "http://argo-events-webhook-eventsource-svc:12000/metaflow-event"
509
+
510
+ config_resources.append('argo-events-controller-manager')
511
+ config_resources.append('argo-events-webhook-eventsource-svc')
512
+
513
+ #################################################
514
+ # METADATA SERVICE
515
+ #################################################
516
+ if "metadata-service" in enabled_components:
517
+ helm_remote(
518
+ 'metaflow-service',
519
+ repo_name='metaflow-tools',
520
+ repo_url='https://outerbounds.github.io/metaflow-tools',
521
+ set=[
522
+ 'metadatadb.user=metaflow',
523
+ 'metadatadb.password=metaflow123',
524
+ 'metadatadb.database=metaflow',
525
+ 'metadatadb.host=postgresql',
526
+ 'image.repository=public.ecr.aws/p7g1e3j4/metaflow-service',
527
+ 'image.tag=2.4.13-fbcc7d04',
528
+ 'resources.requests.cpu=25m',
529
+ 'resources.requests.memory=64Mi',
530
+ 'resources.limits.cpu=50m',
531
+ 'resources.limits.memory=128Mi'
532
+ ]
533
+ )
534
+
535
+ k8s_resource(
536
+ 'metaflow-service',
537
+ port_forwards=['8080:8080'],
538
+ links=[link('http://localhost:8080/ping', 'Ping Metaflow Service')],
539
+ labels=['metadata-service'],
540
+ resource_deps=components['metadata-service']
541
+ )
542
+
543
+ metaflow_config["METAFLOW_DEFAULT_METADATA"] = "service"
544
+ metaflow_config["METAFLOW_SERVICE_URL"] = "http://localhost:8080"
545
+ metaflow_config["METAFLOW_SERVICE_INTERNAL_URL"] = "http://metaflow-service.default.svc.cluster.local:8080"
546
+
547
+ config_resources.append('metaflow-service')
548
+
549
+ #################################################
550
+ # METAFLOW UI
551
+ #################################################
552
+ if "ui" in enabled_components:
553
+ helm_remote(
554
+ 'metaflow-ui',
555
+ repo_name='metaflow-tools',
556
+ repo_url='https://outerbounds.github.io/metaflow-tools',
557
+ set=[
558
+ 'uiBackend.metadatadb.user=metaflow',
559
+ 'uiBackend.metadatadb.password=metaflow123',
560
+ 'uiBackend.metadatadb.name=metaflow',
561
+ 'uiBackend.metadatadb.host=postgresql',
562
+ 'uiBackend.metaflowDatastoreSysRootS3=s3://metaflow-test',
563
+ 'uiBackend.metaflowS3EndpointURL=http://minio.default.svc.cluster.local:9000',
564
+ 'uiBackend.image.name=public.ecr.aws/p7g1e3j4/metaflow-service',
565
+ 'uiBackend.image.tag=2.4.13-fbcc7d04',
566
+ 'uiBackend.env[0].name=AWS_ACCESS_KEY_ID',
567
+ 'uiBackend.env[0].value=rootuser',
568
+ 'uiBackend.env[1].name=AWS_SECRET_ACCESS_KEY',
569
+ 'uiBackend.env[1].value=rootpass123',
570
+ # TODO: configure lower cache limits
571
+ 'uiBackend.resources.requests.cpu=100m',
572
+ 'uiBackend.resources.requests.memory=256Mi',
573
+ 'uiStatic.metaflowUIBackendURL=http://localhost:8083/api',
574
+ 'uiStatic.image.name=public.ecr.aws/outerbounds/metaflow_ui',
575
+ 'uiStatic.image.tag=v1.3.13-5-g5dd049e',
576
+ 'uiStatic.resources.requests.cpu=25m',
577
+ 'uiStatic.resources.requests.memory=64Mi',
578
+ 'uiStatic.resources.limits.cpu=50m',
579
+ 'uiStatic.resources.limits.memory=128Mi',
580
+ ]
581
+ )
582
+
583
+ k8s_resource(
584
+ 'metaflow-ui-static',
585
+ port_forwards=['3000:3000'],
586
+ links=[link('http://localhost:3000', 'Metaflow UI')],
587
+ labels=['metaflow-ui'],
588
+ resource_deps=components['ui']
589
+ )
590
+
591
+ k8s_resource(
592
+ 'metaflow-ui',
593
+ port_forwards=['8083:8083'],
594
+ links=[link('http://localhost:3000', 'Metaflow UI')],
595
+ labels=['metaflow-ui'],
596
+ resource_deps=components['ui']
597
+ )
598
+
599
+ metaflow_config["METAFLOW_UI_URL"] = "http://localhost:3000"
600
+
601
+ config_resources.append('metaflow-ui')
602
+ config_resources.append('metaflow-ui-static')
603
+
604
+ cmd = '''
605
+ ARCH=$(kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.architecture}')
606
+ case "$ARCH" in
607
+ arm64) echo linux-aarch64 ;;
608
+ amd64) echo linux-64 ;;
609
+ *) echo linux-64 ;;
610
+ esac
611
+ '''
612
+
613
+ # For @conda/@pypi emulation
614
+ metaflow_config["METAFLOW_KUBERNETES_CONDA_ARCH"] = str(local(cmd)).strip()
615
+
616
+ local_resource(
617
+ name="generate-configs",
618
+ cmd=write_config_files(),
619
+ resource_deps=config_resources,
620
+ )