ob-metaflow-extensions 1.1.151__py2.py3-none-any.whl → 1.6.2__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.
- metaflow_extensions/outerbounds/__init__.py +1 -1
- metaflow_extensions/outerbounds/plugins/__init__.py +24 -3
- metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +16 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +506 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/__init__.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/__init__.py +4 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/spinners.py +478 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +128 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_deploy_decorator.py +333 -0
- metaflow_extensions/outerbounds/plugins/apps/core/artifacts.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +1029 -0
- metaflow_extensions/outerbounds/plugins/apps/core/click_importer.py +24 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/__init__.py +3 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +618 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/examples.py +125 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +15 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +165 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +966 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +299 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +233 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +537 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +1125 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +337 -0
- metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +115 -0
- metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +1300 -0
- metaflow_extensions/outerbounds/plugins/apps/core/exceptions.py +341 -0
- metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +89 -0
- metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +123 -0
- metaflow_extensions/outerbounds/plugins/apps/core/secrets.py +164 -0
- metaflow_extensions/outerbounds/plugins/apps/core/utils.py +233 -0
- metaflow_extensions/outerbounds/plugins/apps/core/validations.py +17 -0
- metaflow_extensions/outerbounds/plugins/aws/__init__.py +4 -0
- metaflow_extensions/outerbounds/plugins/aws/assume_role.py +3 -0
- metaflow_extensions/outerbounds/plugins/aws/assume_role_decorator.py +118 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/coreweave.py +9 -77
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/external_chckpt.py +85 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/nebius.py +7 -78
- metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +119 -0
- metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +17 -3
- metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +1 -0
- metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +18 -44
- metaflow_extensions/outerbounds/plugins/kubernetes/pod_killer.py +374 -0
- metaflow_extensions/outerbounds/plugins/nim/card.py +1 -6
- metaflow_extensions/outerbounds/plugins/nim/{__init__.py → nim_decorator.py} +13 -49
- metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +294 -233
- metaflow_extensions/outerbounds/plugins/nim/utils.py +36 -0
- metaflow_extensions/outerbounds/plugins/nvcf/constants.py +2 -2
- metaflow_extensions/outerbounds/plugins/nvct/nvct_decorator.py +32 -8
- metaflow_extensions/outerbounds/plugins/nvct/nvct_runner.py +1 -1
- metaflow_extensions/outerbounds/plugins/ollama/__init__.py +171 -16
- metaflow_extensions/outerbounds/plugins/ollama/constants.py +1 -0
- metaflow_extensions/outerbounds/plugins/ollama/exceptions.py +22 -0
- metaflow_extensions/outerbounds/plugins/ollama/ollama.py +1710 -114
- metaflow_extensions/outerbounds/plugins/ollama/status_card.py +292 -0
- metaflow_extensions/outerbounds/plugins/optuna/__init__.py +49 -0
- metaflow_extensions/outerbounds/plugins/profilers/simple_card_decorator.py +96 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/__init__.py +7 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/binary_caller.py +132 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/constants.py +11 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/exceptions.py +13 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/proxy_bootstrap.py +59 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_api.py +93 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_decorator.py +250 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_manager.py +225 -0
- metaflow_extensions/outerbounds/plugins/snowflake/snowflake.py +37 -7
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +18 -8
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +6 -0
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +45 -18
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +18 -9
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +10 -4
- metaflow_extensions/outerbounds/plugins/torchtune/__init__.py +163 -0
- metaflow_extensions/outerbounds/plugins/vllm/__init__.py +255 -0
- metaflow_extensions/outerbounds/plugins/vllm/constants.py +1 -0
- metaflow_extensions/outerbounds/plugins/vllm/exceptions.py +1 -0
- metaflow_extensions/outerbounds/plugins/vllm/status_card.py +352 -0
- metaflow_extensions/outerbounds/plugins/vllm/vllm_manager.py +621 -0
- metaflow_extensions/outerbounds/remote_config.py +46 -9
- metaflow_extensions/outerbounds/toplevel/apps/__init__.py +9 -0
- metaflow_extensions/outerbounds/toplevel/apps/exceptions.py +11 -0
- metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +86 -2
- metaflow_extensions/outerbounds/toplevel/ob_internal.py +4 -0
- metaflow_extensions/outerbounds/toplevel/plugins/optuna/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/plugins/torchtune/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/plugins/vllm/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/s3_proxy.py +88 -0
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/METADATA +2 -2
- ob_metaflow_extensions-1.6.2.dist-info/RECORD +136 -0
- metaflow_extensions/outerbounds/plugins/nim/utilities.py +0 -5
- ob_metaflow_extensions-1.1.151.dist-info/RECORD +0 -74
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional, List, Dict
|
|
3
|
+
from ._state_machine import LogLine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OuterboundsBackendUnhealthyException(Exception):
|
|
7
|
+
"""This exception is raised when the Outerbounds platform is unhealthy (5xx errors) or unreachable."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
url: str,
|
|
12
|
+
method: str,
|
|
13
|
+
status_code: Optional[int] = None,
|
|
14
|
+
text: Optional[str] = None,
|
|
15
|
+
message: Optional[str] = None,
|
|
16
|
+
):
|
|
17
|
+
self.url = url
|
|
18
|
+
self.method = method
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.text = text
|
|
21
|
+
self.message = message
|
|
22
|
+
super().__init__(self.message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OuterboundsForbiddenException(Exception):
|
|
26
|
+
"""This exception is raised when access to an Outerbounds API is forbidden (HTTP 403)."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
url: str,
|
|
31
|
+
method: str,
|
|
32
|
+
text: str,
|
|
33
|
+
):
|
|
34
|
+
self.url = url
|
|
35
|
+
self.method = method
|
|
36
|
+
self.text = text
|
|
37
|
+
self.message = (
|
|
38
|
+
f"Access forbidden (HTTP 403) when calling {url}. "
|
|
39
|
+
"This typically means your credentials lack permission for this operation. "
|
|
40
|
+
"Please verify that you outerbounds / metaflow configuration has access to the "
|
|
41
|
+
"correct perimeter.\n"
|
|
42
|
+
"If you believe this is an error, contact your Outerbounds administrator."
|
|
43
|
+
)
|
|
44
|
+
super().__init__(self.message)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OuterboundsConfigurationException(Exception):
|
|
48
|
+
"""This exception is raised when Outerbounds configuration is missing or invalid."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, missing_config: str):
|
|
51
|
+
self.missing_config = missing_config
|
|
52
|
+
self.message = (
|
|
53
|
+
f"Outerbounds configuration '{missing_config}' not found.\n\n"
|
|
54
|
+
"If running locally:\n"
|
|
55
|
+
" - Run: outerbounds configure <magic-string-from-outerbounds-ui>\n"
|
|
56
|
+
"If running remotely (e.g., in a Metaflow task):\n"
|
|
57
|
+
" - Ensure you have the Outerbounds distribution installed (pip install outerbounds)\n"
|
|
58
|
+
" and not the open-source Metaflow. The Outerbounds fork injects configuration\n"
|
|
59
|
+
" into remote tasks automatically which may not be present in open source metaflow.\n"
|
|
60
|
+
)
|
|
61
|
+
super().__init__(self.message)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CapsuleApiException(Exception):
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
url: str,
|
|
68
|
+
method: str,
|
|
69
|
+
status_code: int,
|
|
70
|
+
text: str,
|
|
71
|
+
message: Optional[str] = None,
|
|
72
|
+
):
|
|
73
|
+
self.url = url
|
|
74
|
+
self.method = method
|
|
75
|
+
self.status_code = status_code
|
|
76
|
+
self.text = text
|
|
77
|
+
self.message = message
|
|
78
|
+
|
|
79
|
+
def __str__(self):
|
|
80
|
+
return (
|
|
81
|
+
f"CapsuleApiException: {self.url} [{self.method}]: Status Code: {self.status_code} \n\n {self.text}"
|
|
82
|
+
+ (f"\n\n {self.message}" if self.message else "")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CapsuleDeploymentException(Exception):
|
|
87
|
+
"""Base exception for all capsule deployment failures."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
capsule_id: str,
|
|
92
|
+
message: str,
|
|
93
|
+
):
|
|
94
|
+
self.capsule_id = capsule_id
|
|
95
|
+
self.message = message
|
|
96
|
+
super().__init__(self.message)
|
|
97
|
+
|
|
98
|
+
def __str__(self):
|
|
99
|
+
return f"CapsuleDeploymentException: [{self.capsule_id}] :: {self.message}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CapsuleCrashLoopException(CapsuleDeploymentException):
|
|
103
|
+
"""Raised when a worker enters CrashLoopBackOff or Failed state."""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
capsule_id: str,
|
|
108
|
+
worker_id: str,
|
|
109
|
+
logs: Optional[List[LogLine]] = None,
|
|
110
|
+
):
|
|
111
|
+
self.worker_id = worker_id
|
|
112
|
+
self.logs = logs or []
|
|
113
|
+
message = f"Worker ID ({worker_id}) is crashlooping. Please check the logs for more information."
|
|
114
|
+
super().__init__(capsule_id, message)
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
return f"CapsuleCrashLoopException: [{self.capsule_id}] :: {self.message}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CapsuleReadinessException(CapsuleDeploymentException):
|
|
121
|
+
"""Raised when capsule fails to meet readiness conditions within timeout."""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
capsule_id: str,
|
|
126
|
+
reason: Optional[str] = None,
|
|
127
|
+
):
|
|
128
|
+
message = f"Capsule {capsule_id} failed to be ready to serve traffic"
|
|
129
|
+
if reason:
|
|
130
|
+
message += f": {reason}"
|
|
131
|
+
super().__init__(capsule_id, message)
|
|
132
|
+
|
|
133
|
+
def __str__(self):
|
|
134
|
+
return f"CapsuleReadinessException: [{self.capsule_id}] :: {self.message}"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CapsuleConcurrentUpgradeException(CapsuleDeploymentException):
|
|
138
|
+
"""Raised when a concurrent upgrade invalidates the current deployment."""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
capsule_id: str,
|
|
143
|
+
expected_version: str,
|
|
144
|
+
actual_version: str,
|
|
145
|
+
modified_by: Optional[str] = None,
|
|
146
|
+
modified_at: Optional[str] = None,
|
|
147
|
+
):
|
|
148
|
+
self.expected_version = expected_version
|
|
149
|
+
self.actual_version = actual_version
|
|
150
|
+
self.modified_by = modified_by
|
|
151
|
+
self.modified_at = modified_at
|
|
152
|
+
message = (
|
|
153
|
+
f"A capsule upgrade was triggered outside current deployment instance. "
|
|
154
|
+
f"Expected version: {expected_version}, actual version: {actual_version}"
|
|
155
|
+
)
|
|
156
|
+
if modified_by:
|
|
157
|
+
message += f". Modified by: {modified_by}"
|
|
158
|
+
if modified_at:
|
|
159
|
+
message += f" at {modified_at}"
|
|
160
|
+
super().__init__(capsule_id, message)
|
|
161
|
+
|
|
162
|
+
def __str__(self):
|
|
163
|
+
return (
|
|
164
|
+
f"CapsuleConcurrentUpgradeException: [{self.capsule_id}] :: {self.message}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CapsuleDeletedDuringDeploymentException(CapsuleDeploymentException):
|
|
169
|
+
"""Raised when a capsule is deleted while deployment is in progress."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, capsule_id: str):
|
|
172
|
+
super().__init__(capsule_id, "Capsule was deleted during deployment")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CodePackagingException(Exception):
|
|
176
|
+
"""Exception raised when code packaging fails."""
|
|
177
|
+
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class AppNotFoundException(Exception):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AppCreationFailedException(Exception):
|
|
186
|
+
"""Raised when app deployment submission fails due to an API error."""
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
app_name: str,
|
|
191
|
+
status_code: int,
|
|
192
|
+
error_text: str,
|
|
193
|
+
):
|
|
194
|
+
self.status_code = status_code
|
|
195
|
+
self.error_text = error_text
|
|
196
|
+
message = f"Failed to submit app deployment: HTTP {status_code} - {error_text}"
|
|
197
|
+
if status_code == 400:
|
|
198
|
+
# 400 = validation error; the app configuration is invalid and must be fixed.
|
|
199
|
+
message = "Invalid app deployment configuration submitted: "
|
|
200
|
+
try:
|
|
201
|
+
reason_for_failure = json.loads(error_text).get("message", error_text)
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
reason_for_failure = error_text
|
|
204
|
+
message += reason_for_failure
|
|
205
|
+
message += (
|
|
206
|
+
"\n\nCheck your config file or CLI parameters if deploying via CLI, "
|
|
207
|
+
"or AppDeployer parameters if deploying programmatically."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
self.message = message
|
|
211
|
+
super().__init__(message)
|
|
212
|
+
|
|
213
|
+
def __str__(self):
|
|
214
|
+
return f"AppCreationFailedException: {self.message}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class AppDeploymentException(Exception):
|
|
218
|
+
"""Base exception for all individual app deployment failures."""
|
|
219
|
+
|
|
220
|
+
def __init__(self, app_id: str, message: str):
|
|
221
|
+
self.app_id = app_id
|
|
222
|
+
self.message = message
|
|
223
|
+
self._deployed_app = None
|
|
224
|
+
super().__init__(self.message)
|
|
225
|
+
|
|
226
|
+
def __str__(self):
|
|
227
|
+
return f"AppDeploymentException: [{self.app_id}] :: {self.message}"
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def deployed_app(self):
|
|
231
|
+
from .deployer import DeployedApp
|
|
232
|
+
|
|
233
|
+
return DeployedApp._from_capsule_id(self.app_id)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class AppCrashLoopException(AppDeploymentException):
|
|
237
|
+
"""Raised when an app worker enters CrashLoopBackOff or Failed state."""
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
app_id: str,
|
|
242
|
+
worker_id: str,
|
|
243
|
+
logs: Optional[List] = None,
|
|
244
|
+
):
|
|
245
|
+
self.worker_id = worker_id
|
|
246
|
+
self.logs = logs or []
|
|
247
|
+
message = f"Worker ({worker_id}) is crashlooping. Please check the logs for more information."
|
|
248
|
+
super().__init__(app_id, message)
|
|
249
|
+
|
|
250
|
+
def __str__(self):
|
|
251
|
+
return f"AppCrashLoopException: [{self.app_id}] :: {self.message}"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class AppReadinessException(AppDeploymentException):
|
|
255
|
+
"""Raised when app fails to meet readiness conditions within timeout."""
|
|
256
|
+
|
|
257
|
+
def __init__(self, app_id: str, reason: Optional[str] = None):
|
|
258
|
+
message = f"App {app_id} failed to be ready to serve traffic"
|
|
259
|
+
if reason:
|
|
260
|
+
message += f": {reason}"
|
|
261
|
+
super().__init__(app_id, message)
|
|
262
|
+
|
|
263
|
+
def __str__(self):
|
|
264
|
+
return f"AppReadinessException: [{self.app_id}] :: {self.message}"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class AppUpgradeInProgressException(AppDeploymentException):
|
|
268
|
+
"""Raised when attempting to deploy while another upgrade is already in progress."""
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
app_id: str,
|
|
273
|
+
upgrader: Optional[str] = None,
|
|
274
|
+
):
|
|
275
|
+
self.upgrader = upgrader
|
|
276
|
+
if upgrader:
|
|
277
|
+
message = (
|
|
278
|
+
f"App {app_id} is currently being upgraded by {upgrader}. "
|
|
279
|
+
"Use force_upgrade=True in AppDeployer to override."
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
message = (
|
|
283
|
+
f"App {app_id} is currently being upgraded. "
|
|
284
|
+
"Use force_upgrade=True in AppDeployer to override."
|
|
285
|
+
)
|
|
286
|
+
super().__init__(app_id, message)
|
|
287
|
+
|
|
288
|
+
def __str__(self):
|
|
289
|
+
return f"AppUpgradeInProgressException: [{self.app_id}] :: {self.message}"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class AppConcurrentUpgradeException(AppDeploymentException):
|
|
293
|
+
"""Raised when a concurrent upgrade invalidates the current deployment mid-flight."""
|
|
294
|
+
|
|
295
|
+
def __init__(
|
|
296
|
+
self,
|
|
297
|
+
app_id: str,
|
|
298
|
+
expected_version: str,
|
|
299
|
+
actual_version: str,
|
|
300
|
+
modified_by: Optional[str] = None,
|
|
301
|
+
modified_at: Optional[str] = None,
|
|
302
|
+
):
|
|
303
|
+
self.expected_version = expected_version
|
|
304
|
+
self.actual_version = actual_version
|
|
305
|
+
self.modified_by = modified_by
|
|
306
|
+
self.modified_at = modified_at
|
|
307
|
+
|
|
308
|
+
modifier_info = ""
|
|
309
|
+
if modified_by:
|
|
310
|
+
modifier_info = f" by '{modified_by}'"
|
|
311
|
+
if modified_at:
|
|
312
|
+
modifier_info += f" at {modified_at}"
|
|
313
|
+
|
|
314
|
+
message = (
|
|
315
|
+
f"Another deployment was triggered{modifier_info} while this deployment was in progress.\n\n"
|
|
316
|
+
f"This deployment expected to be working with version '{expected_version}', but the app "
|
|
317
|
+
f"is now at version '{actual_version}'. The current deployment has been invalidated.\n\n"
|
|
318
|
+
"To avoid this in the future, you can either use a unique `name` for each deployment "
|
|
319
|
+
"or coordinate deployments to ensure concurrent upgrades to the same app don't overlap."
|
|
320
|
+
)
|
|
321
|
+
super().__init__(app_id, message)
|
|
322
|
+
|
|
323
|
+
def __str__(self):
|
|
324
|
+
return f"AppConcurrentUpgradeException: [{self.app_id}] :: {self.message}"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class AppDeletedDuringDeploymentException(AppDeploymentException):
|
|
328
|
+
"""Raised when an app is deleted while deployment is in progress."""
|
|
329
|
+
|
|
330
|
+
def __init__(self, app_id: str):
|
|
331
|
+
message = (
|
|
332
|
+
f"App '{app_id}' was deleted while this deployment was in progress.\n\n"
|
|
333
|
+
"This can happen when another process or user deletes the app during deployment. "
|
|
334
|
+
"Since apps can be programmatically created and deleted, concurrent operations "
|
|
335
|
+
"may conflict. If you did not intend to delete this app, check for other processes "
|
|
336
|
+
"or users that may be managing deployments in this perimeter."
|
|
337
|
+
)
|
|
338
|
+
super().__init__(app_id, message)
|
|
339
|
+
|
|
340
|
+
def __str__(self):
|
|
341
|
+
return f"AppDeletedDuringDeploymentException: [{self.app_id}] :: {self.message}"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from ..app_config import AppConfig
|
|
6
|
+
|
|
7
|
+
DEFAULT_BRANCH = "test"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Account for project / branch and the capsule input.
|
|
11
|
+
def capsule_input_overrides(app_config: "AppConfig", capsule_input: dict):
|
|
12
|
+
project = app_config.get_state("project", None)
|
|
13
|
+
# Update the project/branch related configurations.
|
|
14
|
+
if project is not None:
|
|
15
|
+
branch = app_config.get_state("branch", DEFAULT_BRANCH)
|
|
16
|
+
capsule_input["tags"].extend(
|
|
17
|
+
[dict(key="project", value=project), dict(key="branch", value=branch)]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
persistence = app_config.get_state("persistence", None)
|
|
21
|
+
if persistence is not None and persistence != "none":
|
|
22
|
+
capsule_input["persistence"] = persistence
|
|
23
|
+
|
|
24
|
+
capsule_input["generateStaticUrl"] = app_config.get_state(
|
|
25
|
+
"generate_static_url", False
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
model_asset_conf = app_config.get_state("models", None)
|
|
29
|
+
data_asset_conf = app_config.get_state("data", None)
|
|
30
|
+
code_info = _code_info(app_config)
|
|
31
|
+
# todo:fix me
|
|
32
|
+
_objects_key = "associatedObjects"
|
|
33
|
+
if model_asset_conf or data_asset_conf or code_info:
|
|
34
|
+
capsule_input[_objects_key] = {}
|
|
35
|
+
|
|
36
|
+
if model_asset_conf:
|
|
37
|
+
capsule_input[_objects_key]["models"] = [
|
|
38
|
+
{"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
|
|
39
|
+
for x in model_asset_conf
|
|
40
|
+
]
|
|
41
|
+
if data_asset_conf:
|
|
42
|
+
capsule_input[_objects_key]["data"] = [
|
|
43
|
+
{"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
|
|
44
|
+
for x in data_asset_conf
|
|
45
|
+
]
|
|
46
|
+
if code_info:
|
|
47
|
+
capsule_input[_objects_key]["code"] = code_info
|
|
48
|
+
|
|
49
|
+
return capsule_input
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _code_info(app_config: "AppConfig"):
|
|
53
|
+
from metaflow.metaflow_git import get_repository_info
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
from metaflow.metaflow_git import _call_git # type: ignore
|
|
57
|
+
except ImportError:
|
|
58
|
+
# Fallback if _call_git is not available
|
|
59
|
+
def _call_git(args, path=None):
|
|
60
|
+
return "", 1, True
|
|
61
|
+
|
|
62
|
+
package_dirs = app_config.get_state("packaging_directories")
|
|
63
|
+
if package_dirs is None:
|
|
64
|
+
return None
|
|
65
|
+
for directory in package_dirs:
|
|
66
|
+
repo_info = get_repository_info(directory)
|
|
67
|
+
if len(repo_info) == 0:
|
|
68
|
+
continue
|
|
69
|
+
git_log_info, returncode, failed = _call_git(
|
|
70
|
+
["log", "-1", "--pretty=%B"],
|
|
71
|
+
path=directory,
|
|
72
|
+
)
|
|
73
|
+
repo_url = repo_info["repo_url"]
|
|
74
|
+
if isinstance(repo_url, str):
|
|
75
|
+
_url = (
|
|
76
|
+
repo_url if not repo_url.endswith(".git") else repo_url.rstrip(".git")
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
_url = str(repo_url)
|
|
80
|
+
_code_info = {
|
|
81
|
+
"commitId": repo_info["commit_sha"],
|
|
82
|
+
"commitLink": os.path.join(_url, "commit", str(repo_info["commit_sha"])),
|
|
83
|
+
}
|
|
84
|
+
if not failed and returncode == 0 and isinstance(git_log_info, str):
|
|
85
|
+
_code_info["commitMessage"] = git_log_info.strip()
|
|
86
|
+
|
|
87
|
+
return _code_info
|
|
88
|
+
|
|
89
|
+
return None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from typing import Tuple, Union
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from .utils import safe_requests_wrapper
|
|
7
|
+
from .exceptions import OuterboundsConfigurationException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PerimeterExtractor:
|
|
11
|
+
|
|
12
|
+
config = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def for_ob_cli(
|
|
16
|
+
cls, config_dir: str, profile: str
|
|
17
|
+
) -> Union[Tuple[str, str], Tuple[None, None]]:
|
|
18
|
+
"""
|
|
19
|
+
This function will be called when we are trying to extract the perimeter
|
|
20
|
+
via the ob cli's execution. We will rely on the following logic:
|
|
21
|
+
1. check environment variables like OB_CURRENT_PERIMETER / OBP_PERIMETER
|
|
22
|
+
2. run init config to extract the perimeter related configurations.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Tuple[str, str] : Tuple containing perimeter name , API server url.
|
|
27
|
+
"""
|
|
28
|
+
from outerbounds.utils import metaflowconfig
|
|
29
|
+
|
|
30
|
+
perimeter = None
|
|
31
|
+
api_server = None
|
|
32
|
+
if os.environ.get("OB_CURRENT_PERIMETER") or os.environ.get("OBP_PERIMETER"):
|
|
33
|
+
perimeter = os.environ.get("OB_CURRENT_PERIMETER") or os.environ.get(
|
|
34
|
+
"OBP_PERIMETER"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if os.environ.get("OBP_API_SERVER"):
|
|
38
|
+
api_server = os.environ.get("OBP_API_SERVER")
|
|
39
|
+
|
|
40
|
+
if perimeter is None or api_server is None:
|
|
41
|
+
metaflow_config = metaflowconfig.init_config(config_dir, profile)
|
|
42
|
+
perimeter = metaflow_config.get("OBP_PERIMETER")
|
|
43
|
+
api_server = metaflowconfig.get_sanitized_url_from_config(
|
|
44
|
+
config_dir, profile, "OBP_API_SERVER"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return perimeter, api_server # type: ignore
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def during_programmatic_access(cls) -> Union[Tuple[str, str], Tuple[None, None]]:
|
|
51
|
+
from metaflow.metaflow_config_funcs import init_config
|
|
52
|
+
|
|
53
|
+
clean_url = (
|
|
54
|
+
lambda url: f"https://{url}".rstrip("/")
|
|
55
|
+
if not url.startswith("https://")
|
|
56
|
+
else url
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
config = init_config()
|
|
60
|
+
api_server, perimeter, integrations_url = None, None, None
|
|
61
|
+
perimeter = config.get(
|
|
62
|
+
"OBP_PERIMETER", os.environ.get("OBP_PERIMETER", perimeter)
|
|
63
|
+
)
|
|
64
|
+
if perimeter is None:
|
|
65
|
+
raise OuterboundsConfigurationException("OBP_PERIMETER")
|
|
66
|
+
|
|
67
|
+
api_server = config.get(
|
|
68
|
+
"OBP_API_SERVER", os.environ.get("OBP_API_SERVER", api_server)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if api_server is not None and not api_server.startswith("https://"):
|
|
72
|
+
api_server = clean_url(api_server)
|
|
73
|
+
|
|
74
|
+
if api_server is not None:
|
|
75
|
+
return perimeter, api_server
|
|
76
|
+
|
|
77
|
+
integrations_url = config.get(
|
|
78
|
+
"OBP_INTEGRATIONS_URL", os.environ.get("OBP_INTEGRATIONS_URL", None)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if integrations_url is not None and not integrations_url.startswith("https://"):
|
|
82
|
+
integrations_url = clean_url(integrations_url)
|
|
83
|
+
|
|
84
|
+
if integrations_url is not None:
|
|
85
|
+
api_server = integrations_url.rstrip("/integrations")
|
|
86
|
+
|
|
87
|
+
if api_server is None:
|
|
88
|
+
raise OuterboundsConfigurationException("OBP_API_SERVER")
|
|
89
|
+
|
|
90
|
+
return perimeter, api_server
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def config_during_programmatic_access(cls) -> dict:
|
|
94
|
+
#!HACK: Resolving remote configs is a PITA (all the variable piping we need to do via metaflow)
|
|
95
|
+
# So instead we will just derive the URL. We are in this situation because its a pain
|
|
96
|
+
# to load configurations at arbitrary points in the runtime.
|
|
97
|
+
if cls.config is not None:
|
|
98
|
+
return json.loads(json.dumps(cls.config)) # Return fresh copy
|
|
99
|
+
from metaflow.metaflow_config import SERVICE_HEADERS
|
|
100
|
+
|
|
101
|
+
perimeter, api_server = cls.during_programmatic_access()
|
|
102
|
+
response = safe_requests_wrapper(
|
|
103
|
+
requests.get,
|
|
104
|
+
f"{api_server}/v1/perimeters/{perimeter}/metaflowconfigs/default",
|
|
105
|
+
headers=SERVICE_HEADERS,
|
|
106
|
+
)
|
|
107
|
+
if response.status_code >= 400:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
f"Server error: {response.text}. Please reach out to your Outerbounds support team."
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
remote_config = response.json()
|
|
113
|
+
|
|
114
|
+
if not remote_config.get("config"):
|
|
115
|
+
raise json.JSONDecodeError
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
"Exception retrieving remote outerbounds configuration. "
|
|
119
|
+
"Please reach out to Outerbounds suport team with this stack trace."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
cls.config = remote_config.get("config")
|
|
123
|
+
return json.loads(json.dumps(cls.config)) # Return fresh copy
|