mage-ai 0.9.49__py3-none-any.whl → 0.9.51__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.

Potentially problematic release.


This version of mage-ai might be problematic. Click here for more details.

Files changed (153) hide show
  1. mage_ai/api/operations/base.py +11 -2
  2. mage_ai/api/presenters/WorkspacePresenter.py +1 -0
  3. mage_ai/api/resources/PipelineResource.py +28 -19
  4. mage_ai/cache/utils.py +2 -0
  5. mage_ai/cluster_manager/kubernetes/workload_manager.py +184 -147
  6. mage_ai/cluster_manager/workspace/kubernetes.py +25 -3
  7. mage_ai/data_preparation/executors/block_executor.py +10 -4
  8. mage_ai/data_preparation/models/global_hooks/models.py +9 -3
  9. mage_ai/data_preparation/models/pipeline.py +1 -47
  10. mage_ai/data_preparation/preferences.py +71 -27
  11. mage_ai/data_preparation/templates/custom/python/default.jinja +3 -5
  12. mage_ai/io/postgres.py +1 -0
  13. mage_ai/server/constants.py +1 -1
  14. mage_ai/server/frontend_dist/404.html +2 -2
  15. mage_ai/server/frontend_dist/_next/static/chunks/4138-1ffb2d1ab4fc61f2.js +1 -0
  16. mage_ai/server/frontend_dist/_next/static/chunks/{5499-f60c2fc8ed0442d5.js → 5499-6f0bc8a94f2b78b4.js} +1 -1
  17. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/manage-221f6ba4b04367c6.js → frontend_dist/_next/static/chunks/pages/manage-41c56c0ae1c15cb6.js} +1 -1
  18. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-006a61883c0a6fce.js +1 -0
  19. mage_ai/server/frontend_dist/_next/static/{chun9Xv4R2ZZyX0zoxPm1 → z6eg1yN60N6gosbEr2pGr}/_buildManifest.js +1 -1
  20. mage_ai/server/frontend_dist/block-layout.html +2 -2
  21. mage_ai/server/frontend_dist/compute.html +2 -2
  22. mage_ai/server/frontend_dist/files.html +2 -2
  23. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  24. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  25. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  26. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  27. mage_ai/server/frontend_dist/index.html +2 -2
  28. mage_ai/server/frontend_dist/manage/files.html +2 -2
  29. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  30. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  31. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  32. mage_ai/server/frontend_dist/manage/users.html +2 -2
  33. mage_ai/server/frontend_dist/manage.html +2 -2
  34. mage_ai/server/frontend_dist/oauth.html +3 -3
  35. mage_ai/server/frontend_dist/overview.html +2 -2
  36. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  37. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  38. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  39. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  40. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  41. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  42. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  43. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  44. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  45. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  46. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  47. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  48. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  49. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  50. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  51. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  52. mage_ai/server/frontend_dist/pipelines.html +2 -2
  53. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  54. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  55. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  56. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  57. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  58. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  59. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  60. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  61. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  62. mage_ai/server/frontend_dist/settings.html +2 -2
  63. mage_ai/server/frontend_dist/sign-in.html +3 -3
  64. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  65. mage_ai/server/frontend_dist/templates.html +2 -2
  66. mage_ai/server/frontend_dist/terminal.html +2 -2
  67. mage_ai/server/frontend_dist/test.html +3 -3
  68. mage_ai/server/frontend_dist/triggers.html +2 -2
  69. mage_ai/server/frontend_dist/version-control.html +2 -2
  70. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  71. mage_ai/server/frontend_dist_base_path_template/_next/static/{hcfGediUFcOE3vcIGjt-n → GkYZIZv7ClhebnBGgTrwO}/_buildManifest.js +1 -1
  72. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{1952-0f9a12782f0aaae6.js → 1952-0c8a3cb84da67f53.js} +1 -1
  73. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{2714-68fef54789d7eaeb.js → 2714-1e79e9f2e998b544.js} +1 -1
  74. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{2717-82a714ddff3edf43.js → 2717-e599ab448e3c1b7f.js} +1 -1
  75. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{3437-ed09bb896e50e022.js → 3437-0712d7142aed2c46.js} +1 -1
  76. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/4138-1ffb2d1ab4fc61f2.js +1 -0
  77. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{4783-422429203610c318.js → 4783-1a21d9be47574bba.js} +1 -1
  78. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{5499-f60c2fc8ed0442d5.js → 5499-6f0bc8a94f2b78b4.js} +1 -1
  79. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{5896-7b8e36634d7d94eb.js → 5896-14e5a23b1c6a0769.js} +1 -1
  80. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{6285-648f9a732e100b2f.js → 6285-e9b45335bfb9ccaf.js} +1 -1
  81. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{6798-b904395b0c18647b.js → 6798-1ef0247e65215a0f.js} +1 -1
  82. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{976-0a8c2c4d7acd957b.js → 976-18c98af60b76f1a7.js} +1 -1
  83. mage_ai/server/{frontend_dist/_next/static/chunks/pages/manage-221f6ba4b04367c6.js → frontend_dist_base_path_template/_next/static/chunks/pages/manage-41c56c0ae1c15cb6.js} +1 -1
  84. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{edit-0cd2a275eb8b6345.js → edit-285ea9b8083b5f22.js} +1 -1
  85. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/{[run]-e9c1506c0a1f87b6.js → [run]-15deea898d2fa18b.js} +1 -1
  86. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{settings-495e877aa7ed709e.js → settings-6f5a7e367ec63c43.js} +1 -1
  87. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-006a61883c0a6fce.js +1 -0
  88. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{preferences-f3c29ec53ee35795.js → preferences-b058d0ff37970cd9.js} +1 -1
  89. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  90. mage_ai/server/frontend_dist_base_path_template/compute.html +5 -5
  91. mage_ai/server/frontend_dist_base_path_template/files.html +5 -5
  92. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +5 -5
  93. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +5 -5
  94. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +5 -5
  95. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +5 -5
  96. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  97. mage_ai/server/frontend_dist_base_path_template/manage/files.html +5 -5
  98. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +5 -5
  99. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +5 -5
  100. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +5 -5
  101. mage_ai/server/frontend_dist_base_path_template/manage/users.html +5 -5
  102. mage_ai/server/frontend_dist_base_path_template/manage.html +5 -5
  103. mage_ai/server/frontend_dist_base_path_template/oauth.html +3 -3
  104. mage_ai/server/frontend_dist_base_path_template/overview.html +5 -5
  105. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +5 -5
  106. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +5 -5
  107. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +5 -5
  108. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +5 -5
  109. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  110. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +5 -5
  111. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +5 -5
  112. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +5 -5
  113. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +5 -5
  114. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +5 -5
  115. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +5 -5
  116. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +5 -5
  117. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +5 -5
  118. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +5 -5
  119. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +5 -5
  120. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  121. mage_ai/server/frontend_dist_base_path_template/pipelines.html +5 -5
  122. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +5 -5
  123. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +5 -5
  124. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +5 -5
  125. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +5 -5
  126. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +5 -5
  127. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +5 -5
  128. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +5 -5
  129. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +5 -5
  130. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +5 -5
  131. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  132. mage_ai/server/frontend_dist_base_path_template/sign-in.html +11 -11
  133. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +5 -5
  134. mage_ai/server/frontend_dist_base_path_template/templates.html +5 -5
  135. mage_ai/server/frontend_dist_base_path_template/terminal.html +5 -5
  136. mage_ai/server/frontend_dist_base_path_template/test.html +4 -4
  137. mage_ai/server/frontend_dist_base_path_template/triggers.html +5 -5
  138. mage_ai/server/frontend_dist_base_path_template/version-control.html +5 -5
  139. mage_ai/settings/__init__.py +32 -13
  140. mage_ai/tests/cache/test_pipeline_cache.py +1 -0
  141. mage_ai/tests/cluster_manager/kubernetes/test_workload_manager.py +76 -12
  142. {mage_ai-0.9.49.dist-info → mage_ai-0.9.51.dist-info}/METADATA +1 -1
  143. {mage_ai-0.9.49.dist-info → mage_ai-0.9.51.dist-info}/RECORD +149 -149
  144. mage_ai/server/frontend_dist/_next/static/chunks/4138-a2c37d37dba2f44c.js +0 -1
  145. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-34317532903c1ec9.js +0 -1
  146. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/4138-a2c37d37dba2f44c.js +0 -1
  147. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-34317532903c1ec9.js +0 -1
  148. /mage_ai/server/frontend_dist/_next/static/{chun9Xv4R2ZZyX0zoxPm1 → z6eg1yN60N6gosbEr2pGr}/_ssgManifest.js +0 -0
  149. /mage_ai/server/frontend_dist_base_path_template/_next/static/{hcfGediUFcOE3vcIGjt-n → GkYZIZv7ClhebnBGgTrwO}/_ssgManifest.js +0 -0
  150. {mage_ai-0.9.49.dist-info → mage_ai-0.9.51.dist-info}/LICENSE +0 -0
  151. {mage_ai-0.9.49.dist-info → mage_ai-0.9.51.dist-info}/WHEEL +0 -0
  152. {mage_ai-0.9.49.dist-info → mage_ai-0.9.51.dist-info}/entry_points.txt +0 -0
  153. {mage_ai-0.9.49.dist-info → mage_ai-0.9.51.dist-info}/top_level.txt +0 -0
