outerbounds 0.3.55rc4__py3-none-any.whl → 0.3.89__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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)))
|