ob-metaflow 2.15.18.1__py2.py3-none-any.whl → 2.16.0.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.
Potentially problematic release.
This version of ob-metaflow might be problematic. Click here for more details.
- metaflow/__init__.py +7 -1
- metaflow/_vendor/imghdr/__init__.py +180 -0
- metaflow/cli.py +16 -1
- metaflow/cli_components/init_cmd.py +1 -0
- metaflow/cli_components/run_cmds.py +6 -2
- metaflow/client/core.py +22 -30
- metaflow/cmd/develop/stub_generator.py +19 -2
- metaflow/datastore/task_datastore.py +0 -1
- metaflow/debug.py +5 -0
- metaflow/decorators.py +230 -70
- metaflow/extension_support/__init__.py +15 -8
- metaflow/extension_support/_empty_file.py +2 -2
- metaflow/flowspec.py +80 -53
- metaflow/graph.py +24 -2
- metaflow/meta_files.py +13 -0
- metaflow/metadata_provider/metadata.py +7 -1
- metaflow/metaflow_config.py +5 -0
- metaflow/metaflow_environment.py +82 -25
- metaflow/metaflow_version.py +1 -1
- metaflow/package/__init__.py +664 -0
- metaflow/packaging_sys/__init__.py +870 -0
- metaflow/packaging_sys/backend.py +113 -0
- metaflow/packaging_sys/distribution_support.py +153 -0
- metaflow/packaging_sys/tar_backend.py +86 -0
- metaflow/packaging_sys/utils.py +91 -0
- metaflow/packaging_sys/v1.py +476 -0
- metaflow/plugins/__init__.py +3 -0
- metaflow/plugins/airflow/airflow.py +11 -1
- metaflow/plugins/airflow/airflow_cli.py +15 -4
- metaflow/plugins/argo/argo_workflows.py +346 -301
- metaflow/plugins/argo/argo_workflows_cli.py +16 -4
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/aws/aws_utils.py +1 -1
- metaflow/plugins/aws/batch/batch.py +22 -3
- metaflow/plugins/aws/batch/batch_cli.py +3 -0
- metaflow/plugins/aws/batch/batch_decorator.py +13 -5
- metaflow/plugins/aws/step_functions/step_functions.py +10 -1
- metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_decorator.py +57 -6
- metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
- metaflow/plugins/cards/card_modules/test_cards.py +16 -0
- metaflow/plugins/cards/metadata.py +22 -0
- metaflow/plugins/exit_hook/__init__.py +0 -0
- metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
- metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
- metaflow/plugins/kubernetes/kubernetes.py +8 -1
- metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
- metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
- metaflow/plugins/package_cli.py +25 -23
- metaflow/plugins/parallel_decorator.py +4 -2
- metaflow/plugins/pypi/bootstrap.py +8 -2
- metaflow/plugins/pypi/conda_decorator.py +39 -82
- metaflow/plugins/pypi/conda_environment.py +6 -2
- metaflow/plugins/pypi/pypi_decorator.py +4 -4
- metaflow/plugins/secrets/__init__.py +3 -0
- metaflow/plugins/secrets/secrets_decorator.py +9 -173
- metaflow/plugins/secrets/secrets_func.py +49 -0
- metaflow/plugins/secrets/secrets_spec.py +101 -0
- metaflow/plugins/secrets/utils.py +74 -0
- metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
- metaflow/plugins/timeout_decorator.py +0 -1
- metaflow/plugins/uv/bootstrap.py +11 -0
- metaflow/plugins/uv/uv_environment.py +4 -2
- metaflow/pylint_wrapper.py +5 -1
- metaflow/runner/click_api.py +5 -4
- metaflow/runner/metaflow_runner.py +16 -1
- metaflow/runner/subprocess_manager.py +14 -2
- metaflow/runtime.py +82 -11
- metaflow/task.py +91 -7
- metaflow/user_configs/config_options.py +13 -8
- metaflow/user_configs/config_parameters.py +0 -4
- metaflow/user_decorators/__init__.py +0 -0
- metaflow/user_decorators/common.py +144 -0
- metaflow/user_decorators/mutable_flow.py +499 -0
- metaflow/user_decorators/mutable_step.py +424 -0
- metaflow/user_decorators/user_flow_decorator.py +263 -0
- metaflow/user_decorators/user_step_decorator.py +712 -0
- metaflow/util.py +4 -1
- metaflow/version.py +1 -1
- {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Tiltfile +27 -2
- {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/METADATA +2 -2
- {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/RECORD +90 -70
- metaflow/info_file.py +0 -25
- metaflow/package.py +0 -203
- metaflow/user_configs/config_decorators.py +0 -568
- {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Makefile +0 -0
- {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/WHEEL +0 -0
- {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/entry_points.txt +0 -0
- {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/licenses/LICENSE +0 -0
- {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
import json
|
|
3
1
|
import os
|
|
4
2
|
import platform
|
|
5
3
|
import re
|
|
@@ -7,12 +5,9 @@ import sys
|
|
|
7
5
|
import tempfile
|
|
8
6
|
|
|
9
7
|
from metaflow.decorators import FlowDecorator, StepDecorator
|
|
10
|
-
from metaflow.extension_support import EXT_PKG
|
|
11
8
|
from metaflow.metadata_provider import MetaDatum
|
|
12
9
|
from metaflow.metaflow_environment import InvalidEnvironmentException
|
|
13
|
-
from metaflow.
|
|
14
|
-
|
|
15
|
-
from ...info_file import INFO_FILE
|
|
10
|
+
from metaflow.packaging_sys import ContentType
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
class CondaStepDecorator(StepDecorator):
|
|
@@ -45,26 +40,31 @@ class CondaStepDecorator(StepDecorator):
|
|
|
45
40
|
"python": None,
|
|
46
41
|
"disabled": None,
|
|
47
42
|
}
|
|
43
|
+
|
|
44
|
+
_metaflow_home = None
|
|
45
|
+
_addl_env_vars = None
|
|
46
|
+
|
|
48
47
|
# To define conda channels for the whole solve, users can specify
|
|
49
48
|
# CONDA_CHANNELS in their environment. For pinning specific packages to specific
|
|
50
49
|
# conda channels, users can specify channel::package as the package name.
|
|
51
50
|
|
|
52
|
-
def __init__(self, attributes=None, statically_defined=False):
|
|
51
|
+
def __init__(self, attributes=None, statically_defined=False, inserted_by=None):
|
|
53
52
|
self._attributes_with_user_values = (
|
|
54
53
|
set(attributes.keys()) if attributes is not None else set()
|
|
55
54
|
)
|
|
56
55
|
|
|
57
|
-
super(CondaStepDecorator, self).__init__(
|
|
56
|
+
super(CondaStepDecorator, self).__init__(
|
|
57
|
+
attributes, statically_defined, inserted_by
|
|
58
|
+
)
|
|
58
59
|
|
|
59
60
|
def init(self):
|
|
60
|
-
super(CondaStepDecorator, self).init()
|
|
61
|
-
|
|
62
61
|
# Support legacy 'libraries=' attribute for the decorator.
|
|
63
62
|
self.attributes["packages"] = {
|
|
64
63
|
**self.attributes["libraries"],
|
|
65
64
|
**self.attributes["packages"],
|
|
66
65
|
}
|
|
67
|
-
|
|
66
|
+
# Keep because otherwise make_decorator_spec will fail
|
|
67
|
+
self.attributes["libraries"] = {}
|
|
68
68
|
if self.attributes["packages"]:
|
|
69
69
|
self._attributes_with_user_values.add("packages")
|
|
70
70
|
|
|
@@ -152,67 +152,17 @@ class CondaStepDecorator(StepDecorator):
|
|
|
152
152
|
def runtime_init(self, flow, graph, package, run_id):
|
|
153
153
|
if self.disabled:
|
|
154
154
|
return
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
os.symlink(
|
|
166
|
-
info, os.path.join(self.metaflow_dir.name, os.path.basename(INFO_FILE))
|
|
155
|
+
# We need to make all the code package available to the user code in
|
|
156
|
+
# a temporary directory which will be added to the PYTHONPATH.
|
|
157
|
+
if self.__class__._metaflow_home is None:
|
|
158
|
+
# Do this ONCE per flow
|
|
159
|
+
self.__class__._metaflow_home = tempfile.TemporaryDirectory(dir="/tmp")
|
|
160
|
+
package.extract_into(
|
|
161
|
+
self.__class__._metaflow_home.name, ContentType.ALL_CONTENT
|
|
162
|
+
)
|
|
163
|
+
self.__class__._addl_env_vars = package.get_post_extract_env_vars(
|
|
164
|
+
package.package_metadata, self.__class__._metaflow_home.name
|
|
167
165
|
)
|
|
168
|
-
else:
|
|
169
|
-
# If there is no info file, we will actually create one in this new
|
|
170
|
-
# place because we won't be able to properly resolve the EXT_PKG extensions
|
|
171
|
-
# the same way as outside conda (looking at distributions, etc.). In a
|
|
172
|
-
# Conda environment, as shown below (where we set self.addl_paths), all
|
|
173
|
-
# EXT_PKG extensions are PYTHONPATH extensions. Instead of re-resolving,
|
|
174
|
-
# we use the resolved information that is written out to the INFO file.
|
|
175
|
-
with open(
|
|
176
|
-
os.path.join(self.metaflow_dir.name, os.path.basename(INFO_FILE)),
|
|
177
|
-
mode="wt",
|
|
178
|
-
encoding="utf-8",
|
|
179
|
-
) as f:
|
|
180
|
-
f.write(
|
|
181
|
-
json.dumps(
|
|
182
|
-
self.environment.get_environment_info(include_ext_info=True)
|
|
183
|
-
)
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# Support metaflow extensions.
|
|
187
|
-
self.addl_paths = None
|
|
188
|
-
try:
|
|
189
|
-
m = importlib.import_module(EXT_PKG)
|
|
190
|
-
except ImportError:
|
|
191
|
-
# No additional check needed because if we are here, we already checked
|
|
192
|
-
# for other issues when loading at the toplevel.
|
|
193
|
-
pass
|
|
194
|
-
else:
|
|
195
|
-
custom_paths = list(set(m.__path__))
|
|
196
|
-
# For some reason, at times, unique paths appear multiple times. We
|
|
197
|
-
# simplify to avoid un-necessary links.
|
|
198
|
-
|
|
199
|
-
if len(custom_paths) == 1:
|
|
200
|
-
# Regular package; we take a quick shortcut here.
|
|
201
|
-
os.symlink(
|
|
202
|
-
custom_paths[0],
|
|
203
|
-
os.path.join(self.metaflow_dir.name, EXT_PKG),
|
|
204
|
-
)
|
|
205
|
-
else:
|
|
206
|
-
# This is a namespace package, we therefore create a bunch of
|
|
207
|
-
# directories so that we can symlink in those separately, and we will
|
|
208
|
-
# add those paths to the PYTHONPATH for the interpreter. Note that we
|
|
209
|
-
# don't symlink to the parent of the package because that could end up
|
|
210
|
-
# including more stuff we don't want
|
|
211
|
-
self.addl_paths = []
|
|
212
|
-
for p in custom_paths:
|
|
213
|
-
temp_dir = tempfile.mkdtemp(dir=self.metaflow_dir.name)
|
|
214
|
-
os.symlink(p, os.path.join(temp_dir, EXT_PKG))
|
|
215
|
-
self.addl_paths.append(temp_dir)
|
|
216
166
|
|
|
217
167
|
# # Also install any environment escape overrides directly here to enable
|
|
218
168
|
# # the escape to work even in non metaflow-created subprocesses
|
|
@@ -291,11 +241,15 @@ class CondaStepDecorator(StepDecorator):
|
|
|
291
241
|
if self.disabled:
|
|
292
242
|
return
|
|
293
243
|
# Ensure local installation of Metaflow is visible to user code
|
|
294
|
-
python_path = self.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
244
|
+
python_path = self.__class__._metaflow_home.name
|
|
245
|
+
addl_env_vars = {}
|
|
246
|
+
if self.__class__._addl_env_vars is not None:
|
|
247
|
+
for key, value in self.__class__._addl_env_vars.items():
|
|
248
|
+
if key == "PYTHONPATH":
|
|
249
|
+
addl_env_vars[key] = os.pathsep.join([value, python_path])
|
|
250
|
+
else:
|
|
251
|
+
addl_env_vars[key] = value
|
|
252
|
+
cli_args.env.update(addl_env_vars)
|
|
299
253
|
if self.interpreter:
|
|
300
254
|
# https://github.com/conda/conda/issues/7707
|
|
301
255
|
# Also ref - https://github.com/Netflix/metaflow/pull/178
|
|
@@ -306,7 +260,9 @@ class CondaStepDecorator(StepDecorator):
|
|
|
306
260
|
def runtime_finished(self, exception):
|
|
307
261
|
if self.disabled:
|
|
308
262
|
return
|
|
309
|
-
self.
|
|
263
|
+
if self.__class__._metaflow_home is not None:
|
|
264
|
+
self.__class__._metaflow_home.cleanup()
|
|
265
|
+
self.__class__._metaflow_home = None
|
|
310
266
|
|
|
311
267
|
|
|
312
268
|
class CondaFlowDecorator(FlowDecorator):
|
|
@@ -339,22 +295,23 @@ class CondaFlowDecorator(FlowDecorator):
|
|
|
339
295
|
"disabled": None,
|
|
340
296
|
}
|
|
341
297
|
|
|
342
|
-
def __init__(self, attributes=None, statically_defined=False):
|
|
298
|
+
def __init__(self, attributes=None, statically_defined=False, inserted_by=None):
|
|
343
299
|
self._attributes_with_user_values = (
|
|
344
300
|
set(attributes.keys()) if attributes is not None else set()
|
|
345
301
|
)
|
|
346
302
|
|
|
347
|
-
super(CondaFlowDecorator, self).__init__(
|
|
303
|
+
super(CondaFlowDecorator, self).__init__(
|
|
304
|
+
attributes, statically_defined, inserted_by
|
|
305
|
+
)
|
|
348
306
|
|
|
349
307
|
def init(self):
|
|
350
|
-
super(CondaFlowDecorator, self).init()
|
|
351
|
-
|
|
352
308
|
# Support legacy 'libraries=' attribute for the decorator.
|
|
353
309
|
self.attributes["packages"] = {
|
|
354
310
|
**self.attributes["libraries"],
|
|
355
311
|
**self.attributes["packages"],
|
|
356
312
|
}
|
|
357
|
-
|
|
313
|
+
# Keep because otherwise make_decorator_spec will fail
|
|
314
|
+
self.attributes["libraries"] = {}
|
|
358
315
|
if self.attributes["python"]:
|
|
359
316
|
self.attributes["python"] = str(self.attributes["python"])
|
|
360
317
|
|
|
@@ -17,6 +17,7 @@ from metaflow.debug import debug
|
|
|
17
17
|
from metaflow.exception import MetaflowException
|
|
18
18
|
from metaflow.metaflow_config import get_pinned_conda_libs
|
|
19
19
|
from metaflow.metaflow_environment import MetaflowEnvironment
|
|
20
|
+
from metaflow.packaging_sys import ContentType
|
|
20
21
|
|
|
21
22
|
from . import MAGIC_FILE, _datastore_packageroot
|
|
22
23
|
from .utils import conda_platform
|
|
@@ -35,6 +36,7 @@ class CondaEnvironment(MetaflowEnvironment):
|
|
|
35
36
|
_force_rebuild = False
|
|
36
37
|
|
|
37
38
|
def __init__(self, flow):
|
|
39
|
+
super().__init__(flow)
|
|
38
40
|
self.flow = flow
|
|
39
41
|
|
|
40
42
|
def set_local_root(self, local_root):
|
|
@@ -335,7 +337,7 @@ class CondaEnvironment(MetaflowEnvironment):
|
|
|
335
337
|
environment[decorator.name] = {
|
|
336
338
|
k: copy.deepcopy(decorator.attributes[k])
|
|
337
339
|
for k in decorator.attributes
|
|
338
|
-
if k
|
|
340
|
+
if k not in ("disabled", "libraries")
|
|
339
341
|
}
|
|
340
342
|
else:
|
|
341
343
|
return {}
|
|
@@ -477,7 +479,9 @@ class CondaEnvironment(MetaflowEnvironment):
|
|
|
477
479
|
files = []
|
|
478
480
|
manifest = self.get_environment_manifest_path()
|
|
479
481
|
if os.path.exists(manifest):
|
|
480
|
-
files.append(
|
|
482
|
+
files.append(
|
|
483
|
+
(manifest, os.path.basename(manifest), ContentType.OTHER_CONTENT)
|
|
484
|
+
)
|
|
481
485
|
return files
|
|
482
486
|
|
|
483
487
|
def bootstrap_commands(self, step_name, datastore_type):
|
|
@@ -24,12 +24,12 @@ class PyPIStepDecorator(StepDecorator):
|
|
|
24
24
|
name = "pypi"
|
|
25
25
|
defaults = {"packages": {}, "python": None, "disabled": None} # wheels
|
|
26
26
|
|
|
27
|
-
def __init__(self, attributes=None, statically_defined=False):
|
|
27
|
+
def __init__(self, attributes=None, statically_defined=False, inserted_by=None):
|
|
28
28
|
self._attributes_with_user_values = (
|
|
29
29
|
set(attributes.keys()) if attributes is not None else set()
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
super().__init__(attributes, statically_defined)
|
|
32
|
+
super().__init__(attributes, statically_defined, inserted_by)
|
|
33
33
|
|
|
34
34
|
def step_init(self, flow, graph, step, decos, environment, flow_datastore, logger):
|
|
35
35
|
# The init_environment hook for Environment creates the relevant virtual
|
|
@@ -128,12 +128,12 @@ class PyPIFlowDecorator(FlowDecorator):
|
|
|
128
128
|
name = "pypi_base"
|
|
129
129
|
defaults = {"packages": {}, "python": None, "disabled": None}
|
|
130
130
|
|
|
131
|
-
def __init__(self, attributes=None, statically_defined=False):
|
|
131
|
+
def __init__(self, attributes=None, statically_defined=False, inserted_by=None):
|
|
132
132
|
self._attributes_with_user_values = (
|
|
133
133
|
set(attributes.keys()) if attributes is not None else set()
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
-
super().__init__(attributes, statically_defined)
|
|
136
|
+
super().__init__(attributes, statically_defined, inserted_by)
|
|
137
137
|
|
|
138
138
|
def flow_init(
|
|
139
139
|
self, flow, graph, environment, flow_datastore, metadata, logger, echo, options
|
|
@@ -1,183 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import re
|
|
3
2
|
|
|
4
3
|
from metaflow.exception import MetaflowException
|
|
5
4
|
from metaflow.decorators import StepDecorator
|
|
6
5
|
from metaflow.metaflow_config import DEFAULT_SECRETS_ROLE
|
|
6
|
+
from metaflow.plugins.secrets.secrets_spec import SecretSpec
|
|
7
|
+
from metaflow.plugins.secrets.utils import (
|
|
8
|
+
get_secrets_backend_provider,
|
|
9
|
+
validate_env_vars,
|
|
10
|
+
validate_env_vars_across_secrets,
|
|
11
|
+
validate_env_vars_vs_existing_env,
|
|
12
|
+
)
|
|
7
13
|
from metaflow.unbounded_foreach import UBF_TASK
|
|
8
14
|
|
|
9
|
-
from typing import Any, Dict, List, Union
|
|
10
|
-
|
|
11
|
-
DISALLOWED_SECRETS_ENV_VAR_PREFIXES = ["METAFLOW_"]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_default_secrets_backend_type():
|
|
15
|
-
from metaflow.metaflow_config import DEFAULT_SECRETS_BACKEND_TYPE
|
|
16
|
-
|
|
17
|
-
if DEFAULT_SECRETS_BACKEND_TYPE is None:
|
|
18
|
-
raise MetaflowException(
|
|
19
|
-
"No default secrets backend type configured, but needed by @secrets. "
|
|
20
|
-
"Set METAFLOW_DEFAULT_SECRETS_BACKEND_TYPE."
|
|
21
|
-
)
|
|
22
|
-
return DEFAULT_SECRETS_BACKEND_TYPE
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SecretSpec:
|
|
26
|
-
def __init__(self, secrets_backend_type, secret_id, options={}, role=None):
|
|
27
|
-
self._secrets_backend_type = secrets_backend_type
|
|
28
|
-
self._secret_id = secret_id
|
|
29
|
-
self._options = options
|
|
30
|
-
self._role = role
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def secrets_backend_type(self):
|
|
34
|
-
return self._secrets_backend_type
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def secret_id(self):
|
|
38
|
-
return self._secret_id
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def options(self):
|
|
42
|
-
return self._options
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def role(self):
|
|
46
|
-
return self._role
|
|
47
|
-
|
|
48
|
-
def to_json(self):
|
|
49
|
-
"""Mainly used for testing... not the same as the input dict in secret_spec_from_dict()!"""
|
|
50
|
-
return {
|
|
51
|
-
"secrets_backend_type": self.secrets_backend_type,
|
|
52
|
-
"secret_id": self.secret_id,
|
|
53
|
-
"options": self.options,
|
|
54
|
-
"role": self.role,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
def __str__(self):
|
|
58
|
-
return "%s (%s)" % (self._secret_id, self._secrets_backend_type)
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def secret_spec_from_str(secret_spec_str, role):
|
|
62
|
-
# "." may be used in secret_id one day (provider specific). HOWEVER, it provides the best UX for
|
|
63
|
-
# non-conflicting cases (i.e. for secret ids that don't contain "."). This is true for all AWS
|
|
64
|
-
# Secrets Manager secrets.
|
|
65
|
-
#
|
|
66
|
-
# So we skew heavily optimize for best upfront UX for the present (1/2023).
|
|
67
|
-
#
|
|
68
|
-
# If/when a certain secret backend supports "." secret names, we can figure out a solution at that time.
|
|
69
|
-
# At a minimum, dictionary style secret spec may be used with no code changes (see secret_spec_from_dict()).
|
|
70
|
-
# Other options could be:
|
|
71
|
-
# - accept and document that "." secret_ids don't work in Metaflow (across all possible providers)
|
|
72
|
-
# - add a Metaflow config variable that specifies the separator (default ".")
|
|
73
|
-
# - smarter spec parsing, that errors on secrets that look ambiguous. "aws-secrets-manager.XYZ" could mean:
|
|
74
|
-
# + secret_id "XYZ" in aws-secrets-manager backend, OR
|
|
75
|
-
# + secret_id "aws-secrets-manager.XYZ" default backend (if it is defined).
|
|
76
|
-
# + in this case, user can simply set "azure-key-vault.aws-secrets-manager.XYZ" instead!
|
|
77
|
-
parts = secret_spec_str.split(".", maxsplit=1)
|
|
78
|
-
if len(parts) == 1:
|
|
79
|
-
secrets_backend_type = get_default_secrets_backend_type()
|
|
80
|
-
secret_id = parts[0]
|
|
81
|
-
else:
|
|
82
|
-
secrets_backend_type = parts[0]
|
|
83
|
-
secret_id = parts[1]
|
|
84
|
-
return SecretSpec(
|
|
85
|
-
secrets_backend_type, secret_id=secret_id, options={}, role=role
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
@staticmethod
|
|
89
|
-
def secret_spec_from_dict(secret_spec_dict, role):
|
|
90
|
-
if "type" not in secret_spec_dict:
|
|
91
|
-
secrets_backend_type = get_default_secrets_backend_type()
|
|
92
|
-
else:
|
|
93
|
-
secrets_backend_type = secret_spec_dict["type"]
|
|
94
|
-
if not isinstance(secrets_backend_type, str):
|
|
95
|
-
raise MetaflowException(
|
|
96
|
-
"Bad @secrets specification - 'type' must be a string - found %s"
|
|
97
|
-
% type(secrets_backend_type)
|
|
98
|
-
)
|
|
99
|
-
secret_id = secret_spec_dict.get("id")
|
|
100
|
-
if not isinstance(secret_id, str):
|
|
101
|
-
raise MetaflowException(
|
|
102
|
-
"Bad @secrets specification - 'id' must be a string - found %s"
|
|
103
|
-
% type(secret_id)
|
|
104
|
-
)
|
|
105
|
-
options = secret_spec_dict.get("options", {})
|
|
106
|
-
if not isinstance(options, dict):
|
|
107
|
-
raise MetaflowException(
|
|
108
|
-
"Bad @secrets specification - 'option' must be a dict - found %s"
|
|
109
|
-
% type(options)
|
|
110
|
-
)
|
|
111
|
-
role_for_source = secret_spec_dict.get("role", None)
|
|
112
|
-
if role_for_source is not None:
|
|
113
|
-
if not isinstance(role_for_source, str):
|
|
114
|
-
raise MetaflowException(
|
|
115
|
-
"Bad @secrets specification - 'role' must be a str - found %s"
|
|
116
|
-
% type(role_for_source)
|
|
117
|
-
)
|
|
118
|
-
role = role_for_source
|
|
119
|
-
return SecretSpec(
|
|
120
|
-
secrets_backend_type, secret_id=secret_id, options=options, role=role
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def validate_env_vars_across_secrets(all_secrets_env_vars):
|
|
125
|
-
vars_injected_by = {}
|
|
126
|
-
for secret_spec, env_vars in all_secrets_env_vars:
|
|
127
|
-
for k in env_vars:
|
|
128
|
-
if k in vars_injected_by:
|
|
129
|
-
raise MetaflowException(
|
|
130
|
-
"Secret '%s' will inject '%s' as env var, and it is also added by '%s'"
|
|
131
|
-
% (secret_spec, k, vars_injected_by[k])
|
|
132
|
-
)
|
|
133
|
-
vars_injected_by[k] = secret_spec
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def validate_env_vars_vs_existing_env(all_secrets_env_vars):
|
|
137
|
-
for secret_spec, env_vars in all_secrets_env_vars:
|
|
138
|
-
for k in env_vars:
|
|
139
|
-
if k in os.environ:
|
|
140
|
-
raise MetaflowException(
|
|
141
|
-
"Secret '%s' will inject '%s' as env var, but it already exists in env"
|
|
142
|
-
% (secret_spec, k)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def validate_env_vars(env_vars):
|
|
147
|
-
for k, v in env_vars.items():
|
|
148
|
-
if not isinstance(k, str):
|
|
149
|
-
raise MetaflowException("Found non string key %s (%s)" % (str(k), type(k)))
|
|
150
|
-
if not isinstance(v, str):
|
|
151
|
-
raise MetaflowException(
|
|
152
|
-
"Found non string value %s (%s)" % (str(v), type(v))
|
|
153
|
-
)
|
|
154
|
-
if not re.fullmatch("[a-zA-Z_][a-zA-Z0-9_]*", k):
|
|
155
|
-
raise MetaflowException("Found invalid env var name '%s'." % k)
|
|
156
|
-
for disallowed_prefix in DISALLOWED_SECRETS_ENV_VAR_PREFIXES:
|
|
157
|
-
if k.startswith(disallowed_prefix):
|
|
158
|
-
raise MetaflowException(
|
|
159
|
-
"Found disallowed env var name '%s' (starts with '%s')."
|
|
160
|
-
% (k, disallowed_prefix)
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def get_secrets_backend_provider(secrets_backend_type):
|
|
165
|
-
from metaflow.plugins import SECRETS_PROVIDERS
|
|
166
|
-
|
|
167
|
-
try:
|
|
168
|
-
provider_cls = [
|
|
169
|
-
pc for pc in SECRETS_PROVIDERS if pc.TYPE == secrets_backend_type
|
|
170
|
-
][0]
|
|
171
|
-
return provider_cls()
|
|
172
|
-
except IndexError:
|
|
173
|
-
raise MetaflowException(
|
|
174
|
-
"Unknown secrets backend type %s (available types: %s)"
|
|
175
|
-
% (
|
|
176
|
-
secrets_backend_type,
|
|
177
|
-
", ".join(pc.TYPE for pc in SECRETS_PROVIDERS if pc.TYPE != "inline"),
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
|
|
181
15
|
|
|
182
16
|
class SecretsDecorator(StepDecorator):
|
|
183
17
|
"""
|
|
@@ -188,6 +22,8 @@ class SecretsDecorator(StepDecorator):
|
|
|
188
22
|
----------
|
|
189
23
|
sources : List[Union[str, Dict[str, Any]]], default: []
|
|
190
24
|
List of secret specs, defining how the secrets are to be retrieved
|
|
25
|
+
role : str, optional, default: None
|
|
26
|
+
Role to use for fetching secrets
|
|
191
27
|
"""
|
|
192
28
|
|
|
193
29
|
name = "secrets"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, Union
|
|
2
|
+
|
|
3
|
+
from metaflow.metaflow_config import DEFAULT_SECRETS_ROLE
|
|
4
|
+
from metaflow.exception import MetaflowException
|
|
5
|
+
from metaflow.plugins.secrets.secrets_spec import SecretSpec
|
|
6
|
+
from metaflow.plugins.secrets.utils import get_secrets_backend_provider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_secret(
|
|
10
|
+
source: Union[str, Dict[str, Any]], role: Optional[str] = None
|
|
11
|
+
) -> Dict[str, str]:
|
|
12
|
+
"""
|
|
13
|
+
Get secret from source
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
source : Union[str, Dict[str, Any]]
|
|
18
|
+
Secret spec, defining how the secret is to be retrieved
|
|
19
|
+
role : str, optional
|
|
20
|
+
Role to use for fetching secrets
|
|
21
|
+
"""
|
|
22
|
+
if role is None:
|
|
23
|
+
role = DEFAULT_SECRETS_ROLE
|
|
24
|
+
|
|
25
|
+
secret_spec = None
|
|
26
|
+
|
|
27
|
+
if isinstance(source, str):
|
|
28
|
+
secret_spec = SecretSpec.secret_spec_from_str(source, role=role)
|
|
29
|
+
elif isinstance(source, dict):
|
|
30
|
+
secret_spec = SecretSpec.secret_spec_from_dict(source, role=role)
|
|
31
|
+
else:
|
|
32
|
+
raise MetaflowException(
|
|
33
|
+
"get_secrets sources items must be either a string or a dict"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
secrets_backend_provider = get_secrets_backend_provider(
|
|
37
|
+
secret_spec.secrets_backend_type
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
dict_for_secret = secrets_backend_provider.get_secret_as_dict(
|
|
41
|
+
secret_spec.secret_id,
|
|
42
|
+
options=secret_spec.options,
|
|
43
|
+
role=secret_spec.role,
|
|
44
|
+
)
|
|
45
|
+
return dict_for_secret
|
|
46
|
+
except Exception as e:
|
|
47
|
+
raise MetaflowException(
|
|
48
|
+
"Failed to retrieve secret '%s': %s" % (secret_spec.secret_id, e)
|
|
49
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from metaflow.exception import MetaflowException
|
|
2
|
+
from metaflow.plugins.secrets.utils import get_default_secrets_backend_type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SecretSpec:
|
|
6
|
+
def __init__(self, secrets_backend_type, secret_id, options={}, role=None):
|
|
7
|
+
self._secrets_backend_type = secrets_backend_type
|
|
8
|
+
self._secret_id = secret_id
|
|
9
|
+
self._options = options
|
|
10
|
+
self._role = role
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def secrets_backend_type(self):
|
|
14
|
+
return self._secrets_backend_type
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def secret_id(self):
|
|
18
|
+
return self._secret_id
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def options(self):
|
|
22
|
+
return self._options
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def role(self):
|
|
26
|
+
return self._role
|
|
27
|
+
|
|
28
|
+
def to_json(self):
|
|
29
|
+
"""Mainly used for testing... not the same as the input dict in secret_spec_from_dict()!"""
|
|
30
|
+
return {
|
|
31
|
+
"secrets_backend_type": self.secrets_backend_type,
|
|
32
|
+
"secret_id": self.secret_id,
|
|
33
|
+
"options": self.options,
|
|
34
|
+
"role": self.role,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return "%s (%s)" % (self._secret_id, self._secrets_backend_type)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def secret_spec_from_str(secret_spec_str, role):
|
|
42
|
+
# "." may be used in secret_id one day (provider specific). HOWEVER, it provides the best UX for
|
|
43
|
+
# non-conflicting cases (i.e. for secret ids that don't contain "."). This is true for all AWS
|
|
44
|
+
# Secrets Manager secrets.
|
|
45
|
+
#
|
|
46
|
+
# So we skew heavily optimize for best upfront UX for the present (1/2023).
|
|
47
|
+
#
|
|
48
|
+
# If/when a certain secret backend supports "." secret names, we can figure out a solution at that time.
|
|
49
|
+
# At a minimum, dictionary style secret spec may be used with no code changes (see secret_spec_from_dict()).
|
|
50
|
+
# Other options could be:
|
|
51
|
+
# - accept and document that "." secret_ids don't work in Metaflow (across all possible providers)
|
|
52
|
+
# - add a Metaflow config variable that specifies the separator (default ".")
|
|
53
|
+
# - smarter spec parsing, that errors on secrets that look ambiguous. "aws-secrets-manager.XYZ" could mean:
|
|
54
|
+
# + secret_id "XYZ" in aws-secrets-manager backend, OR
|
|
55
|
+
# + secret_id "aws-secrets-manager.XYZ" default backend (if it is defined).
|
|
56
|
+
# + in this case, user can simply set "azure-key-vault.aws-secrets-manager.XYZ" instead!
|
|
57
|
+
parts = secret_spec_str.split(".", maxsplit=1)
|
|
58
|
+
if len(parts) == 1:
|
|
59
|
+
secrets_backend_type = get_default_secrets_backend_type()
|
|
60
|
+
secret_id = parts[0]
|
|
61
|
+
else:
|
|
62
|
+
secrets_backend_type = parts[0]
|
|
63
|
+
secret_id = parts[1]
|
|
64
|
+
return SecretSpec(
|
|
65
|
+
secrets_backend_type, secret_id=secret_id, options={}, role=role
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def secret_spec_from_dict(secret_spec_dict, role):
|
|
70
|
+
if "type" not in secret_spec_dict:
|
|
71
|
+
secrets_backend_type = get_default_secrets_backend_type()
|
|
72
|
+
else:
|
|
73
|
+
secrets_backend_type = secret_spec_dict["type"]
|
|
74
|
+
if not isinstance(secrets_backend_type, str):
|
|
75
|
+
raise MetaflowException(
|
|
76
|
+
"Bad @secrets specification - 'type' must be a string - found %s"
|
|
77
|
+
% type(secrets_backend_type)
|
|
78
|
+
)
|
|
79
|
+
secret_id = secret_spec_dict.get("id")
|
|
80
|
+
if not isinstance(secret_id, str):
|
|
81
|
+
raise MetaflowException(
|
|
82
|
+
"Bad @secrets specification - 'id' must be a string - found %s"
|
|
83
|
+
% type(secret_id)
|
|
84
|
+
)
|
|
85
|
+
options = secret_spec_dict.get("options", {})
|
|
86
|
+
if not isinstance(options, dict):
|
|
87
|
+
raise MetaflowException(
|
|
88
|
+
"Bad @secrets specification - 'option' must be a dict - found %s"
|
|
89
|
+
% type(options)
|
|
90
|
+
)
|
|
91
|
+
role_for_source = secret_spec_dict.get("role", None)
|
|
92
|
+
if role_for_source is not None:
|
|
93
|
+
if not isinstance(role_for_source, str):
|
|
94
|
+
raise MetaflowException(
|
|
95
|
+
"Bad @secrets specification - 'role' must be a str - found %s"
|
|
96
|
+
% type(role_for_source)
|
|
97
|
+
)
|
|
98
|
+
role = role_for_source
|
|
99
|
+
return SecretSpec(
|
|
100
|
+
secrets_backend_type, secret_id=secret_id, options=options, role=role
|
|
101
|
+
)
|