@@ -83,7 +83,9 @@ class WorkloadManager:
83
83
  services = self.core_client.list_namespaced_service(self.namespace).items
84
84
  workloads_list = []
85
85
 
86
- stateful_sets = self.apps_client.list_namespaced_stateful_set(self.namespace).items
86
+ stateful_sets = self.apps_client.list_namespaced_stateful_set(
87
+ self.namespace
88
+ ).items
87
89
  stateful_set_mapping = dict()
88
90
  for ss in stateful_sets:
89
91
  try:
@@ -123,11 +125,12 @@ class WorkloadManager:
123
125
  try:
124
126
  if node_name:
125
127
  items = self.core_client.list_node(
126
- field_selector=f'metadata.name={node_name}').items
128
+ field_selector=f'metadata.name={node_name}'
129
+ ).items
127
130
  node = items[0]
128
131
  ip = find(
129
132
  lambda a: a.type == 'ExternalIP',
130
- node.status.addresses
133
+ node.status.addresses,
131
134
  ).address
132
135
  if ip:
133
136
  node_port = service.spec.ports[0].node_port
@@ -188,12 +191,7 @@ class WorkloadManager:
188
191
  ingress_name = workspace_config.ingress_name
189
192
 
190
193
  volumes = []
191
- volume_mounts = [
192
- {
193
- 'name': 'mage-data',
194
- 'mountPath': '/home/src'
195
- }
196
- ]
194
+ volume_mounts = [{'name': 'mage-data', 'mountPath': '/home/src'}]
197
195
 
