metaflow 2.15.18__py2.py3-none-any.whl → 2.15.20__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/_vendor/imghdr/__init__.py +180 -0
- metaflow/cmd/develop/stub_generator.py +19 -2
- metaflow/plugins/__init__.py +3 -0
- metaflow/plugins/airflow/airflow.py +6 -0
- metaflow/plugins/argo/argo_workflows.py +316 -287
- metaflow/plugins/argo/exit_hooks.py +209 -0
- metaflow/plugins/aws/aws_utils.py +1 -1
- metaflow/plugins/aws/step_functions/step_functions.py +6 -0
- metaflow/plugins/cards/card_cli.py +20 -1
- metaflow/plugins/cards/card_creator.py +24 -1
- metaflow/plugins/cards/card_decorator.py +57 -1
- 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/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/runner/metaflow_runner.py +16 -1
- metaflow/runtime.py +45 -0
- metaflow/version.py +1 -1
- {metaflow-2.15.18.data → metaflow-2.15.20.data}/data/share/metaflow/devtools/Tiltfile +27 -2
- {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/METADATA +2 -2
- {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/RECORD +34 -25
- {metaflow-2.15.18.data → metaflow-2.15.20.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.18.data → metaflow-2.15.20.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/WHEEL +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.18.dist-info → metaflow-2.15.20.dist-info}/top_level.txt +0 -0
@@ -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
|
+
)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
from metaflow.exception import MetaflowException
|
4
|
+
|
5
|
+
DISALLOWED_SECRETS_ENV_VAR_PREFIXES = ["METAFLOW_"]
|
6
|
+
|
7
|
+
|
8
|
+
def get_default_secrets_backend_type():
|
9
|
+
from metaflow.metaflow_config import DEFAULT_SECRETS_BACKEND_TYPE
|
10
|
+
|
11
|
+
if DEFAULT_SECRETS_BACKEND_TYPE is None:
|
12
|
+
raise MetaflowException(
|
13
|
+
"No default secrets backend type configured, but needed by @secrets. "
|
14
|
+
"Set METAFLOW_DEFAULT_SECRETS_BACKEND_TYPE."
|
15
|
+
)
|
16
|
+
return DEFAULT_SECRETS_BACKEND_TYPE
|
17
|
+
|
18
|
+
|
19
|
+
def validate_env_vars_across_secrets(all_secrets_env_vars):
|
20
|
+
vars_injected_by = {}
|
21
|
+
for secret_spec, env_vars in all_secrets_env_vars:
|
22
|
+
for k in env_vars:
|
23
|
+
if k in vars_injected_by:
|
24
|
+
raise MetaflowException(
|
25
|
+
"Secret '%s' will inject '%s' as env var, and it is also added by '%s'"
|
26
|
+
% (secret_spec, k, vars_injected_by[k])
|
27
|
+
)
|
28
|
+
vars_injected_by[k] = secret_spec
|
29
|
+
|
30
|
+
|
31
|
+
def validate_env_vars_vs_existing_env(all_secrets_env_vars):
|
32
|
+
for secret_spec, env_vars in all_secrets_env_vars:
|
33
|
+
for k in env_vars:
|
34
|
+
if k in os.environ:
|
35
|
+
raise MetaflowException(
|
36
|
+
"Secret '%s' will inject '%s' as env var, but it already exists in env"
|
37
|
+
% (secret_spec, k)
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
def validate_env_vars(env_vars):
|
42
|
+
for k, v in env_vars.items():
|
43
|
+
if not isinstance(k, str):
|
44
|
+
raise MetaflowException("Found non string key %s (%s)" % (str(k), type(k)))
|
45
|
+
if not isinstance(v, str):
|
46
|
+
raise MetaflowException(
|
47
|
+
"Found non string value %s (%s)" % (str(v), type(v))
|
48
|
+
)
|
49
|
+
if not re.fullmatch("[a-zA-Z_][a-zA-Z0-9_]*", k):
|
50
|
+
raise MetaflowException("Found invalid env var name '%s'." % k)
|
51
|
+
for disallowed_prefix in DISALLOWED_SECRETS_ENV_VAR_PREFIXES:
|
52
|
+
if k.startswith(disallowed_prefix):
|
53
|
+
raise MetaflowException(
|
54
|
+
"Found disallowed env var name '%s' (starts with '%s')."
|
55
|
+
% (k, disallowed_prefix)
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
def get_secrets_backend_provider(secrets_backend_type):
|
60
|
+
from metaflow.plugins import SECRETS_PROVIDERS
|
61
|
+
|
62
|
+
try:
|
63
|
+
provider_cls = [
|
64
|
+
pc for pc in SECRETS_PROVIDERS if pc.TYPE == secrets_backend_type
|
65
|
+
][0]
|
66
|
+
return provider_cls()
|
67
|
+
except IndexError:
|
68
|
+
raise MetaflowException(
|
69
|
+
"Unknown secrets backend type %s (available types: %s)"
|
70
|
+
% (
|
71
|
+
secrets_backend_type,
|
72
|
+
", ".join(pc.TYPE for pc in SECRETS_PROVIDERS if pc.TYPE != "inline"),
|
73
|
+
)
|
74
|
+
)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import importlib
|
2
|
+
import inspect
|
2
3
|
import os
|
3
4
|
import sys
|
4
5
|
import json
|
@@ -200,8 +201,22 @@ class RunnerMeta(type):
|
|
200
201
|
def f(self, *args, **kwargs):
|
201
202
|
return runner_subcommand(self, *args, **kwargs)
|
202
203
|
|
203
|
-
f.__doc__ = runner_subcommand.__doc__ or ""
|
204
|
+
f.__doc__ = runner_subcommand.__init__.__doc__ or ""
|
204
205
|
f.__name__ = subcommand_name
|
206
|
+
sig = inspect.signature(runner_subcommand)
|
207
|
+
# We take all the same parameters except replace the first with
|
208
|
+
# simple "self"
|
209
|
+
new_parameters = {}
|
210
|
+
for name, param in sig.parameters.items():
|
211
|
+
if new_parameters:
|
212
|
+
new_parameters[name] = param
|
213
|
+
else:
|
214
|
+
new_parameters["self"] = inspect.Parameter(
|
215
|
+
"self", inspect.Parameter.POSITIONAL_OR_KEYWORD
|
216
|
+
)
|
217
|
+
f.__signature__ = inspect.Signature(
|
218
|
+
list(new_parameters.values()), return_annotation=runner_subcommand
|
219
|
+
)
|
205
220
|
|
206
221
|
return f
|
207
222
|
|
metaflow/runtime.py
CHANGED
@@ -569,6 +569,7 @@ class NativeRuntime(object):
|
|
569
569
|
for step in self._flow:
|
570
570
|
for deco in step.decorators:
|
571
571
|
deco.runtime_finished(exception)
|
572
|
+
self._run_exit_hooks()
|
572
573
|
|
573
574
|
# assert that end was executed and it was successful
|
574
575
|
if ("end", ()) in self._finished:
|
@@ -591,6 +592,50 @@ class NativeRuntime(object):
|
|
591
592
|
"The *end* step was not successful by the end of flow."
|
592
593
|
)
|
593
594
|
|
595
|
+
def _run_exit_hooks(self):
|
596
|
+
try:
|
597
|
+
exit_hook_decos = self._flow._flow_decorators.get("exit_hook", [])
|
598
|
+
if not exit_hook_decos:
|
599
|
+
return
|
600
|
+
|
601
|
+
successful = ("end", ()) in self._finished or self._clone_only
|
602
|
+
pathspec = f"{self._graph.name}/{self._run_id}"
|
603
|
+
flow_file = self._environment.get_environment_info()["script"]
|
604
|
+
|
605
|
+
def _call(fn_name):
|
606
|
+
try:
|
607
|
+
result = (
|
608
|
+
subprocess.check_output(
|
609
|
+
args=[
|
610
|
+
sys.executable,
|
611
|
+
"-m",
|
612
|
+
"metaflow.plugins.exit_hook.exit_hook_script",
|
613
|
+
flow_file,
|
614
|
+
fn_name,
|
615
|
+
pathspec,
|
616
|
+
],
|
617
|
+
env=os.environ,
|
618
|
+
)
|
619
|
+
.decode()
|
620
|
+
.strip()
|
621
|
+
)
|
622
|
+
print(result)
|
623
|
+
except subprocess.CalledProcessError as e:
|
624
|
+
print(f"[exit_hook] Hook '{fn_name}' failed with error: {e}")
|
625
|
+
except Exception as e:
|
626
|
+
print(f"[exit_hook] Unexpected error in hook '{fn_name}': {e}")
|
627
|
+
|
628
|
+
# Call all exit hook functions regardless of individual failures
|
629
|
+
for fn_name in [
|
630
|
+
name
|
631
|
+
for deco in exit_hook_decos
|
632
|
+
for name in (deco.success_hooks if successful else deco.error_hooks)
|
633
|
+
]:
|
634
|
+
_call(fn_name)
|
635
|
+
|
636
|
+
except Exception as ex:
|
637
|
+
pass # do not fail due to exit hooks for whatever reason.
|
638
|
+
|
594
639
|
def _killall(self):
|
595
640
|
# If we are here, all children have received a signal and are shutting down.
|
596
641
|
# We want to give them an opportunity to do so and then kill
|
metaflow/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
metaflow_version = "2.15.
|
1
|
+
metaflow_version = "2.15.20"
|
@@ -224,6 +224,19 @@ if "argo-workflows" in enabled_components:
|
|
224
224
|
]
|
225
225
|
)
|
226
226
|
|
227
|
+
# This fixes issue described in: https://github.com/argoproj/argo-workflows/issues/10340
|
228
|
+
k8s_yaml(encode_yaml({
|
229
|
+
'apiVersion': 'v1',
|
230
|
+
'kind': 'Secret',
|
231
|
+
'metadata': {
|
232
|
+
'name': 'default.service-account-token',
|
233
|
+
'annotations': {
|
234
|
+
'kubernetes.io/service-account.name': 'default'
|
235
|
+
}
|
236
|
+
},
|
237
|
+
'type': 'kubernetes.io/service-account-token'
|
238
|
+
}))
|
239
|
+
|
227
240
|
k8s_yaml(encode_yaml({
|
228
241
|
'apiVersion': 'rbac.authorization.k8s.io/v1',
|
229
242
|
'kind': 'Role',
|
@@ -231,11 +244,23 @@ if "argo-workflows" in enabled_components:
|
|
231
244
|
'name': 'argo-workflowtaskresults-role',
|
232
245
|
'namespace': 'default'
|
233
246
|
},
|
234
|
-
'rules': [
|
247
|
+
'rules': [
|
248
|
+
{
|
235
249
|
'apiGroups': ['argoproj.io'],
|
236
250
|
'resources': ['workflowtaskresults'],
|
237
251
|
'verbs': ['create', 'patch', 'get', 'list']
|
238
|
-
|
252
|
+
},
|
253
|
+
{
|
254
|
+
'apiGroups': ['argoproj.io'],
|
255
|
+
'resources': ['workflowtasksets'],
|
256
|
+
'verbs': ['watch', 'list']
|
257
|
+
},
|
258
|
+
{
|
259
|
+
'apiGroups': ['argoproj.io'],
|
260
|
+
'resources': ['workflowtasksets/status'],
|
261
|
+
'verbs': ['patch']
|
262
|
+
},
|
263
|
+
]
|
239
264
|
}))
|
240
265
|
|
241
266
|
k8s_yaml(encode_yaml({
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: metaflow
|
3
|
-
Version: 2.15.
|
3
|
+
Version: 2.15.20
|
4
4
|
Summary: Metaflow: More AI and ML, Less Engineering
|
5
5
|
Author: Metaflow Developers
|
6
6
|
Author-email: help@metaflow.org
|
@@ -26,7 +26,7 @@ License-File: LICENSE
|
|
26
26
|
Requires-Dist: requests
|
27
27
|
Requires-Dist: boto3
|
28
28
|
Provides-Extra: stubs
|
29
|
-
Requires-Dist: metaflow-stubs==2.15.
|
29
|
+
Requires-Dist: metaflow-stubs==2.15.20; extra == "stubs"
|
30
30
|
Dynamic: author
|
31
31
|
Dynamic: author-email
|
32
32
|
Dynamic: classifier
|