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.

Files changed (93) hide show
  1. metaflow/__init__.py +7 -1
  2. metaflow/_vendor/imghdr/__init__.py +180 -0
  3. metaflow/cli.py +16 -1
  4. metaflow/cli_components/init_cmd.py +1 -0
  5. metaflow/cli_components/run_cmds.py +6 -2
  6. metaflow/client/core.py +22 -30
  7. metaflow/cmd/develop/stub_generator.py +19 -2
  8. metaflow/datastore/task_datastore.py +0 -1
  9. metaflow/debug.py +5 -0
  10. metaflow/decorators.py +230 -70
  11. metaflow/extension_support/__init__.py +15 -8
  12. metaflow/extension_support/_empty_file.py +2 -2
  13. metaflow/flowspec.py +80 -53
  14. metaflow/graph.py +24 -2
  15. metaflow/meta_files.py +13 -0
  16. metaflow/metadata_provider/metadata.py +7 -1
  17. metaflow/metaflow_config.py +5 -0
  18. metaflow/metaflow_environment.py +82 -25
  19. metaflow/metaflow_version.py +1 -1
  20. metaflow/package/__init__.py +664 -0
  21. metaflow/packaging_sys/__init__.py +870 -0
  22. metaflow/packaging_sys/backend.py +113 -0
  23. metaflow/packaging_sys/distribution_support.py +153 -0
  24. metaflow/packaging_sys/tar_backend.py +86 -0
  25. metaflow/packaging_sys/utils.py +91 -0
  26. metaflow/packaging_sys/v1.py +476 -0
  27. metaflow/plugins/__init__.py +3 -0
  28. metaflow/plugins/airflow/airflow.py +11 -1
  29. metaflow/plugins/airflow/airflow_cli.py +15 -4
  30. metaflow/plugins/argo/argo_workflows.py +346 -301
  31. metaflow/plugins/argo/argo_workflows_cli.py +16 -4
  32. metaflow/plugins/argo/exit_hooks.py +209 -0
  33. metaflow/plugins/aws/aws_utils.py +1 -1
  34. metaflow/plugins/aws/batch/batch.py +22 -3
  35. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  36. metaflow/plugins/aws/batch/batch_decorator.py +13 -5
  37. metaflow/plugins/aws/step_functions/step_functions.py +10 -1
  38. metaflow/plugins/aws/step_functions/step_functions_cli.py +15 -4
  39. metaflow/plugins/cards/card_cli.py +20 -1
  40. metaflow/plugins/cards/card_creator.py +24 -1
  41. metaflow/plugins/cards/card_decorator.py +57 -6
  42. metaflow/plugins/cards/card_modules/convert_to_native_type.py +5 -2
  43. metaflow/plugins/cards/card_modules/test_cards.py +16 -0
  44. metaflow/plugins/cards/metadata.py +22 -0
  45. metaflow/plugins/exit_hook/__init__.py +0 -0
  46. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  47. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  48. metaflow/plugins/kubernetes/kubernetes.py +8 -1
  49. metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
  50. metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
  51. metaflow/plugins/package_cli.py +25 -23
  52. metaflow/plugins/parallel_decorator.py +4 -2
  53. metaflow/plugins/pypi/bootstrap.py +8 -2
  54. metaflow/plugins/pypi/conda_decorator.py +39 -82
  55. metaflow/plugins/pypi/conda_environment.py +6 -2
  56. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  57. metaflow/plugins/secrets/__init__.py +3 -0
  58. metaflow/plugins/secrets/secrets_decorator.py +9 -173
  59. metaflow/plugins/secrets/secrets_func.py +49 -0
  60. metaflow/plugins/secrets/secrets_spec.py +101 -0
  61. metaflow/plugins/secrets/utils.py +74 -0
  62. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  63. metaflow/plugins/timeout_decorator.py +0 -1
  64. metaflow/plugins/uv/bootstrap.py +11 -0
  65. metaflow/plugins/uv/uv_environment.py +4 -2
  66. metaflow/pylint_wrapper.py +5 -1
  67. metaflow/runner/click_api.py +5 -4
  68. metaflow/runner/metaflow_runner.py +16 -1
  69. metaflow/runner/subprocess_manager.py +14 -2
  70. metaflow/runtime.py +82 -11
  71. metaflow/task.py +91 -7
  72. metaflow/user_configs/config_options.py +13 -8
  73. metaflow/user_configs/config_parameters.py +0 -4
  74. metaflow/user_decorators/__init__.py +0 -0
  75. metaflow/user_decorators/common.py +144 -0
  76. metaflow/user_decorators/mutable_flow.py +499 -0
  77. metaflow/user_decorators/mutable_step.py +424 -0
  78. metaflow/user_decorators/user_flow_decorator.py +263 -0
  79. metaflow/user_decorators/user_step_decorator.py +712 -0
  80. metaflow/util.py +4 -1
  81. metaflow/version.py +1 -1
  82. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Tiltfile +27 -2
  83. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/METADATA +2 -2
  84. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/RECORD +90 -70
  85. metaflow/info_file.py +0 -25
  86. metaflow/package.py +0 -203
  87. metaflow/user_configs/config_decorators.py +0 -568
  88. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/Makefile +0 -0
  89. {ob_metaflow-2.15.18.1.data → ob_metaflow-2.16.0.1.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  90. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/WHEEL +0 -0
  91. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/entry_points.txt +0 -0
  92. {ob_metaflow-2.15.18.1.dist-info → ob_metaflow-2.16.0.1.dist-info}/licenses/LICENSE +0 -0
  93. {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.util import get_metaflow_root
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__(attributes, statically_defined)
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
- del self.attributes["libraries"]
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
- # Create a symlink to metaflow installed outside the virtual environment.
156
- self.metaflow_dir = tempfile.TemporaryDirectory(dir="/tmp")
157
- os.symlink(
158
- os.path.join(get_metaflow_root(), "metaflow"),
159
- os.path.join(self.metaflow_dir.name, "metaflow"),
160
- )
161
-
162
- info = os.path.join(get_metaflow_root(), os.path.basename(INFO_FILE))
163
- # Symlink the INFO file as well to properly propagate down the Metaflow version
164
- if os.path.isfile(info):
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.metaflow_dir.name
295
- if self.addl_paths is not None:
296
- addl_paths = os.pathsep.join(self.addl_paths)
297
- python_path = os.pathsep.join([addl_paths, python_path])
298
- cli_args.env["PYTHONPATH"] = python_path
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.metaflow_dir.cleanup()
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__(attributes, statically_defined)
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
- del self.attributes["libraries"]
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 != "disabled"
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((manifest, os.path.basename(manifest)))
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
@@ -9,3 +9,6 @@ class SecretsProvider(abc.ABC):
9
9
  def get_secret_as_dict(self, secret_id, options={}, role=None) -> Dict[str, str]:
10
10
  """Retrieve the secret from secrets backend, and return a dictionary of
11
11
  environment variables."""
12
+
13
+
14
+ from .secrets_func import get_secret
@@ -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
+ )