198
196
  env_vars = self.__populate_env_vars(
199
197
  name,
@@ -207,7 +205,9 @@ class WorkloadManager:
207
205
  lifecycle_config = workspace_config.lifecycle_config or LifecycleConfig()
208
206
  if lifecycle_config.post_start:
209
207
  if lifecycle_config.post_start.hook_path:
210
- post_start_file_name = os.path.basename(lifecycle_config.post_start.hook_path)
208
+ post_start_file_name = os.path.basename(
209
+ lifecycle_config.post_start.hook_path
210
+ )
211
211
  volume_mounts.append(
212
212
  {
213
213
  'name': 'lifecycle-hooks',
@@ -230,18 +230,13 @@ class WorkloadManager:
230
230
  'exec': {
231
231
  'command': post_start_command,
232
232
  }
233
- }
233
+ },
234
234
  }
235
235
 
236
236
  mage_container_config = {
237
237
  'name': f'{name}-container',
238
238
  'image': 'mageai/mageai:latest',
239
- 'ports': [
240
- {
241
- 'containerPort': 6789,
242
- 'name': 'web'
243
- }
244
- ],
239
+ 'ports': [{'containerPort': 6789, 'name': 'web'}],
245
240
  'volumeMounts': volume_mounts,
246
241
  **container_config,
247
242
  }
@@ -266,7 +261,7 @@ class WorkloadManager:
266
261
  'name': 'lifecycle-hooks',
267
262
  'mountPath': '/app/initial-config.json',
268
263
  'subPath': 'initial-config.json',
269
- }
264
+ },
270
265
  ],
271
266
  'env': [
272
267
  {
@@ -276,7 +271,7 @@ class WorkloadManager:
276
271
  {
277
272
  'name': KUBE_NAMESPACE,
278
273
  'value': os.getenv(KUBE_NAMESPACE, 'default'),
279
- }
274
+ },
280
275
  ],
281
276
  }
282
277
  )
@@ -294,32 +289,23 @@ class WorkloadManager:
294
289
  '/cloud_sql_proxy',
295
290
  '-log_debug_stdout',
296
291
  f'-instances={os.getenv(CLOUD_SQL_CONNECTION_NAME)}=tcp:5432',
