outerbounds 0.3.55rc4__py3-none-any.whl → 0.3.89__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.
- outerbounds/_vendor/PyYAML.LICENSE +20 -0
- outerbounds/_vendor/__init__.py +0 -0
- outerbounds/_vendor/_yaml/__init__.py +34 -0
- outerbounds/_vendor/click/__init__.py +73 -0
- outerbounds/_vendor/click/_compat.py +626 -0
- outerbounds/_vendor/click/_termui_impl.py +717 -0
- outerbounds/_vendor/click/_textwrap.py +49 -0
- outerbounds/_vendor/click/_winconsole.py +279 -0
- outerbounds/_vendor/click/core.py +2998 -0
- outerbounds/_vendor/click/decorators.py +497 -0
- outerbounds/_vendor/click/exceptions.py +287 -0
- outerbounds/_vendor/click/formatting.py +301 -0
- outerbounds/_vendor/click/globals.py +68 -0
- outerbounds/_vendor/click/parser.py +529 -0
- outerbounds/_vendor/click/py.typed +0 -0
- outerbounds/_vendor/click/shell_completion.py +580 -0
- outerbounds/_vendor/click/termui.py +787 -0
- outerbounds/_vendor/click/testing.py +479 -0
- outerbounds/_vendor/click/types.py +1073 -0
- outerbounds/_vendor/click/utils.py +580 -0
- outerbounds/_vendor/click.LICENSE +28 -0
- outerbounds/_vendor/vendor_any.txt +2 -0
- outerbounds/_vendor/yaml/__init__.py +471 -0
- outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
- outerbounds/_vendor/yaml/composer.py +146 -0
- outerbounds/_vendor/yaml/constructor.py +862 -0
- outerbounds/_vendor/yaml/cyaml.py +177 -0
- outerbounds/_vendor/yaml/dumper.py +138 -0
- outerbounds/_vendor/yaml/emitter.py +1239 -0
- outerbounds/_vendor/yaml/error.py +94 -0
- outerbounds/_vendor/yaml/events.py +104 -0
- outerbounds/_vendor/yaml/loader.py +62 -0
- outerbounds/_vendor/yaml/nodes.py +51 -0
- outerbounds/_vendor/yaml/parser.py +629 -0
- outerbounds/_vendor/yaml/reader.py +208 -0
- outerbounds/_vendor/yaml/representer.py +378 -0
- outerbounds/_vendor/yaml/resolver.py +245 -0
- outerbounds/_vendor/yaml/scanner.py +1555 -0
- outerbounds/_vendor/yaml/serializer.py +127 -0
- outerbounds/_vendor/yaml/tokens.py +129 -0
- outerbounds/command_groups/cli.py +1 -1
- outerbounds/command_groups/local_setup_cli.py +249 -33
- outerbounds/command_groups/perimeters_cli.py +168 -33
- outerbounds/command_groups/workstations_cli.py +29 -16
- outerbounds/utils/kubeconfig.py +2 -2
- outerbounds/utils/metaflowconfig.py +111 -21
- outerbounds/utils/schema.py +6 -0
- outerbounds/utils/utils.py +19 -0
- outerbounds/vendor.py +159 -0
- {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/METADATA +14 -6
- outerbounds-0.3.89.dist-info/RECORD +57 -0
- outerbounds-0.3.55rc4.dist-info/RECORD +0 -15
- {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
__all__ = ["Serializer", "SerializerError"]
|
2
|
+
|
3
|
+
from .error import YAMLError
|
4
|
+
from .events import *
|
5
|
+
from .nodes import *
|
6
|
+
|
7
|
+
|
8
|
+
class SerializerError(YAMLError):
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
class Serializer:
|
13
|
+
|
14
|
+
ANCHOR_TEMPLATE = "id%03d"
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
encoding=None,
|
19
|
+
explicit_start=None,
|
20
|
+
explicit_end=None,
|
21
|
+
version=None,
|
22
|
+
tags=None,
|
23
|
+
):
|
24
|
+
self.use_encoding = encoding
|
25
|
+
self.use_explicit_start = explicit_start
|
26
|
+
self.use_explicit_end = explicit_end
|
27
|
+
self.use_version = version
|
28
|
+
self.use_tags = tags
|
29
|
+
self.serialized_nodes = {}
|
30
|
+
self.anchors = {}
|
31
|
+
self.last_anchor_id = 0
|
32
|
+
self.closed = None
|
33
|
+
|
34
|
+
def open(self):
|
35
|
+
if self.closed is None:
|
36
|
+
self.emit(StreamStartEvent(encoding=self.use_encoding))
|
37
|
+
self.closed = False
|
38
|
+
elif self.closed:
|
39
|
+
raise SerializerError("serializer is closed")
|
40
|
+
else:
|
41
|
+
raise SerializerError("serializer is already opened")
|
42
|
+
|
43
|
+
def close(self):
|
44
|
+
if self.closed is None:
|
45
|
+
raise SerializerError("serializer is not opened")
|
46
|
+
elif not self.closed:
|
47
|
+
self.emit(StreamEndEvent())
|
48
|
+
self.closed = True
|
49
|
+
|
50
|
+
# def __del__(self):
|
51
|
+
# self.close()
|
52
|
+
|
53
|
+
def serialize(self, node):
|
54
|
+
if self.closed is None:
|
55
|
+
raise SerializerError("serializer is not opened")
|
56
|
+
elif self.closed:
|
57
|
+
raise SerializerError("serializer is closed")
|
58
|
+
self.emit(
|
59
|
+
DocumentStartEvent(
|
60
|
+
explicit=self.use_explicit_start,
|
61
|
+
version=self.use_version,
|
62
|
+
tags=self.use_tags,
|
63
|
+
)
|
64
|
+
)
|
65
|
+
self.anchor_node(node)
|
66
|
+
self.serialize_node(node, None, None)
|
67
|
+
self.emit(DocumentEndEvent(explicit=self.use_explicit_end))
|
68
|
+
self.serialized_nodes = {}
|
69
|
+
self.anchors = {}
|
70
|
+
self.last_anchor_id = 0
|
71
|
+
|
72
|
+
def anchor_node(self, node):
|
73
|
+
if node in self.anchors:
|
74
|
+
if self.anchors[node] is None:
|
75
|
+
self.anchors[node] = self.generate_anchor(node)
|
76
|
+
else:
|
77
|
+
self.anchors[node] = None
|
78
|
+
if isinstance(node, SequenceNode):
|
79
|
+
for item in node.value:
|
80
|
+
self.anchor_node(item)
|
81
|
+
elif isinstance(node, MappingNode):
|
82
|
+
for key, value in node.value:
|
83
|
+
self.anchor_node(key)
|
84
|
+
self.anchor_node(value)
|
85
|
+
|
86
|
+
def generate_anchor(self, node):
|
87
|
+
self.last_anchor_id += 1
|
88
|
+
return self.ANCHOR_TEMPLATE % self.last_anchor_id
|
89
|
+
|
90
|
+
def serialize_node(self, node, parent, index):
|
91
|
+
alias = self.anchors[node]
|
92
|
+
if node in self.serialized_nodes:
|
93
|
+
self.emit(AliasEvent(alias))
|
94
|
+
else:
|
95
|
+
self.serialized_nodes[node] = True
|
96
|
+
self.descend_resolver(parent, index)
|
97
|
+
if isinstance(node, ScalarNode):
|
98
|
+
detected_tag = self.resolve(ScalarNode, node.value, (True, False))
|
99
|
+
default_tag = self.resolve(ScalarNode, node.value, (False, True))
|
100
|
+
implicit = (node.tag == detected_tag), (node.tag == default_tag)
|
101
|
+
self.emit(
|
102
|
+
ScalarEvent(alias, node.tag, implicit, node.value, style=node.style)
|
103
|
+
)
|
104
|
+
elif isinstance(node, SequenceNode):
|
105
|
+
implicit = node.tag == self.resolve(SequenceNode, node.value, True)
|
106
|
+
self.emit(
|
107
|
+
SequenceStartEvent(
|
108
|
+
alias, node.tag, implicit, flow_style=node.flow_style
|
109
|
+
)
|
110
|
+
)
|
111
|
+
index = 0
|
112
|
+
for item in node.value:
|
113
|
+
self.serialize_node(item, node, index)
|
114
|
+
index += 1
|
115
|
+
self.emit(SequenceEndEvent())
|
116
|
+
elif isinstance(node, MappingNode):
|
117
|
+
implicit = node.tag == self.resolve(MappingNode, node.value, True)
|
118
|
+
self.emit(
|
119
|
+
MappingStartEvent(
|
120
|
+
alias, node.tag, implicit, flow_style=node.flow_style
|
121
|
+
)
|
122
|
+
)
|
123
|
+
for key, value in node.value:
|
124
|
+
self.serialize_node(key, node, None)
|
125
|
+
self.serialize_node(value, node, key)
|
126
|
+
self.emit(MappingEndEvent())
|
127
|
+
self.ascend_resolver()
|
@@ -0,0 +1,129 @@
|
|
1
|
+
class Token(object):
|
2
|
+
def __init__(self, start_mark, end_mark):
|
3
|
+
self.start_mark = start_mark
|
4
|
+
self.end_mark = end_mark
|
5
|
+
|
6
|
+
def __repr__(self):
|
7
|
+
attributes = [key for key in self.__dict__ if not key.endswith("_mark")]
|
8
|
+
attributes.sort()
|
9
|
+
arguments = ", ".join(
|
10
|
+
["%s=%r" % (key, getattr(self, key)) for key in attributes]
|
11
|
+
)
|
12
|
+
return "%s(%s)" % (self.__class__.__name__, arguments)
|
13
|
+
|
14
|
+
|
15
|
+
# class BOMToken(Token):
|
16
|
+
# id = '<byte order mark>'
|
17
|
+
|
18
|
+
|
19
|
+
class DirectiveToken(Token):
|
20
|
+
id = "<directive>"
|
21
|
+
|
22
|
+
def __init__(self, name, value, start_mark, end_mark):
|
23
|
+
self.name = name
|
24
|
+
self.value = value
|
25
|
+
self.start_mark = start_mark
|
26
|
+
self.end_mark = end_mark
|
27
|
+
|
28
|
+
|
29
|
+
class DocumentStartToken(Token):
|
30
|
+
id = "<document start>"
|
31
|
+
|
32
|
+
|
33
|
+
class DocumentEndToken(Token):
|
34
|
+
id = "<document end>"
|
35
|
+
|
36
|
+
|
37
|
+
class StreamStartToken(Token):
|
38
|
+
id = "<stream start>"
|
39
|
+
|
40
|
+
def __init__(self, start_mark=None, end_mark=None, encoding=None):
|
41
|
+
self.start_mark = start_mark
|
42
|
+
self.end_mark = end_mark
|
43
|
+
self.encoding = encoding
|
44
|
+
|
45
|
+
|
46
|
+
class StreamEndToken(Token):
|
47
|
+
id = "<stream end>"
|
48
|
+
|
49
|
+
|
50
|
+
class BlockSequenceStartToken(Token):
|
51
|
+
id = "<block sequence start>"
|
52
|
+
|
53
|
+
|
54
|
+
class BlockMappingStartToken(Token):
|
55
|
+
id = "<block mapping start>"
|
56
|
+
|
57
|
+
|
58
|
+
class BlockEndToken(Token):
|
59
|
+
id = "<block end>"
|
60
|
+
|
61
|
+
|
62
|
+
class FlowSequenceStartToken(Token):
|
63
|
+
id = "["
|
64
|
+
|
65
|
+
|
66
|
+
class FlowMappingStartToken(Token):
|
67
|
+
id = "{"
|
68
|
+
|
69
|
+
|
70
|
+
class FlowSequenceEndToken(Token):
|
71
|
+
id = "]"
|
72
|
+
|
73
|
+
|
74
|
+
class FlowMappingEndToken(Token):
|
75
|
+
id = "}"
|
76
|
+
|
77
|
+
|
78
|
+
class KeyToken(Token):
|
79
|
+
id = "?"
|
80
|
+
|
81
|
+
|
82
|
+
class ValueToken(Token):
|
83
|
+
id = ":"
|
84
|
+
|
85
|
+
|
86
|
+
class BlockEntryToken(Token):
|
87
|
+
id = "-"
|
88
|
+
|
89
|
+
|
90
|
+
class FlowEntryToken(Token):
|
91
|
+
id = ","
|
92
|
+
|
93
|
+
|
94
|
+
class AliasToken(Token):
|
95
|
+
id = "<alias>"
|
96
|
+
|
97
|
+
def __init__(self, value, start_mark, end_mark):
|
98
|
+
self.value = value
|
99
|
+
self.start_mark = start_mark
|
100
|
+
self.end_mark = end_mark
|
101
|
+
|
102
|
+
|
103
|
+
class AnchorToken(Token):
|
104
|
+
id = "<anchor>"
|
105
|
+
|
106
|
+
def __init__(self, value, start_mark, end_mark):
|
107
|
+
self.value = value
|
108
|
+
self.start_mark = start_mark
|
109
|
+
self.end_mark = end_mark
|
110
|
+
|
111
|
+
|
112
|
+
class TagToken(Token):
|
113
|
+
id = "<tag>"
|
114
|
+
|
115
|
+
def __init__(self, value, start_mark, end_mark):
|
116
|
+
self.value = value
|
117
|
+
self.start_mark = start_mark
|
118
|
+
self.end_mark = end_mark
|
119
|
+
|
120
|
+
|
121
|
+
class ScalarToken(Token):
|
122
|
+
id = "<scalar>"
|
123
|
+
|
124
|
+
def __init__(self, value, plain, start_mark, end_mark, style=None):
|
125
|
+
self.value = value
|
126
|
+
self.plain = plain
|
127
|
+
self.start_mark = start_mark
|
128
|
+
self.end_mark = end_mark
|
129
|
+
self.style = style
|
@@ -11,9 +11,7 @@ from importlib.machinery import PathFinder
|
|
11
11
|
from os import path
|
12
12
|
from pathlib import Path
|
13
13
|
from typing import Any, Callable, Dict, List
|
14
|
-
|
15
|
-
import boto3
|
16
|
-
import click
|
14
|
+
from outerbounds._vendor import click
|
17
15
|
import requests
|
18
16
|
from requests.exceptions import HTTPError
|
19
17
|
|
@@ -31,7 +29,8 @@ the Outerbounds platform.
|
|
31
29
|
To remove that package, please try `python -m pip uninstall metaflow -y` or reach out to Outerbounds support.
|
32
30
|
After uninstalling the Metaflow package, please reinstall the Outerbounds package using `python -m pip
|
33
31
|
install outerbounds --force`.
|
34
|
-
As always, please reach out to Outerbounds support for any questions.
|
32
|
+
As always, please reach out to Outerbounds support for any questions.
|
33
|
+
"""
|
35
34
|
|
36
35
|
MISSING_EXTENSIONS_MESSAGE = (
|
37
36
|
"The Outerbounds Platform extensions for Metaflow was not found."
|
@@ -43,6 +42,8 @@ BAD_EXTENSION_MESSAGE = (
|
|
43
42
|
"Mis-installation of the Outerbounds Platform extension package has been detected."
|
44
43
|
)
|
45
44
|
|
45
|
+
PERIMETER_CONFIG_URL_KEY = "OB_CURRENT_PERIMETER_MF_CONFIG_URL"
|
46
|
+
|
46
47
|
|
47
48
|
class Narrator:
|
48
49
|
def __init__(self, verbose):
|
@@ -54,11 +55,11 @@ class Narrator:
|
|
54
55
|
|
55
56
|
def section_ok(self):
|
56
57
|
if not self.verbose:
|
57
|
-
click.secho("\
|
58
|
+
click.secho("\U00002705", err=True)
|
58
59
|
|
59
60
|
def section_not_ok(self):
|
60
61
|
if not self.verbose:
|
61
|
-
click.secho("\
|
62
|
+
click.secho("\U0000274C", err=True)
|
62
63
|
|
63
64
|
def announce_check(self, name):
|
64
65
|
if self.verbose:
|
@@ -232,25 +233,33 @@ class ConfigEntrySpec:
|
|
232
233
|
self.expected = expected
|
233
234
|
|
234
235
|
|
235
|
-
def get_config_specs():
|
236
|
-
|
237
|
-
ConfigEntrySpec(
|
238
|
-
"METAFLOW_DATASTORE_SYSROOT_S3", "s3://[a-z0-9\-]+/metaflow[/]?"
|
239
|
-
),
|
240
|
-
ConfigEntrySpec("METAFLOW_DATATOOLS_S3ROOT", "s3://[a-z0-9\-]+/data[/]?"),
|
236
|
+
def get_config_specs(default_datastore: str):
|
237
|
+
spec = [
|
241
238
|
ConfigEntrySpec("METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER", "obp", expected="obp"),
|
242
|
-
ConfigEntrySpec("METAFLOW_DEFAULT_DATASTORE", "s3", expected="s3"),
|
243
239
|
ConfigEntrySpec("METAFLOW_DEFAULT_METADATA", "service", expected="service"),
|
244
|
-
ConfigEntrySpec(
|
245
|
-
|
246
|
-
),
|
247
|
-
ConfigEntrySpec("
|
248
|
-
ConfigEntrySpec("
|
249
|
-
ConfigEntrySpec("
|
250
|
-
ConfigEntrySpec("METAFLOW_UI_URL", "https://ui\..*"),
|
251
|
-
ConfigEntrySpec("OBP_AUTH_SERVER", "auth\..*"),
|
240
|
+
ConfigEntrySpec("METAFLOW_KUBERNETES_NAMESPACE", r"jobs-.*"),
|
241
|
+
ConfigEntrySpec("METAFLOW_KUBERNETES_SANDBOX_INIT_SCRIPT", r"eval \$\(.*"),
|
242
|
+
ConfigEntrySpec("METAFLOW_SERVICE_AUTH_KEY", r"[a-zA-Z0-9!_\-\.]+"),
|
243
|
+
ConfigEntrySpec("METAFLOW_SERVICE_URL", r"https://metadata\..*"),
|
244
|
+
ConfigEntrySpec("METAFLOW_UI_URL", r"https://ui\..*"),
|
245
|
+
ConfigEntrySpec("OBP_AUTH_SERVER", r"auth\..*"),
|
252
246
|
]
|
253
247
|
|
248
|
+
if default_datastore == "s3":
|
249
|
+
spec.extend(
|
250
|
+
[
|
251
|
+
ConfigEntrySpec(
|
252
|
+
"METAFLOW_DATASTORE_SYSROOT_S3",
|
253
|
+
r"s3://[a-z0-9\-]+/metaflow(-[a-z0-9\-]+)?[/]?",
|
254
|
+
),
|
255
|
+
ConfigEntrySpec(
|
256
|
+
"METAFLOW_DATATOOLS_S3ROOT",
|
257
|
+
r"s3://[a-z0-9\-]+/data(-[a-z0-9\-]+)?[/]?",
|
258
|
+
),
|
259
|
+
]
|
260
|
+
)
|
261
|
+
return spec
|
262
|
+
|
254
263
|
|
255
264
|
def check_metaflow_config(narrator: Narrator) -> CommandStatus:
|
256
265
|
narrator.announce_section("local Metaflow config")
|
@@ -261,8 +270,20 @@ def check_metaflow_config(narrator: Narrator) -> CommandStatus:
|
|
261
270
|
mitigation="",
|
262
271
|
)
|
263
272
|
|
264
|
-
|
265
|
-
|
273
|
+
profile = os.environ.get("METAFLOW_PROFILE")
|
274
|
+
config_dir = os.path.expanduser(
|
275
|
+
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
276
|
+
)
|
277
|
+
|
278
|
+
config = metaflowconfig.init_config(config_dir, profile)
|
279
|
+
|
280
|
+
if "OBP_METAFLOW_CONFIG_URL" in config:
|
281
|
+
# If the config is fetched from a remote source, not much to check
|
282
|
+
narrator.announce_check("config entry OBP_METAFLOW_CONFIG_URL")
|
283
|
+
narrator.ok()
|
284
|
+
return check_status
|
285
|
+
|
286
|
+
for spec in get_config_specs(config.get("METAFLOW_DEFAULT_DATASTORE", "")):
|
266
287
|
narrator.announce_check("config entry " + spec.name)
|
267
288
|
if spec.name not in config:
|
268
289
|
reason = "Missing"
|
@@ -304,7 +325,12 @@ def check_metaflow_token(narrator: Narrator) -> CommandStatus:
|
|
304
325
|
mitigation="",
|
305
326
|
)
|
306
327
|
|
307
|
-
|
328
|
+
profile = os.environ.get("METAFLOW_PROFILE")
|
329
|
+
config_dir = os.path.expanduser(
|
330
|
+
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
331
|
+
)
|
332
|
+
|
333
|
+
config = metaflowconfig.init_config(config_dir, profile)
|
308
334
|
try:
|
309
335
|
if "OBP_AUTH_SERVER" in config:
|
310
336
|
k8s_response = requests.get(
|
@@ -363,7 +389,13 @@ def check_workstation_api_accessible(narrator: Narrator) -> CommandStatus:
|
|
363
389
|
)
|
364
390
|
|
365
391
|
try:
|
366
|
-
|
392
|
+
profile = os.environ.get("METAFLOW_PROFILE")
|
393
|
+
config_dir = os.path.expanduser(
|
394
|
+
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
395
|
+
)
|
396
|
+
|
397
|
+
config = metaflowconfig.init_config(config_dir, profile)
|
398
|
+
|
367
399
|
missing_keys = []
|
368
400
|
if "METAFLOW_SERVICE_AUTH_KEY" not in config:
|
369
401
|
missing_keys.append("METAFLOW_SERVICE_AUTH_KEY")
|
@@ -422,7 +454,13 @@ def check_kubeconfig_valid_for_workstations(narrator: Narrator) -> CommandStatus
|
|
422
454
|
)
|
423
455
|
|
424
456
|
try:
|
425
|
-
|
457
|
+
profile = os.environ.get("METAFLOW_PROFILE")
|
458
|
+
config_dir = os.path.expanduser(
|
459
|
+
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
460
|
+
)
|
461
|
+
|
462
|
+
config = metaflowconfig.init_config(config_dir, profile)
|
463
|
+
|
426
464
|
missing_keys = []
|
427
465
|
if "METAFLOW_SERVICE_AUTH_KEY" not in config:
|
428
466
|
missing_keys.append("METAFLOW_SERVICE_AUTH_KEY")
|
@@ -585,6 +623,7 @@ class ConfigurationWriter:
|
|
585
623
|
self.decoded_config = None
|
586
624
|
self.out_dir = out_dir
|
587
625
|
self.profile = profile
|
626
|
+
self.selected_perimeter = None
|
588
627
|
|
589
628
|
ob_config_dir = path.expanduser(os.getenv("OBP_CONFIG_DIR", out_dir))
|
590
629
|
self.ob_config_path = path.join(
|
@@ -596,8 +635,11 @@ class ConfigurationWriter:
|
|
596
635
|
self.decoded_config = deserialize(self.encoded_config)
|
597
636
|
|
598
637
|
def process_decoded_config(self):
|
638
|
+
assert self.decoded_config is not None
|
599
639
|
config_type = self.decoded_config.get("OB_CONFIG_TYPE", "inline")
|
600
640
|
if config_type == "inline":
|
641
|
+
if "OBP_PERIMETER" in self.decoded_config:
|
642
|
+
self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
|
601
643
|
if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
|
602
644
|
self.decoded_config = {
|
603
645
|
"OBP_METAFLOW_CONFIG_URL": self.decoded_config[
|
@@ -616,6 +658,8 @@ class ConfigurationWriter:
|
|
616
658
|
f"{str(e)} key is required for aws-ref config type"
|
617
659
|
)
|
618
660
|
try:
|
661
|
+
import boto3
|
662
|
+
|
619
663
|
client = boto3.client("secretsmanager", region_name=region)
|
620
664
|
response = client.get_secret_value(SecretId=secret_arn)
|
621
665
|
self.decoded_config = json.loads(response["SecretBinary"])
|
@@ -633,6 +677,7 @@ class ConfigurationWriter:
|
|
633
677
|
return path.join(self.out_dir, "config_{}.json".format(self.profile))
|
634
678
|
|
635
679
|
def display(self):
|
680
|
+
assert self.decoded_config is not None
|
636
681
|
# Create a copy so we can use the real config later, possibly
|
637
682
|
display_config = dict()
|
638
683
|
for k in self.decoded_config.keys():
|
@@ -647,6 +692,7 @@ class ConfigurationWriter:
|
|
647
692
|
return self.confirm_overwrite_config(self.path())
|
648
693
|
|
649
694
|
def write_config(self):
|
695
|
+
assert self.decoded_config is not None
|
650
696
|
config_path = self.path()
|
651
697
|
# TODO config contains auth token - restrict file/dir modes
|
652
698
|
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
@@ -654,13 +700,13 @@ class ConfigurationWriter:
|
|
654
700
|
with open(config_path, "w") as fd:
|
655
701
|
json.dump(self.existing, fd, indent=4)
|
656
702
|
|
657
|
-
|
658
|
-
remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
|
659
|
-
if "OBP_PERIMETER" in remote_config and "OBP_PERIMETER_URL" in remote_config:
|
703
|
+
if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
|
660
704
|
with open(self.ob_config_path, "w") as fd:
|
661
705
|
ob_config_dict = {
|
662
|
-
"OB_CURRENT_PERIMETER":
|
663
|
-
|
706
|
+
"OB_CURRENT_PERIMETER": self.selected_perimeter,
|
707
|
+
PERIMETER_CONFIG_URL_KEY: self.decoded_config[
|
708
|
+
"OBP_METAFLOW_CONFIG_URL"
|
709
|
+
],
|
664
710
|
}
|
665
711
|
json.dump(ob_config_dict, fd, indent=4)
|
666
712
|
|
@@ -686,6 +732,64 @@ class ConfigurationWriter:
|
|
686
732
|
return True
|
687
733
|
|
688
734
|
|
735
|
+
def get_gha_jwt(audience: str):
|
736
|
+
# These are specific environment variables that are set by GitHub Actions.
|
737
|
+
if (
|
738
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN" in os.environ
|
739
|
+
and "ACTIONS_ID_TOKEN_REQUEST_URL" in os.environ
|
740
|
+
):
|
741
|
+
try:
|
742
|
+
response = requests.get(
|
743
|
+
url=os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"],
|
744
|
+
headers={
|
745
|
+
"Authorization": f"Bearer {os.environ['ACTIONS_ID_TOKEN_REQUEST_TOKEN']}"
|
746
|
+
},
|
747
|
+
params={"audience": audience},
|
748
|
+
)
|
749
|
+
response.raise_for_status()
|
750
|
+
return response.json()["value"]
|
751
|
+
except Exception as e:
|
752
|
+
click.secho(
|
753
|
+
"Failed to fetch JWT token from GitHub Actions. Please make sure you are permission 'id-token: write' is set on the GHA jobs level.",
|
754
|
+
fg="red",
|
755
|
+
)
|
756
|
+
sys.exit(1)
|
757
|
+
|
758
|
+
click.secho(
|
759
|
+
"The --github-actions flag was set, but we didn't not find '$ACTIONS_ID_TOKEN_REQUEST_TOKEN' and '$ACTIONS_ID_TOKEN_REQUEST_URL' environment variables. Please make sure you are running this command in a GitHub Actions environment and with correct permissions as per https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers",
|
760
|
+
fg="red",
|
761
|
+
)
|
762
|
+
sys.exit(1)
|
763
|
+
|
764
|
+
|
765
|
+
def get_origin_token(
|
766
|
+
service_principal_name: str,
|
767
|
+
deployment: str,
|
768
|
+
perimeter: str,
|
769
|
+
token: str,
|
770
|
+
auth_server: str,
|
771
|
+
):
|
772
|
+
try:
|
773
|
+
response = requests.get(
|
774
|
+
f"{auth_server}/generate/service-principal",
|
775
|
+
headers={"x-api-key": token},
|
776
|
+
data=json.dumps(
|
777
|
+
{
|
778
|
+
"servicePrincipalName": service_principal_name,
|
779
|
+
"deploymentName": deployment,
|
780
|
+
"perimeter": perimeter,
|
781
|
+
}
|
782
|
+
),
|
783
|
+
)
|
784
|
+
response.raise_for_status()
|
785
|
+
return response.json()["token"]
|
786
|
+
except Exception as e:
|
787
|
+
click.secho(
|
788
|
+
f"Failed to get origin token from {auth_server}. Error: {str(e)}", fg="red"
|
789
|
+
)
|
790
|
+
sys.exit(1)
|
791
|
+
|
792
|
+
|
689
793
|
@click.group(help="The Outerbounds Platform CLI", no_args_is_help=True)
|
690
794
|
def cli(**kwargs):
|
691
795
|
pass
|
@@ -727,7 +831,6 @@ def check(no_config, verbose, output, workstation=False):
|
|
727
831
|
]
|
728
832
|
else:
|
729
833
|
check_names = [
|
730
|
-
"metaflow_config",
|
731
834
|
"metaflow_token",
|
732
835
|
"kubeconfig",
|
733
836
|
"api_connectivity",
|
@@ -772,7 +875,7 @@ def check(no_config, verbose, output, workstation=False):
|
|
772
875
|
)
|
773
876
|
@click.argument("encoded_config", required=True)
|
774
877
|
def configure(
|
775
|
-
encoded_config
|
878
|
+
encoded_config: str, config_dir=None, profile=None, echo=None, force=False
|
776
879
|
):
|
777
880
|
writer = ConfigurationWriter(encoded_config, config_dir, profile)
|
778
881
|
try:
|
@@ -794,3 +897,116 @@ def configure(
|
|
794
897
|
except Exception as e:
|
795
898
|
click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
|
796
899
|
click.secho("Error: {}".format(str(e)))
|
900
|
+
|
901
|
+
|
902
|
+
@cli.command(
|
903
|
+
help="Authenticate service principals using JWT minted by their IDPs and configure Metaflow"
|
904
|
+
)
|
905
|
+
@click.option(
|
906
|
+
"-n",
|
907
|
+
"--name",
|
908
|
+
default="",
|
909
|
+
help="The name of service principals to authenticate",
|
910
|
+
required=True,
|
911
|
+
)
|
912
|
+
@click.option(
|
913
|
+
"--deployment-domain",
|
914
|
+
default="",
|
915
|
+
help="The full domain of the target Outerbounds Platform deployment (eg. 'foo.obp.outerbounds.com')",
|
916
|
+
required=True,
|
917
|
+
)
|
918
|
+
@click.option(
|
919
|
+
"-p",
|
920
|
+
"--perimeter",
|
921
|
+
default="default",
|
922
|
+
help="The name of the perimeter to authenticate the service principal in",
|
923
|
+
)
|
924
|
+
@click.option(
|
925
|
+
"-t",
|
926
|
+
"--jwt-token",
|
927
|
+
default="",
|
928
|
+
help="The JWT token that will be used to authenticate against the OBP Auth Server.",
|
929
|
+
)
|
930
|
+
@click.option(
|
931
|
+
"--github-actions",
|
932
|
+
is_flag=True,
|
933
|
+
help="Set if the command is being run in a GitHub Actions environment. If both --jwt-token and --github-actions are specified the --github-actions flag will be ignored.",
|
934
|
+
)
|
935
|
+
@click.option(
|
936
|
+
"-d",
|
937
|
+
"--config-dir",
|
938
|
+
default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
939
|
+
help="Path to Metaflow configuration directory",
|
940
|
+
show_default=True,
|
941
|
+
)
|
942
|
+
@click.option(
|
943
|
+
"--profile",
|
944
|
+
default="",
|
945
|
+
help="Configure a named profile. Activate the profile by setting "
|
946
|
+
"`METAFLOW_PROFILE` environment variable.",
|
947
|
+
)
|
948
|
+
@click.option(
|
949
|
+
"-e",
|
950
|
+
"--echo",
|
951
|
+
is_flag=True,
|
952
|
+
help="Print decoded configuration to stdout",
|
953
|
+
)
|
954
|
+
@click.option(
|
955
|
+
"-f",
|
956
|
+
"--force",
|
957
|
+
is_flag=True,
|
958
|
+
help="Force overwrite of existing configuration",
|
959
|
+
)
|
960
|
+
def service_principal_configure(
|
961
|
+
name: str,
|
962
|
+
deployment_domain: str,
|
963
|
+
perimeter: str,
|
964
|
+
jwt_token="",
|
965
|
+
github_actions=False,
|
966
|
+
config_dir=None,
|
967
|
+
profile=None,
|
968
|
+
echo=None,
|
969
|
+
force=False,
|
970
|
+
):
|
971
|
+
audience = f"https://{deployment_domain}"
|
972
|
+
if jwt_token == "" and github_actions:
|
973
|
+
jwt_token = get_gha_jwt(audience)
|
974
|
+
|
975
|
+
if jwt_token == "":
|
976
|
+
click.secho(
|
977
|
+
"No JWT token provided. Please provider either a valid jwt token or set --github-actions",
|
978
|
+
fg="red",
|
979
|
+
)
|
980
|
+
sys.exit(1)
|
981
|
+
|
982
|
+
auth_server = f"https://auth.{deployment_domain}"
|
983
|
+
deployment_name = deployment_domain.split(".")[0]
|
984
|
+
origin_token = get_origin_token(
|
985
|
+
name, deployment_name, perimeter, jwt_token, auth_server
|
986
|
+
)
|
987
|
+
|
988
|
+
api_server = f"https://api.{deployment_domain}"
|
989
|
+
metaflow_config = metaflowconfig.get_remote_metaflow_config_for_perimeter(
|
990
|
+
origin_token, perimeter, api_server
|
991
|
+
)
|
992
|
+
|
993
|
+
writer = ConfigurationWriter(serialize(metaflow_config), config_dir, profile)
|
994
|
+
try:
|
995
|
+
writer.decode()
|
996
|
+
except:
|
997
|
+
click.secho("Decoding the configuration text failed.", fg="red")
|
998
|
+
sys.exit(1)
|
999
|
+
try:
|
1000
|
+
writer.process_decoded_config()
|
1001
|
+
except DecodedConfigProcessingError as e:
|
1002
|
+
click.secho("Resolving the configuration remotely failed.", fg="red")
|
1003
|
+
click.secho(str(e), fg="magenta")
|
1004
|
+
sys.exit(1)
|
1005
|
+
try:
|
1006
|
+
if echo == True:
|
1007
|
+
writer.display()
|
1008
|
+
if force or writer.confirm_overwrite():
|
1009
|
+
writer.write_config()
|
1010
|
+
except Exception as e:
|
1011
|
+
click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
|
1012
|
+
click.secho("Error: {}".format(str(e)))
|