ob-metaflow-extensions 1.1.175rc2__py2.py3-none-any.whl → 1.1.175rc4__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.
Potentially problematic release.
This version of ob-metaflow-extensions might be problematic. Click here for more details.
- metaflow_extensions/outerbounds/plugins/__init__.py +1 -2
- metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -3
- metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +112 -0
- metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +9 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +41 -15
- metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +6 -6
- metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +25 -9
- metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +1 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +110 -50
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +131 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +353 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +176 -50
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +4 -4
- metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +132 -0
- metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +44 -2
- metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +1 -0
- metaflow_extensions/outerbounds/toplevel/ob_internal.py +1 -0
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/METADATA +1 -1
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/RECORD +21 -18
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,7 @@ No external dependencies required - uses only Python standard library.
|
|
|
12
12
|
import os
|
|
13
13
|
import json
|
|
14
14
|
from typing import Any, Dict, List, Optional, Union, Type
|
|
15
|
-
|
|
15
|
+
import re
|
|
16
16
|
|
|
17
17
|
from .config_utils import (
|
|
18
18
|
ConfigField,
|
|
@@ -41,9 +41,142 @@ class AuthType:
|
|
|
41
41
|
return [cls.BROWSER, cls.API]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class UnitParser:
|
|
45
|
+
UNIT_FREE_REGEX = r"^\d+$"
|
|
46
|
+
|
|
47
|
+
metrics = {
|
|
48
|
+
"memory": {
|
|
49
|
+
"default_unit": "Mi",
|
|
50
|
+
"requires_unit": True, # if a Unit free value is provided then we will add the default unit to it.
|
|
51
|
+
# Regex to match values with units (e.g., "512Mi", "4Gi", "1024Ki")
|
|
52
|
+
"unit_regex": r"^\d+(\.\d+)?(Ki|Mi|Gi|Ti|Pi|Ei)$",
|
|
53
|
+
},
|
|
54
|
+
"cpu": {
|
|
55
|
+
"default_unit": None,
|
|
56
|
+
"requires_unit": False, # if a Unit free value is provided then we will not add the default unit to it.
|
|
57
|
+
# Accepts values like 400m, 4, 0.4, 1000n, etc.
|
|
58
|
+
# Regex to match values with units (e.g., "400m", "1000n", "2", "0.5")
|
|
59
|
+
"unit_regex": r"^(\d+(\.\d+)?(m|n)?|\d+(\.\d+)?)$",
|
|
60
|
+
},
|
|
61
|
+
"disk": {
|
|
62
|
+
"default_unit": "Mi",
|
|
63
|
+
"requires_unit": True, # if a Unit free value is provided then we will add the default unit to it.
|
|
64
|
+
# Regex to match values with units (e.g., "100Mi", "1Gi", "500Ki")
|
|
65
|
+
"unit_regex": r"^\d+(\.\d+)?(Ki|Mi|Gi|Ti|Pi|Ei)$",
|
|
66
|
+
},
|
|
67
|
+
"gpu": {
|
|
68
|
+
"default_unit": None,
|
|
69
|
+
"requires_unit": False,
|
|
70
|
+
# Regex to match values with units (usually just integer count, e.g., "1", "2")
|
|
71
|
+
"unit_regex": r"^\d+$",
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def __init__(self, metric_name: str):
|
|
76
|
+
self.metric_name = metric_name
|
|
77
|
+
|
|
78
|
+
def validate(self, value: str):
|
|
79
|
+
if self.metrics[self.metric_name]["requires_unit"]:
|
|
80
|
+
if not re.match(self.metrics[self.metric_name]["unit_regex"], value):
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def process(self, value: str):
|
|
85
|
+
value = str(value)
|
|
86
|
+
if self.metrics[self.metric_name]["requires_unit"]:
|
|
87
|
+
if re.match(self.UNIT_FREE_REGEX, value):
|
|
88
|
+
# This means the value is unit free and we need to add the default unit to it.
|
|
89
|
+
value = "%s%s" % (
|
|
90
|
+
value.strip(),
|
|
91
|
+
self.metrics[self.metric_name]["default_unit"],
|
|
92
|
+
)
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
if re.match(self.metrics[self.metric_name]["unit_regex"], value):
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
def parse(self, value: str):
|
|
101
|
+
if value is None:
|
|
102
|
+
return None
|
|
103
|
+
return self.process(value)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def validation_wrapper_fn(
|
|
107
|
+
metric_name: str,
|
|
108
|
+
):
|
|
109
|
+
def validation_fn(value: str):
|
|
110
|
+
if value is None:
|
|
111
|
+
return True
|
|
112
|
+
field_info = ResourceConfig._get_field(ResourceConfig, metric_name) # type: ignore
|
|
113
|
+
parser = UnitParser(metric_name)
|
|
114
|
+
validation = parser.validate(value)
|
|
115
|
+
if not validation:
|
|
116
|
+
raise ConfigValidationFailedException(
|
|
117
|
+
field_name=metric_name,
|
|
118
|
+
field_info=field_info,
|
|
119
|
+
current_value=value,
|
|
120
|
+
message=f"Invalid value for `{metric_name}`. Must be of the format {parser.metrics[metric_name]['unit_regex']}.",
|
|
121
|
+
)
|
|
122
|
+
return validation
|
|
123
|
+
|
|
124
|
+
return validation_fn
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BasicValidations:
|
|
128
|
+
def __init__(self, config_meta_class, field_name):
|
|
129
|
+
self.config_meta_class = config_meta_class
|
|
130
|
+
self.field_name = field_name
|
|
131
|
+
|
|
132
|
+
def _get_field(self):
|
|
133
|
+
return self.config_meta_class._get_field(self.config_meta_class, self.field_name) # type: ignore
|
|
134
|
+
|
|
135
|
+
def enum_validation(self, enums: List[str], current_value):
|
|
136
|
+
if current_value not in enums:
|
|
137
|
+
raise ConfigValidationFailedException(
|
|
138
|
+
field_name=self.field_name,
|
|
139
|
+
field_info=self._get_field(),
|
|
140
|
+
current_value=current_value,
|
|
141
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must be one of: {' '.join(enums)}",
|
|
142
|
+
)
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
def range_validation(self, min_value, max_value, current_value):
|
|
146
|
+
if current_value < min_value or current_value > max_value:
|
|
147
|
+
raise ConfigValidationFailedException(
|
|
148
|
+
field_name=self.field_name,
|
|
149
|
+
field_info=self._get_field(),
|
|
150
|
+
current_value=current_value,
|
|
151
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must be between {min_value} and {max_value}",
|
|
152
|
+
)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def length_validation(self, max_length, current_value):
|
|
156
|
+
if len(current_value) > max_length:
|
|
157
|
+
raise ConfigValidationFailedException(
|
|
158
|
+
field_name=self.field_name,
|
|
159
|
+
field_info=self._get_field(),
|
|
160
|
+
current_value=current_value,
|
|
161
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must be less than {max_length}",
|
|
162
|
+
)
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def regex_validation(self, regex, current_value):
|
|
166
|
+
if not re.match(regex, current_value):
|
|
167
|
+
raise ConfigValidationFailedException(
|
|
168
|
+
field_name=self.field_name,
|
|
169
|
+
field_info=self._get_field(),
|
|
170
|
+
current_value=current_value,
|
|
171
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must match regex {regex}",
|
|
172
|
+
)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
44
176
|
class ResourceConfig(metaclass=ConfigMeta):
|
|
45
177
|
"""Resource configuration for the app."""
|
|
46
178
|
|
|
179
|
+
# TODO: Add Unit Validation/Parsing Support for the Fields.
|
|
47
180
|
cpu = ConfigField(
|
|
48
181
|
default="1",
|
|
49
182
|
cli_meta=CLIOption(
|
|
@@ -53,6 +186,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
53
186
|
),
|
|
54
187
|
field_type=str,
|
|
55
188
|
example="500m",
|
|
189
|
+
validation_fn=UnitParser.validation_wrapper_fn("cpu"),
|
|
190
|
+
parsing_fn=UnitParser("cpu").parse,
|
|
56
191
|
)
|
|
57
192
|
memory = ConfigField(
|
|
58
193
|
default="4Gi",
|
|
@@ -63,6 +198,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
63
198
|
),
|
|
64
199
|
field_type=str,
|
|
65
200
|
example="512Mi",
|
|
201
|
+
validation_fn=UnitParser.validation_wrapper_fn("memory"),
|
|
202
|
+
parsing_fn=UnitParser("memory").parse,
|
|
66
203
|
)
|
|
67
204
|
gpu = ConfigField(
|
|
68
205
|
cli_meta=CLIOption(
|
|
@@ -72,6 +209,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
72
209
|
),
|
|
73
210
|
field_type=str,
|
|
74
211
|
example="1",
|
|
212
|
+
validation_fn=UnitParser.validation_wrapper_fn("gpu"),
|
|
213
|
+
parsing_fn=UnitParser("gpu").parse,
|
|
75
214
|
)
|
|
76
215
|
disk = ConfigField(
|
|
77
216
|
default="20Gi",
|
|
@@ -82,6 +221,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
82
221
|
),
|
|
83
222
|
field_type=str,
|
|
84
223
|
example="1Gi",
|
|
224
|
+
validation_fn=UnitParser.validation_wrapper_fn("disk"),
|
|
225
|
+
parsing_fn=UnitParser("disk").parse,
|
|
85
226
|
)
|
|
86
227
|
|
|
87
228
|
|
|
@@ -156,14 +297,11 @@ class AuthConfig(metaclass=ConfigMeta):
|
|
|
156
297
|
|
|
157
298
|
@staticmethod
|
|
158
299
|
def validate(auth_config: "AuthConfig"):
|
|
159
|
-
if auth_config.type is
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
message=f"Invalid auth type: {auth_config.type}. Must be one of {AuthType.choices()}",
|
|
165
|
-
)
|
|
166
|
-
return True
|
|
300
|
+
if auth_config.type is None:
|
|
301
|
+
return True
|
|
302
|
+
return BasicValidations(AuthConfig, "type").enum_validation(
|
|
303
|
+
AuthType.choices(), auth_config.type
|
|
304
|
+
)
|
|
167
305
|
|
|
168
306
|
|
|
169
307
|
class ReplicaConfig(metaclass=ConfigMeta):
|
|
@@ -207,12 +345,14 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
207
345
|
replica_config.fixed is None,
|
|
208
346
|
]
|
|
209
347
|
):
|
|
348
|
+
# if nothing is set then set
|
|
210
349
|
replica_config.fixed = 1
|
|
350
|
+
elif replica_config.min is not None and replica_config.max is None:
|
|
351
|
+
replica_config.max = replica_config.min
|
|
211
352
|
return
|
|
212
353
|
|
|
213
354
|
@staticmethod
|
|
214
355
|
def validate(replica_config: "ReplicaConfig"):
|
|
215
|
-
# TODO: Have a better validation story.
|
|
216
356
|
both_min_max_set = (
|
|
217
357
|
replica_config.min is not None and replica_config.max is not None
|
|
218
358
|
)
|
|
@@ -226,17 +366,17 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
226
366
|
def _greater_than_equals_zero(x):
|
|
227
367
|
return x is not None and x >= 0
|
|
228
368
|
|
|
229
|
-
if both_min_max_set and replica_config.min > replica_config.max:
|
|
369
|
+
if both_min_max_set and replica_config.min > replica_config.max: # type: ignore
|
|
230
370
|
raise ConfigValidationFailedException(
|
|
231
371
|
field_name="min",
|
|
232
|
-
field_info=replica_config._get_field("min"),
|
|
372
|
+
field_info=replica_config._get_field("min"), # type: ignore
|
|
233
373
|
current_value=replica_config.min,
|
|
234
374
|
message="Min replicas cannot be greater than max replicas",
|
|
235
375
|
)
|
|
236
376
|
if fixed_set and any_min_max_set:
|
|
237
377
|
raise ConfigValidationFailedException(
|
|
238
378
|
field_name="fixed",
|
|
239
|
-
field_info=replica_config._get_field("fixed"),
|
|
379
|
+
field_info=replica_config._get_field("fixed"), # type: ignore
|
|
240
380
|
current_value=replica_config.fixed,
|
|
241
381
|
message="Fixed replicas cannot be set when min or max replicas are set",
|
|
242
382
|
)
|
|
@@ -244,15 +384,15 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
244
384
|
if max_is_set and not min_is_set:
|
|
245
385
|
raise ConfigValidationFailedException(
|
|
246
386
|
field_name="min",
|
|
247
|
-
field_info=replica_config._get_field("min"),
|
|
387
|
+
field_info=replica_config._get_field("min"), # type: ignore
|
|
248
388
|
current_value=replica_config.min,
|
|
249
389
|
message="If max replicas is set then min replicas must be set too.",
|
|
250
390
|
)
|
|
251
391
|
|
|
252
|
-
if fixed_set and replica_config.fixed < 0:
|
|
392
|
+
if fixed_set and replica_config.fixed < 0: # type: ignore
|
|
253
393
|
raise ConfigValidationFailedException(
|
|
254
394
|
field_name="fixed",
|
|
255
|
-
field_info=replica_config._get_field("fixed"),
|
|
395
|
+
field_info=replica_config._get_field("fixed"), # type: ignore
|
|
256
396
|
current_value=replica_config.fixed,
|
|
257
397
|
message="Fixed replicas cannot be less than 0",
|
|
258
398
|
)
|
|
@@ -260,7 +400,7 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
260
400
|
if min_is_set and not _greater_than_equals_zero(replica_config.min):
|
|
261
401
|
raise ConfigValidationFailedException(
|
|
262
402
|
field_name="min",
|
|
263
|
-
field_info=replica_config._get_field("min"),
|
|
403
|
+
field_info=replica_config._get_field("min"), # type: ignore
|
|
264
404
|
current_value=replica_config.min,
|
|
265
405
|
message="Min replicas cannot be less than 0",
|
|
266
406
|
)
|
|
@@ -268,7 +408,7 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
268
408
|
if max_is_set and not _greater_than_equals_zero(replica_config.max):
|
|
269
409
|
raise ConfigValidationFailedException(
|
|
270
410
|
field_name="max",
|
|
271
|
-
field_info=replica_config._get_field("max"),
|
|
411
|
+
field_info=replica_config._get_field("max"), # type: ignore
|
|
272
412
|
current_value=replica_config.max,
|
|
273
413
|
message="Max replicas cannot be less than 0",
|
|
274
414
|
)
|
|
@@ -346,7 +486,7 @@ class DependencyConfig(metaclass=ConfigMeta):
|
|
|
346
486
|
):
|
|
347
487
|
raise ConfigValidationFailedException(
|
|
348
488
|
field_name="from_requirements_file",
|
|
349
|
-
field_info=dependency_config._get_field("from_requirements_file"),
|
|
489
|
+
field_info=dependency_config._get_field("from_requirements_file"), # type: ignore
|
|
350
490
|
current_value=dependency_config.from_requirements_file,
|
|
351
491
|
message="Cannot set from_requirements_file and from_pyproject_toml at the same time",
|
|
352
492
|
)
|
|
@@ -358,7 +498,7 @@ class DependencyConfig(metaclass=ConfigMeta):
|
|
|
358
498
|
):
|
|
359
499
|
raise ConfigValidationFailedException(
|
|
360
500
|
field_name="pypi" if dependency_config.pypi else "conda",
|
|
361
|
-
field_info=dependency_config._get_field(
|
|
501
|
+
field_info=dependency_config._get_field( # type: ignore
|
|
362
502
|
"pypi" if dependency_config.pypi else "conda"
|
|
363
503
|
),
|
|
364
504
|
current_value=dependency_config.pypi or dependency_config.conda,
|
|
@@ -395,27 +535,17 @@ class BasicAppValidations:
|
|
|
395
535
|
def name(name):
|
|
396
536
|
if name is None:
|
|
397
537
|
return True
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
message="Name cannot be longer than 128 characters",
|
|
404
|
-
)
|
|
405
|
-
return True
|
|
538
|
+
regex = r"^[a-z0-9-]+$" # Only allow lowercase letters, numbers, and hyphens
|
|
539
|
+
validator = BasicValidations(CoreConfig, "name")
|
|
540
|
+
return validator.length_validation(20, name) and validator.regex_validation(
|
|
541
|
+
regex, name
|
|
542
|
+
)
|
|
406
543
|
|
|
407
544
|
@staticmethod
|
|
408
545
|
def port(port):
|
|
409
546
|
if port is None:
|
|
410
547
|
return True
|
|
411
|
-
|
|
412
|
-
raise ConfigValidationFailedException(
|
|
413
|
-
field_name="port",
|
|
414
|
-
field_info=CoreConfig._get_field(CoreConfig, "port"),
|
|
415
|
-
current_value=port,
|
|
416
|
-
message="Port must be between 1 and 65535",
|
|
417
|
-
)
|
|
418
|
-
return True
|
|
548
|
+
return BasicValidations(CoreConfig, "port").range_validation(1, 65535, port)
|
|
419
549
|
|
|
420
550
|
@staticmethod
|
|
421
551
|
def tags(tags):
|
|
@@ -424,7 +554,7 @@ class BasicAppValidations:
|
|
|
424
554
|
if not all(isinstance(tag, dict) and len(tag) == 1 for tag in tags):
|
|
425
555
|
raise ConfigValidationFailedException(
|
|
426
556
|
field_name="tags",
|
|
427
|
-
field_info=CoreConfig._get_field(CoreConfig, "tags"),
|
|
557
|
+
field_info=CoreConfig._get_field(CoreConfig, "tags"), # type: ignore
|
|
428
558
|
current_value=tags,
|
|
429
559
|
message="Tags must be a list of dictionaries with one key. Currently they are set to %s "
|
|
430
560
|
% (str(tags)),
|
|
@@ -439,9 +569,10 @@ class BasicAppValidations:
|
|
|
439
569
|
if not isinstance(secrets, list):
|
|
440
570
|
raise ConfigValidationFailedException(
|
|
441
571
|
field_name="secrets",
|
|
442
|
-
field_info=CoreConfig._get_field(CoreConfig, "secrets"),
|
|
572
|
+
field_info=CoreConfig._get_field(CoreConfig, "secrets"), # type: ignore
|
|
443
573
|
current_value=secrets,
|
|
444
|
-
message="Secrets must be a list of strings"
|
|
574
|
+
message="Secrets must be a list of strings. Currently they are set to %s "
|
|
575
|
+
% (str(secrets)),
|
|
445
576
|
)
|
|
446
577
|
from ..validations import secrets_validator
|
|
447
578
|
|
|
@@ -450,7 +581,7 @@ class BasicAppValidations:
|
|
|
450
581
|
except Exception as e:
|
|
451
582
|
raise ConfigValidationFailedException(
|
|
452
583
|
field_name="secrets",
|
|
453
|
-
field_info=CoreConfig._get_field(CoreConfig, "secrets"),
|
|
584
|
+
field_info=CoreConfig._get_field(CoreConfig, "secrets"), # type: ignore
|
|
454
585
|
current_value=secrets,
|
|
455
586
|
message=f"Secrets validation failed, {e}",
|
|
456
587
|
)
|
|
@@ -460,14 +591,9 @@ class BasicAppValidations:
|
|
|
460
591
|
def persistence(persistence):
|
|
461
592
|
if persistence is None:
|
|
462
593
|
return True
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
field_info=CoreConfig._get_field(CoreConfig, "persistence"),
|
|
467
|
-
current_value=persistence,
|
|
468
|
-
message=f"Persistence must be one of: {['none', 'postgres']}",
|
|
469
|
-
)
|
|
470
|
-
return True
|
|
594
|
+
return BasicValidations(CoreConfig, "persistence").enum_validation(
|
|
595
|
+
["none", "postgres"], persistence
|
|
596
|
+
)
|
|
471
597
|
|
|
472
598
|
|
|
473
599
|
class CoreConfig(metaclass=ConfigMeta):
|
|
@@ -787,7 +913,7 @@ How to read this schema:
|
|
|
787
913
|
merged_config = CoreConfig()
|
|
788
914
|
|
|
789
915
|
# Process each field according to its behavior
|
|
790
|
-
for field_name, field_info in CoreConfig._fields.items():
|
|
916
|
+
for field_name, field_info in CoreConfig._fields.items(): # type: ignore
|
|
791
917
|
base_value = getattr(base_config, field_name, None)
|
|
792
918
|
override_value = getattr(override_config, field_name, None)
|
|
793
919
|
|
|
@@ -852,7 +978,7 @@ How to read this schema:
|
|
|
852
978
|
for v in value:
|
|
853
979
|
_env_dict.update(v)
|
|
854
980
|
return _env_dict
|
|
855
|
-
if type(value) == tuple:
|
|
981
|
+
if type(value) == tuple or type(value) == list:
|
|
856
982
|
obj = list(x for x in source_data[key])
|
|
857
983
|
if len(obj) == 0:
|
|
858
984
|
return None # Dont return Empty Lists so that we can set Nones
|
|
@@ -94,24 +94,24 @@ properties:
|
|
|
94
94
|
required: []
|
|
95
95
|
properties:
|
|
96
96
|
cpu:
|
|
97
|
-
description: CPU resource request and limit.
|
|
97
|
+
description: CPU resource request and limit. (validation applied)
|
|
98
98
|
type: string
|
|
99
99
|
default: '1'
|
|
100
100
|
example: 500m
|
|
101
101
|
mutation_behavior: union
|
|
102
102
|
memory:
|
|
103
|
-
description: Memory resource request and limit.
|
|
103
|
+
description: Memory resource request and limit. (validation applied)
|
|
104
104
|
type: string
|
|
105
105
|
default: 4Gi
|
|
106
106
|
example: 512Mi
|
|
107
107
|
mutation_behavior: union
|
|
108
108
|
gpu:
|
|
109
|
-
description: GPU resource request and limit.
|
|
109
|
+
description: GPU resource request and limit. (validation applied)
|
|
110
110
|
type: string
|
|
111
111
|
example: '1'
|
|
112
112
|
mutation_behavior: union
|
|
113
113
|
disk:
|
|
114
|
-
description: Storage resource request and limit.
|
|
114
|
+
description: Storage resource request and limit. (validation applied)
|
|
115
115
|
type: string
|
|
116
116
|
default: 20Gi
|
|
117
117
|
example: 1Gi
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from .config import TypedCoreConfig
|
|
2
|
+
|
|
3
|
+
from ._state_machine import DEPLOYMENT_READY_CONDITIONS
|
|
4
|
+
from .app_config import AppConfig, AppConfigError
|
|
5
|
+
from .capsule import CapsuleDeployer, list_and_filter_capsules
|
|
6
|
+
from functools import partial
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Type
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AppDeployer(TypedCoreConfig):
|
|
12
|
+
""" """
|
|
13
|
+
|
|
14
|
+
__init__ = TypedCoreConfig.__init__
|
|
15
|
+
|
|
16
|
+
_app_config: AppConfig
|
|
17
|
+
|
|
18
|
+
_state = {}
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def app_config(self) -> AppConfig:
|
|
22
|
+
if not hasattr(self, "_app_config"):
|
|
23
|
+
self._app_config = AppConfig(self._config)
|
|
24
|
+
return self._app_config
|
|
25
|
+
|
|
26
|
+
# Things that need to be set before deploy
|
|
27
|
+
@classmethod
|
|
28
|
+
def _set_state(
|
|
29
|
+
cls,
|
|
30
|
+
perimeter: str,
|
|
31
|
+
api_url: str,
|
|
32
|
+
code_package_url: str = None,
|
|
33
|
+
code_package_key: str = None,
|
|
34
|
+
name: str = None,
|
|
35
|
+
image: str = None,
|
|
36
|
+
):
|
|
37
|
+
cls._state["perimeter"] = perimeter
|
|
38
|
+
cls._state["api_url"] = api_url
|
|
39
|
+
cls._state["code_package_url"] = code_package_url
|
|
40
|
+
cls._state["code_package_key"] = code_package_key
|
|
41
|
+
cls._state["name"] = name
|
|
42
|
+
cls._state["image"] = image
|
|
43
|
+
|
|
44
|
+
def deploy(
|
|
45
|
+
self,
|
|
46
|
+
readiness_condition=DEPLOYMENT_READY_CONDITIONS.ATLEAST_ONE_RUNNING,
|
|
47
|
+
max_wait_time=600,
|
|
48
|
+
readiness_wait_time=10,
|
|
49
|
+
logger_fn=partial(print, file=sys.stderr),
|
|
50
|
+
status_file=None,
|
|
51
|
+
no_loader=False,
|
|
52
|
+
**kwargs,
|
|
53
|
+
):
|
|
54
|
+
# Name setting from top level if none is set in the code
|
|
55
|
+
if self.app_config._core_config.name is None:
|
|
56
|
+
self.app_config._core_config.name = self._state["name"]
|
|
57
|
+
|
|
58
|
+
self.app_config.commit()
|
|
59
|
+
|
|
60
|
+
# Set any state that might have been passed down from the top level
|
|
61
|
+
for k, v in self._state.items():
|
|
62
|
+
if self.app_config.get_state(k) is None:
|
|
63
|
+
self.app_config.set_state(k, v)
|
|
64
|
+
|
|
65
|
+
capsule = CapsuleDeployer(
|
|
66
|
+
self.app_config,
|
|
67
|
+
self._state["api_url"],
|
|
68
|
+
create_timeout=max_wait_time,
|
|
69
|
+
debug_dir=None,
|
|
70
|
+
success_terminal_state_condition=readiness_condition,
|
|
71
|
+
readiness_wait_time=readiness_wait_time,
|
|
72
|
+
logger_fn=logger_fn,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
currently_present_capsules = list_and_filter_capsules(
|
|
76
|
+
capsule.capsule_api,
|
|
77
|
+
None,
|
|
78
|
+
None,
|
|
79
|
+
capsule.name,
|
|
80
|
+
None,
|
|
81
|
+
None,
|
|
82
|
+
None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
force_upgrade = self.app_config.get_state("force_upgrade", False)
|
|
86
|
+
|
|
87
|
+
if len(currently_present_capsules) > 0:
|
|
88
|
+
# Only update the capsule if there is no upgrade in progress
|
|
89
|
+
# Only update a "already updating" capsule if the `--force-upgrade` flag is provided.
|
|
90
|
+
_curr_cap = currently_present_capsules[0]
|
|
91
|
+
this_capsule_is_being_updated = _curr_cap.get("status", {}).get(
|
|
92
|
+
"updateInProgress", False
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if this_capsule_is_being_updated and not force_upgrade:
|
|
96
|
+
_upgrader = _curr_cap.get("metadata", {}).get("lastModifiedBy", None)
|
|
97
|
+
message = f"{capsule.capsule_type} is currently being upgraded"
|
|
98
|
+
if _upgrader:
|
|
99
|
+
message = (
|
|
100
|
+
f"{capsule.capsule_type} is currently being upgraded. Upgrade was launched by {_upgrader}. "
|
|
101
|
+
"If you wish to force upgrade, you can do so by providing the `--force-upgrade` flag."
|
|
102
|
+
)
|
|
103
|
+
raise AppConfigError(message)
|
|
104
|
+
|
|
105
|
+
logger_fn(
|
|
106
|
+
f"🚀 {'' if not force_upgrade else 'Force'} Upgrading {capsule.capsule_type.lower()} `{capsule.name}`....",
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
logger_fn(
|
|
110
|
+
f"🚀 Deploying {capsule.capsule_type.lower()} `{capsule.name}`....",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
capsule.create()
|
|
114
|
+
final_status = capsule.wait_for_terminal_state()
|
|
115
|
+
return final_status
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class apps:
|
|
119
|
+
|
|
120
|
+
_name_prefix = None
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def set_name_prefix(cls, name_prefix: str):
|
|
124
|
+
cls._name_prefix = name_prefix
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def name_prefix(self) -> str:
|
|
128
|
+
return self._name_prefix
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def Deployer(self) -> Type[AppDeployer]:
|
|
132
|
+
return AppDeployer
|
|
@@ -41,5 +41,47 @@ class PerimeterExtractor:
|
|
|
41
41
|
|
|
42
42
|
@classmethod
|
|
43
43
|
def during_metaflow_execution(cls) -> str:
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
from metaflow.metaflow_config_funcs import init_config
|
|
45
|
+
|
|
46
|
+
clean_url = (
|
|
47
|
+
lambda url: f"https://{url}".rstrip("/")
|
|
48
|
+
if not url.startswith("https://")
|
|
49
|
+
else url
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
config = init_config()
|
|
53
|
+
api_server, perimeter, integrations_url = None, None, None
|
|
54
|
+
perimeter = config.get(
|
|
55
|
+
"OBP_PERIMETER", os.environ.get("OBP_PERIMETER", perimeter)
|
|
56
|
+
)
|
|
57
|
+
if perimeter is None:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"Perimeter not found in metaflow config or environment variables"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
api_server = config.get(
|
|
63
|
+
"OBP_API_SERVER", os.environ.get("OBP_API_SERVER", api_server)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if api_server is not None and not api_server.startswith("https://"):
|
|
67
|
+
api_server = clean_url(api_server)
|
|
68
|
+
|
|
69
|
+
if api_server is not None:
|
|
70
|
+
return perimeter, api_server
|
|
71
|
+
|
|
72
|
+
integrations_url = config.get(
|
|
73
|
+
"OBP_INTEGRATIONS_URL", os.environ.get("OBP_INTEGRATIONS_URL", None)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if integrations_url is not None and not integrations_url.startswith("https://"):
|
|
77
|
+
integrations_url = clean_url(integrations_url)
|
|
78
|
+
|
|
79
|
+
if integrations_url is not None:
|
|
80
|
+
api_server = integrations_url.rstrip("/integrations")
|
|
81
|
+
|
|
82
|
+
if api_server is None:
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
"API server not found in metaflow config or environment variables"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return perimeter, api_server
|