297
- f'-credential_file=/secrets/{credential_file_path}'
292
+ f'-credential_file=/secrets/{credential_file_path}',
298
293
  ],
299
- 'securityContext': {
300
- 'runAsNonRoot': True
301
- },
302
- 'resources': {
303
- 'requests': {
304
- 'memory': '1Gi',
305
- 'cpu': '1'
306
- }
307
- },
294
+ 'securityContext': {'runAsNonRoot': True},
295
+ 'resources': {'requests': {'memory': '1Gi', 'cpu': '1'}},
308
296
  'volumeMounts': [
309
297
  {
310
298
  'name': 'service-account-volume',
311
299
  'mountPath': '/secrets/',
312
- 'readOnly': True
300
+ 'readOnly': True,
313
301
  }
314
- ]
302
+ ],
315
303
  }
316
304
  )
317
305
  volumes.append(
318
306
  {
319
307
  'name': 'service-account-volume',
320
- 'secret': {
321
- 'secretName': os.getenv(SERVICE_ACCOUNT_SECRETS_NAME)
322
- }
308
+ 'secret': {'secretName': os.getenv(SERVICE_ACCOUNT_SECRETS_NAME)},
323
309
  }
324
310
  )
325
311
 
@@ -343,8 +329,8 @@ class WorkloadManager:
343
329
  **({'mode': 0o0755} if key.endswith('.sh') else {}),
344
330
  }
345
331
  for key in config_map
346
- ]
347
- }
332
+ ],
333
+ },
348
334
  }
349
335
  )
350
336
 
@@ -362,45 +348,28 @@ class WorkloadManager:
362
348
  stateful_set = {
363
349
  'apiVersion': 'apps/v1',
364
350
  'kind': 'StatefulSet',
365
- 'metadata': {
366
- 'name': name,
367
- 'labels': {
368
- 'app': name
369
- }
370
- },
351
+ 'metadata': {'name': name, 'labels': {'app': name}},
371
352
  'spec': {
372
- 'selector': {
373
- 'matchLabels': {
374
- 'app': name
375
- }
376
- },
353
+ 'selector': {'matchLabels': {'app': name}},
377
354
  'replicas': 1,
378
355
  'minReadySeconds': 10,
379
356
  'template': {
380
- 'metadata': {
381
- 'labels': {
382
- 'app': name
383
- }
384
- },
385
- 'spec': stateful_set_template_spec
357
+ 'metadata': {'labels': {'app': name}},
358
+ 'spec': stateful_set_template_spec,
386
359
  },
387
360
  'volumeClaimTemplates': [
388
361
  {
389
- 'metadata': {
390
- 'name': 'mage-data'
391
- },
362
+ 'metadata': {'name': 'mage-data'},
392
363
  'spec': {
393
364
  'accessModes': [storage_access_mode],
394
365
  'storageClassName': storage_class_name,
395
366
  'resources': {
396
- 'requests': {
397
- 'storage': storage_request_size
398
- }
399
- }
400
- }
367
+ 'requests': {'storage': storage_request_size}
368
+ },
369
+ },
401
370
  }
402
- ]
403
- }
371
+ ],
372
+ },
404
373
  }
405
374
 
406
375
  self.apps_client.create_namespaced_stateful_set(self.namespace, stateful_set)
@@ -409,8 +378,9 @@ class WorkloadManager:
409
378
 
410
379
  annotations = {}
411
380
  if os.getenv(KUBE_SERVICE_GCP_BACKEND_CONFIG):
412
- annotations[GCP_BACKEND_CONFIG_ANNOTATION] = \
413
- os.getenv(KUBE_SERVICE_GCP_BACKEND_CONFIG)
381
+ annotations[GCP_BACKEND_CONFIG_ANNOTATION] = os.getenv(
382
+ KUBE_SERVICE_GCP_BACKEND_CONFIG
383
+ )
414
384
 
415
385
  service = {
416
386
  'apiVersion': 'v1',
@@ -421,7 +391,7 @@ class WorkloadManager:
421
391
  'app': name,
422
392
  'dev-instance': '1',
423
393
  },
424
- 'annotations': annotations
394
+ 'annotations': annotations,
425
395
  },
426
396
  'spec': {
427
397
  'ports': [
@@ -430,14 +400,14 @@ class WorkloadManager:
430
400
  'port': 6789,
431
401
  }
432
402
  ],
433
- 'selector': {
434
- 'app': name
435
- },
436
- 'type': os.getenv(KUBE_SERVICE_TYPE, NODE_PORT_SERVICE_TYPE)
437
- }
403
+ 'selector': {'app': name},
404
+ 'type': os.getenv(KUBE_SERVICE_TYPE, NODE_PORT_SERVICE_TYPE),
405
+ },
438
406
  }
439
407
 
440
- k8s_service = self.core_client.create_namespaced_service(self.namespace, service)
408
+ k8s_service = self.core_client.create_namespaced_service(
409
+ self.namespace, service
410
+ )
441
411
 
442
412
  try:
443
413
  if ingress_name:
@@ -452,9 +422,12 @@ class WorkloadManager:
452
422
  self,
453
423
  ingress_name: str,
454
424
  service_name: str,
455
- workspace_name: str,
425
+ workload_name: str,
456
426
  ) -> None:
457
- ingress = self.networking_client.read_namespaced_ingress(ingress_name, self.namespace)
427
+ ingress = self.networking_client.read_namespaced_ingress(
428
+ ingress_name,
429
+ self.namespace,
430
+ )
458
431
  rule = ingress.spec.rules[0]
459
432
  paths = rule.http.paths
460
433
  paths.insert(
@@ -462,32 +435,84 @@ class WorkloadManager:
462
435
  client.V1HTTPIngressPath(
463
436
  backend=client.V1IngressBackend(
464
437
  service=client.V1IngressServiceBackend(
465
- name=service_name,
466
- port=client.V1ServiceBackendPort(
467
- number=6789
468
- )
438
+ name=service_name, port=client.V1ServiceBackendPort(number=6789)
469
439
  )
470
440
  ),
471
- path=f'/{workspace_name}',
441
+ path=f'/{workload_name}',
472
442
  path_type='Prefix',
473
- )
443
+ ),
474
444
  )
475
445
  ingress.spec.rules[0] = client.V1IngressRule(
476
446
  host=rule.host,
477
447
  http=client.V1HTTPIngressRuleValue(paths=paths),
478
448
  )
479
- self.networking_client.patch_namespaced_ingress(ingress_name, self.namespace, ingress)
449
+ self.networking_client.patch_namespaced_ingress(
450
+ ingress_name, self.namespace, ingress
451
+ )
452
+
453
+ def get_url_from_ingress(self, ingress_name: str, workload_name: str) -> str:
454
+ ingress = self.networking_client.read_namespaced_ingress(
455
+ ingress_name,
456
+ self.namespace,
457
+ )
458
+ rule = ingress.spec.rules[0]
459
+ host = rule.host
460
+
461
+ tls_enabled = False
462
+ try:
463
+ tls = ingress.spec.tls[0]
464
+ tls_enabled = host in tls.hosts
465
+ except Exception:
466
+ pass
480
467
 
481
- def delete_workload(self, name: str):
468
+ paths = rule.http.paths
469
+ for path in paths:
470
+ if path.backend.service.name == f'{workload_name}-service':
471
+ prefix = 'https' if tls_enabled else 'http'
472
+ return f'{prefix}://{host}{path.path}'
473
+
474
+ def remove_service_from_ingress_paths(
475
+ self,
476
+ ingress_name: str,
477
+ workload_name: str,
478
+ ) -> None:
479
+ ingress = self.networking_client.read_namespaced_ingress(
480
+ ingress_name, self.namespace
481
+ )
482
+ rule = ingress.spec.rules[0]
483
+ paths = rule.http.paths
484
+ for path in paths:
485
+ if path.backend.service.name == f'{workload_name}-service':
486
+ paths.remove(path)
487
+ break
488
+ ingress.spec.rules[0] = client.V1IngressRule(
489
+ host=rule.host,
490
+ http=client.V1HTTPIngressRuleValue(paths=paths),
491
+ )
492
+ self.networking_client.patch_namespaced_ingress(
493
+ ingress_name, self.namespace, ingress
494
+ )
495
+
496
+ def delete_workload(self, name: str, ingress_name: str = None):
482
497
  self.apps_client.delete_namespaced_stateful_set(name, self.namespace)
483
498
  self.core_client.delete_namespaced_service(f'{name}-service', self.namespace)
484
499
  try:
485
- self.core_client.delete_namespaced_config_map(f'{name}-hooks', self.namespace)
500
+ self.core_client.delete_namespaced_config_map(
501
+ f'{name}-hooks', self.namespace
502
+ )
486
503
  except ApiException as ex:
487
504
  # The delete operation will return a 404 response if the config map does not exist
488
505
  if ex.status != 404:
489
506
  raise
490
507
 
508
+ try:
509
+ if ingress_name:
510
+ self.remove_service_from_ingress_paths(ingress_name, name)
511
+ except Exception as ex:
512
+ raise Exception(
513
+ 'Failed to delete workspace path from ingress, you may need to manually delete it'
514
+ ) from ex
515
+
491
516
  def get_workload_activity(self, name: str) -> Dict:
492
517
  pods = self.core_client.list_namespaced_pod(self.namespace).items
493
518
  pod_name = None
@@ -527,12 +552,16 @@ class WorkloadManager:
527
552
 
528
553
  def scale_down_workload(self, name: str) -> None:
529
554
  self.apps_client.patch_namespaced_stateful_set(
530
- name, namespace=self.namespace, body={'spec': {'replicas': 0}},
555
+ name,
556
+ namespace=self.namespace,
557
+ body={'spec': {'replicas': 0}},
531
558
  )
532
559
 
533
560
  def restart_workload(self, name: str) -> None:
534
561
  self.apps_client.patch_namespaced_stateful_set(
535
- name, namespace=self.namespace, body={'spec': {'replicas': 1}},
562
+ name,
563
+ namespace=self.namespace,
564
+ body={'spec': {'replicas': 1}},
536
565
  )
537
566
 
538
567
  def create_hooks_config_map(
@@ -546,7 +575,9 @@ class WorkloadManager:
546
575
  if pre_start_script_path:
547
576
  if not mage_container_config:
548
577
  raise ConfigurationError('The container config can not be empty')
549
- self.__validate_pre_start_script(pre_start_script_path, mage_container_config)
578
+ self.__validate_pre_start_script(
579
+ pre_start_script_path, mage_container_config
580
+ )
550
581
 
551
582
  with open(pre_start_script_path, 'r', encoding='utf-8') as f:
552
583
  pre_start_script = f.read()
@@ -567,11 +598,10 @@ class WorkloadManager:
567
598
  'data': config_map_data,
568
599
  'metadata': {
569
600
  'name': f'{name}-hooks',
570
- }
601
+ },
571
602
  }
572
603
  self.core_client.create_namespaced_config_map(
573
- namespace=self.namespace,
574
- body=config_map
604
+ namespace=self.namespace, body=config_map
575
605
  )
576
606
 
577
607
  return config_map_data
@@ -588,7 +618,9 @@ class WorkloadManager:
588
618
  except Exception as ex:
589
619
  raise Exception(f'Pre-start script is invalid: {str(ex)}')
590
620
 
591
- spec = importlib.util.spec_from_file_location('pre_start', pre_start_script_path)
621
+ spec = importlib.util.spec_from_file_location(
622
+ 'pre_start', pre_start_script_path
623
+ )
592
624
  module = importlib.util.module_from_spec(spec)
593
625
 
594
626
  try:
@@ -602,7 +634,9 @@ class WorkloadManager:
602
634
  f', error: {str(ex)}'
603
635
  )
604
636
  except Exception as ex:
605
- raise ConfigurationError(f'Pre-start script validation failed with error: {str(ex)}')
637
+ raise ConfigurationError(
638
+ f'Pre-start script validation failed with error: {str(ex)}'
639
+ )
606
640
 
607
641
  def __populate_env_vars(
608
642
  self,
@@ -619,20 +653,26 @@ class WorkloadManager:
619
653
  }
620
654
  ]
621
655
  if set_base_path:
622
- env_vars.append({
623
- 'name': 'MAGE_BASE_PATH',
624
- 'value': name,
625
- })
656
+ env_vars.append(
657
+ {
658
+ 'name': 'MAGE_BASE_PATH',
659
+ 'value': name,
660
+ }
661
+ )
626
662
  if project_type:
627
- env_vars.append({
628
- 'name': 'PROJECT_TYPE',
629
- 'value': project_type,
630
- })
663
+ env_vars.append(
664
+ {
665
+ 'name': 'PROJECT_TYPE',
666
+ 'value': project_type,
667
+ }
668
+ )
631
669
  if project_uuid:
632
- env_vars.append({
633
- 'name': 'PROJECT_UUID',
634
- 'value': project_uuid,
635
- })
670
+ env_vars.append(
671
+ {
672
+ 'name': 'PROJECT_UUID',
673
+ 'value': project_uuid,
674
+ }
675
+ )
636
676
 
637
677
  connection_url_secrets_name = os.getenv(CONNECTION_URL_SECRETS_NAME)
638
678
  if connection_url_secrets_name:
@@ -642,9 +682,9 @@ class WorkloadManager:
642
682
  'valueFrom': {
643
683
  'secretKeyRef': {
644
684
  'name': connection_url_secrets_name,
645
- 'key': 'connection_url'
685
+ 'key': 'connection_url',
646
686
  }
647
- }
687
+ },
648
688
  }
649
689
  )
650
690
 
@@ -653,50 +693,47 @@ class WorkloadManager:
653
693
  KUBE_NAMESPACE,
654
694
  ]:
655
695
  if os.getenv(var) is not None:
656
- env_vars.append({
657
- 'name': var,
658
- 'value': str(os.getenv(var)),
659
- })
696
+ env_vars.append(
697
+ {
698
+ 'name': var,
699
+ 'value': str(os.getenv(var)),
700
+ }
701
+ )
660
702
 
661
703
  # For connecting to CloudSQL PostgreSQL database.
662
704
  db_secrets_name = os.getenv(DB_SECRETS_NAME)
663
705
  if db_secrets_name:
664
- env_vars.extend([
665
- {
666
- 'name': PG_DB_USER,
667
- 'valueFrom': {
668
- 'secretKeyRef': {
669
- 'name': db_secrets_name,
670
- 'key': 'username'
671
- }
672
- }
673
- },
674
- {
675
- 'name': PG_DB_PASS,
676
- 'valueFrom': {
677
- 'secretKeyRef': {
678
- 'name': db_secrets_name,
679
- 'key': 'password'
680
- }
681
- }
682
- },
683
- {
684
- 'name': PG_DB_NAME,
685
- 'valueFrom': {
686
- 'secretKeyRef': {
687
- 'name': db_secrets_name,
688
- 'key': 'database'
689
- }
690
- }
691
- }
692
- ])
706
+ env_vars.extend(
707
+ [
708
+ {
709
+ 'name': PG_DB_USER,
710
+ 'valueFrom': {
711
+ 'secretKeyRef': {'name': db_secrets_name, 'key': 'username'}
712
+ },
713
+ },
714
+ {
715
+ 'name': PG_DB_PASS,
716
+ 'valueFrom': {
717
+ 'secretKeyRef': {'name': db_secrets_name, 'key': 'password'}
718
+ },
719
+ },
720
+ {
721
+ 'name': PG_DB_NAME,
722
+ 'valueFrom': {
723
+ 'secretKeyRef': {'name': db_secrets_name, 'key': 'database'}
724
+ },
725
+ },
726
+ ]
727
+ )
693
728
 
694
729
  if container_config and 'env' in container_config:
695
730
  env_vars += container_config['env']
696
731
 
697
732
  return env_vars
698
733
 
699
- def __get_configurable_parameters(self, workspace_config: KubernetesWorkspaceConfig) -> Dict:
734
+ def __get_configurable_parameters(
735
+ self, workspace_config: KubernetesWorkspaceConfig
736
+ ) -> Dict:
700
737
  service_account_name_default = None
701
738
  storage_class_name_default = None
702
739
  storage_access_mode_default = None
@@ -729,8 +766,10 @@ class WorkloadManager:
729
766
  return dict(
730
767
  service_account_name=workspace_config.service_account_name
731
768
  or service_account_name_default,
732
- storage_class_name=workspace_config.storage_class_name or storage_class_name_default,
733
- storage_access_mode=workspace_config.storage_access_mode or storage_access_mode_default,
769
+ storage_class_name=workspace_config.storage_class_name
770
+ or storage_class_name_default,
771
+ storage_access_mode=workspace_config.storage_access_mode
772
+ or storage_access_mode_default,
734
773
  storage_request_size=storage_request_size,
735
774
  )
736
775
 
@@ -747,9 +786,7 @@ class WorkloadManager:
747
786
  pv = {
748
787
  'apiVersion': 'v1',
749
788
  'kind': 'PersistentVolume',
750
- 'metadata': {
751
- 'name': f'{name}-pv'
752
- },
789
+ 'metadata': {'name': f'{name}-pv'},
753
790
  'spec': {
754
791
  'capacity': {
755
792
  'storage': storage_request_size,
@@ -769,14 +806,14 @@ class WorkloadManager:
769
806
  {
770
807
  'key': 'kubernetes.io/hostname',
771
808
  'operator': 'In',
772
- 'values': hostnames
809
+ 'values': hostnames,
773
810
  }
774
811
  ]
775
812
  }
776
813
  ]
777
814
  }
778
- }
779
- }
815
+ },
816
+ },
780
817
  }
781
818
  persistent_volumes = self.core_client.list_persistent_volume().items
782
819
  for volume in persistent_volumes:
@@ -80,12 +80,34 @@ class KubernetesWorkspace(Workspace):
80
80
  return cls(name)
81
81
 
82
82
  def delete(self, **kwargs):
83
- self.workload_manager.delete_workload(self.name)
84
-
85
- super().delete(**kwargs)
83
+ try:
84
+ self.workload_manager.delete_workload(
85
+ self.name, ingress_name=self.config.ingress_name
86
+ )
87
+ finally:
88
+ super().delete(**kwargs)
86
89
 
87
90
  def stop(self, **kwargs):
88
91
  self.workload_manager.scale_down_workload(self.name)
89
92
 
90
93
  def resume(self, **kwargs):
91
94
  self.workload_manager.restart_workload(self.name)
95
+
96
+ def to_dict(self):
97
+ config = dict(
98
+ name=self.name,
99
+ **self.config.to_dict(),
100
+ )
101
+
102
+ ingress_name = config.get('ingress_name')
103
+ try:
104
+ if ingress_name:
105
+ url = self.workload_manager.get_url_from_ingress(
106
+ ingress_name,
107
+ self.name,
108
+ )
109
+ config['url'] = url
110
+ except Exception:
111
+ pass
112
+
113
+ return config
@@ -1255,10 +1255,16 @@ class BlockExecutor:
1255
1255
  if status == BlockRun.BlockRunStatus.COMPLETED:
1256
1256
  update_kwargs['completed_at'] = datetime.now(tz=pytz.UTC)
1257
1257
 
1258
- if BlockRun.BlockRunStatus.FAILED == status and error_details:
1259
- update_kwargs['metrics'] = merge_dict(block_run.metrics or {}, dict(
1260
- __error_details=error_details,
1261
- ))
1258
+ # Cannot save raw value in DB; it breaks:
1259
+ # sqlalchemy.exc.StatementError:
1260
+ # (builtins.TypeError) Object of type Py4JJavaError is not JSON serializable
1261
+ # [SQL: UPDATE block_run SET updated_at=CURRENT_TIMESTAMP, status=?, metrics=?
1262
+ # WHERE block_run.id = ?]
1263
+
1264
+ # if BlockRun.BlockRunStatus.FAILED == status and error_details:
1265
+ # update_kwargs['metrics'] = merge_dict(block_run.metrics or {}, dict(
1266
+ # __error_details=error_details,
1267
+ # ))
1262
1268
 
1263
1269
  block_run.update(**update_kwargs)
1264
1270
  return
@@ -239,6 +239,7 @@ class Hook(BaseDataClass):
239
239
  self,
240
240
  pipeline_run: PipelineRun = None,
241
241
  resource_parent_type: str = None,
242
+ **kwargs,
242
243
  ) -> Dict:
243
244
  self.output = {}
244
245
 
@@ -421,9 +422,14 @@ class Hook(BaseDataClass):
421
422
  if pipeline_run and pipeline_run.block_runs:
422
423
  for block_run in pipeline_run.block_runs:
423
424
  block_run.refresh()
424
-
425
- if block_run.metrics.get('__error_details'):
426
- error_details_arr.append(block_run.metrics.get('__error_details'))
425
+ # Cannot save raw value in DB; it breaks:
426
+ # sqlalchemy.exc.StatementError:
427
+ # (builtins.TypeError) Object of type Py4JJavaError is not JSON serializable
428
+ # [SQL: UPDATE block_run SET updated_at=CURRENT_TIMESTAMP, status=?, metrics=?
429
+ # WHERE block_run.id = ?]
430
+
431
+ # if block_run.metrics.get('__error_details'):
432
+ # error_details_arr.append(block_run.metrics.get('__error_details'))
427
433
 
428
434
  self.status = HookStatus.load(error=err, errors=error_details_arr)
429
435