outerbounds 0.3.68__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 +1 -5
- outerbounds/command_groups/perimeters_cli.py +135 -25
- outerbounds/command_groups/workstations_cli.py +2 -2
- outerbounds/utils/kubeconfig.py +2 -2
- outerbounds/utils/metaflowconfig.py +68 -9
- outerbounds/utils/utils.py +19 -0
- outerbounds/vendor.py +159 -0
- {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/METADATA +13 -7
- outerbounds-0.3.89.dist-info/RECORD +57 -0
- outerbounds-0.3.68.dist-info/RECORD +0 -15
- {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.68.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,8 +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 click
|
14
|
+
from outerbounds._vendor import click
|
16
15
|
import requests
|
17
16
|
from requests.exceptions import HTTPError
|
18
17
|
|
@@ -641,7 +640,6 @@ class ConfigurationWriter:
|
|
641
640
|
if config_type == "inline":
|
642
641
|
if "OBP_PERIMETER" in self.decoded_config:
|
643
642
|
self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
|
644
|
-
|
645
643
|
if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
|
646
644
|
self.decoded_config = {
|
647
645
|
"OBP_METAFLOW_CONFIG_URL": self.decoded_config[
|
@@ -702,8 +700,6 @@ class ConfigurationWriter:
|
|
702
700
|
with open(config_path, "w") as fd:
|
703
701
|
json.dump(self.existing, fd, indent=4)
|
704
702
|
|
705
|
-
# Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
|
706
|
-
remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
|
707
703
|
if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
|
708
704
|
with open(self.ob_config_path, "w") as fd:
|
709
705
|
ob_config_dict = {
|
@@ -1,22 +1,13 @@
|
|
1
|
-
import base64
|
2
|
-
import hashlib
|
3
1
|
import json
|
4
2
|
import os
|
5
|
-
import re
|
6
|
-
import subprocess
|
7
3
|
import sys
|
8
|
-
import zlib
|
9
|
-
from base64 import b64decode, b64encode
|
10
|
-
from importlib.machinery import PathFinder
|
11
4
|
from os import path
|
12
|
-
from
|
13
|
-
from
|
14
|
-
|
15
|
-
import click
|
5
|
+
from typing import Any, Dict
|
6
|
+
from outerbounds._vendor import click
|
16
7
|
import requests
|
17
|
-
from requests.exceptions import HTTPError
|
18
8
|
|
19
|
-
from ..utils import
|
9
|
+
from ..utils import metaflowconfig
|
10
|
+
from ..utils.utils import safe_write_to_disk
|
20
11
|
from ..utils.schema import (
|
21
12
|
CommandStatus,
|
22
13
|
OuterboundsCommandResponse,
|
@@ -81,7 +72,7 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
|
|
81
72
|
id, perimeters, output, switch_perimeter_response, switch_perimeter_step
|
82
73
|
)
|
83
74
|
|
84
|
-
path_to_config = get_ob_config_file_path(config_dir, profile)
|
75
|
+
path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
|
85
76
|
|
86
77
|
import fcntl
|
87
78
|
|
@@ -125,9 +116,31 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
|
|
125
116
|
)
|
126
117
|
|
127
118
|
switch_perimeter_response.add_step(switch_perimeter_step)
|
119
|
+
|
120
|
+
ensure_cloud_creds_step = CommandStatus(
|
121
|
+
"EnsureCloudCredentials",
|
122
|
+
OuterboundsCommandStatus.OK,
|
123
|
+
"Cloud credentials were successfully updated.",
|
124
|
+
)
|
125
|
+
|
126
|
+
try:
|
127
|
+
ensure_cloud_credentials_for_shell(config_dir, profile)
|
128
|
+
except:
|
129
|
+
click.secho(
|
130
|
+
"Failed to update cloud credentials.",
|
131
|
+
fg="red",
|
132
|
+
err=True,
|
133
|
+
)
|
134
|
+
ensure_cloud_creds_step.update(
|
135
|
+
status=OuterboundsCommandStatus.FAIL,
|
136
|
+
reason="Failed to update cloud credentials.",
|
137
|
+
mitigation="",
|
138
|
+
)
|
139
|
+
|
140
|
+
switch_perimeter_response.add_step(ensure_cloud_creds_step)
|
141
|
+
|
128
142
|
if output == "json":
|
129
143
|
click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
|
130
|
-
return
|
131
144
|
|
132
145
|
|
133
146
|
@perimeter.command(help="Show current perimeter")
|
@@ -276,6 +289,58 @@ def list(config_dir=None, profile=None, output=""):
|
|
276
289
|
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
277
290
|
|
278
291
|
|
292
|
+
@perimeter.command(
|
293
|
+
help="Ensure credentials for cloud are synced with perimeter", hidden=True
|
294
|
+
)
|
295
|
+
@click.option(
|
296
|
+
"-d",
|
297
|
+
"--config-dir",
|
298
|
+
default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
299
|
+
help="Path to Metaflow configuration directory",
|
300
|
+
show_default=True,
|
301
|
+
)
|
302
|
+
@click.option(
|
303
|
+
"-p",
|
304
|
+
"--profile",
|
305
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
306
|
+
help="The named metaflow profile in which your workstation exists",
|
307
|
+
)
|
308
|
+
@click.option(
|
309
|
+
"-o",
|
310
|
+
"--output",
|
311
|
+
default="",
|
312
|
+
help="Show output in the specified format.",
|
313
|
+
type=click.Choice(["json", ""]),
|
314
|
+
)
|
315
|
+
def ensure_cloud_creds(config_dir=None, profile=None, output=""):
|
316
|
+
ensure_cloud_creds_step = CommandStatus(
|
317
|
+
"EnsureCloudCredentials",
|
318
|
+
OuterboundsCommandStatus.OK,
|
319
|
+
"Cloud credentials were successfully updated.",
|
320
|
+
)
|
321
|
+
|
322
|
+
ensure_cloud_creds_response = OuterboundsCommandResponse()
|
323
|
+
|
324
|
+
try:
|
325
|
+
ensure_cloud_credentials_for_shell(config_dir, profile)
|
326
|
+
click.secho("Cloud credentials updated successfully.", fg="green", err=True)
|
327
|
+
except:
|
328
|
+
click.secho(
|
329
|
+
"Failed to update cloud credentials.",
|
330
|
+
fg="red",
|
331
|
+
err=True,
|
332
|
+
)
|
333
|
+
ensure_cloud_creds_step.update(
|
334
|
+
status=OuterboundsCommandStatus.FAIL,
|
335
|
+
reason="Failed to update cloud credentials.",
|
336
|
+
mitigation="",
|
337
|
+
)
|
338
|
+
|
339
|
+
ensure_cloud_creds_response.add_step(ensure_cloud_creds_step)
|
340
|
+
if output == "json":
|
341
|
+
click.echo(json.dumps(ensure_cloud_creds_response.as_dict(), indent=4))
|
342
|
+
|
343
|
+
|
279
344
|
def get_list_perimeters_api_response(config_dir, profile):
|
280
345
|
metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
281
346
|
api_url = metaflowconfig.get_sanitized_url_from_config(
|
@@ -289,15 +354,6 @@ def get_list_perimeters_api_response(config_dir, profile):
|
|
289
354
|
return perimeters_response.json()["perimeters"]
|
290
355
|
|
291
356
|
|
292
|
-
def get_ob_config_file_path(config_dir: str, profile: str) -> str:
|
293
|
-
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
294
|
-
# If neither are set, use ~/.metaflowconfig
|
295
|
-
obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
|
296
|
-
|
297
|
-
ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
|
298
|
-
return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
|
299
|
-
|
300
|
-
|
301
357
|
def get_perimeters_from_api_or_fail_command(
|
302
358
|
config_dir: str,
|
303
359
|
profile: str,
|
@@ -332,7 +388,7 @@ def get_ob_config_or_fail_command(
|
|
332
388
|
command_response: OuterboundsCommandResponse,
|
333
389
|
command_step: CommandStatus,
|
334
390
|
) -> Dict[str, str]:
|
335
|
-
path_to_config = get_ob_config_file_path(config_dir, profile)
|
391
|
+
path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
|
336
392
|
|
337
393
|
if not os.path.exists(path_to_config):
|
338
394
|
click.secho(
|
@@ -372,6 +428,20 @@ def get_ob_config_or_fail_command(
|
|
372
428
|
return ob_config_dict
|
373
429
|
|
374
430
|
|
431
|
+
def ensure_cloud_credentials_for_shell(config_dir, profile):
|
432
|
+
if "WORKSTATION_ID" not in os.environ:
|
433
|
+
# Naive check to see if we're running in workstation. No need to ensure anything
|
434
|
+
# if this is not a workstation.
|
435
|
+
return
|
436
|
+
|
437
|
+
mf_config = metaflowconfig.init_config(config_dir, profile)
|
438
|
+
|
439
|
+
# Currently we only support GCP. TODO: utkarsh to add support for AWS and Azure
|
440
|
+
if "METAFLOW_DEFAULT_GCP_CLIENT_PROVIDER" in mf_config:
|
441
|
+
# This is a GCP deployment.
|
442
|
+
ensure_gcp_cloud_creds(config_dir, profile)
|
443
|
+
|
444
|
+
|
375
445
|
def confirm_user_has_access_to_perimeter_or_fail(
|
376
446
|
perimeter_id: str,
|
377
447
|
perimeters: Dict[str, Any],
|
@@ -396,4 +466,44 @@ def confirm_user_has_access_to_perimeter_or_fail(
|
|
396
466
|
sys.exit(1)
|
397
467
|
|
398
468
|
|
469
|
+
def ensure_gcp_cloud_creds(config_dir, profile):
|
470
|
+
token_info = get_gcp_auth_credentials(config_dir, profile)
|
471
|
+
auth_url = metaflowconfig.get_sanitized_url_from_config(
|
472
|
+
config_dir, profile, "OBP_AUTH_SERVER"
|
473
|
+
)
|
474
|
+
metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
475
|
+
|
476
|
+
# GOOGLE_APPLICATION_CREDENTIALS is a well known gcloud environment variable
|
477
|
+
credentials_file_loc = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
|
478
|
+
|
479
|
+
credentials_json = {
|
480
|
+
"type": "external_account",
|
481
|
+
"audience": f"//iam.googleapis.com/projects/{token_info['gcpProjectNumber']}/locations/global/workloadIdentityPools/{token_info['gcpWorkloadIdentityPool']}/providers/{token_info['gcpWorkloadIdentityPoolProvider']}",
|
482
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
|
483
|
+
"token_url": "https://sts.googleapis.com/v1/token",
|
484
|
+
"service_account_impersonation_url": f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{token_info['gcpServiceAccountEmail']}:generateAccessToken",
|
485
|
+
"credential_source": {
|
486
|
+
"url": f"{auth_url}/generate/gcp",
|
487
|
+
"headers": {"x-api-key": metaflow_token},
|
488
|
+
"format": {"type": "json", "subject_token_field_name": "token"},
|
489
|
+
},
|
490
|
+
}
|
491
|
+
|
492
|
+
safe_write_to_disk(credentials_file_loc, json.dumps(credentials_json))
|
493
|
+
|
494
|
+
|
495
|
+
def get_gcp_auth_credentials(config_dir, profile):
|
496
|
+
token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
497
|
+
auth_server_url = metaflowconfig.get_sanitized_url_from_config(
|
498
|
+
config_dir, profile, "OBP_AUTH_SERVER"
|
499
|
+
)
|
500
|
+
|
501
|
+
response = requests.get(
|
502
|
+
"{}/generate/gcp".format(auth_server_url), headers={"x-api-key": token}
|
503
|
+
)
|
504
|
+
response.raise_for_status()
|
505
|
+
|
506
|
+
return response.json()
|
507
|
+
|
508
|
+
|
399
509
|
cli.add_command(perimeter, name="perimeter")
|
outerbounds/utils/kubeconfig.py
CHANGED
@@ -1,23 +1,45 @@
|
|
1
|
-
import click
|
1
|
+
from outerbounds._vendor import click
|
2
2
|
import json
|
3
3
|
import os
|
4
4
|
import requests
|
5
5
|
from os import path
|
6
|
-
from typing import Dict
|
6
|
+
from typing import Dict, Union
|
7
7
|
import sys
|
8
8
|
|
9
|
+
"""
|
10
|
+
key: perimeter specific URL to fetch the remote metaflow config from
|
11
|
+
value: the remote metaflow config
|
12
|
+
"""
|
13
|
+
CACHED_REMOTE_METAFLOW_CONFIG: Dict[str, Dict[str, str]] = {}
|
14
|
+
|
15
|
+
|
16
|
+
CURRENT_PERIMETER_KEY = "OB_CURRENT_PERIMETER"
|
17
|
+
CURRENT_PERIMETER_URL = "OB_CURRENT_PERIMETER_MF_CONFIG_URL"
|
18
|
+
CURRENT_PERIMETER_URL_LEGACY_KEY = (
|
19
|
+
"OB_CURRENT_PERIMETER_URL" # For backwards compatibility with workstations.
|
20
|
+
)
|
21
|
+
|
9
22
|
|
10
23
|
def init_config(config_dir, profile) -> Dict[str, str]:
|
24
|
+
global CACHED_REMOTE_METAFLOW_CONFIG
|
11
25
|
config = read_metaflow_config_from_filesystem(config_dir, profile)
|
12
26
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
27
|
+
# Either user has an ob_config.json file with the perimeter URL
|
28
|
+
# or the default config on the filesystem has the config URL in it.
|
29
|
+
perimeter_specifc_url = get_perimeter_config_url_if_set_in_ob_config(
|
30
|
+
config_dir, profile
|
31
|
+
) or config.get("OBP_METAFLOW_CONFIG_URL", "")
|
32
|
+
|
33
|
+
if perimeter_specifc_url != "":
|
34
|
+
if perimeter_specifc_url in CACHED_REMOTE_METAFLOW_CONFIG:
|
35
|
+
return CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url]
|
36
|
+
|
37
|
+
remote_config = init_config_from_url(config_dir, profile, perimeter_specifc_url)
|
38
|
+
remote_config["OBP_METAFLOW_CONFIG_URL"] = perimeter_specifc_url
|
39
|
+
|
40
|
+
CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url] = remote_config
|
19
41
|
return remote_config
|
20
|
-
|
42
|
+
|
21
43
|
return config
|
22
44
|
|
23
45
|
|
@@ -101,3 +123,40 @@ def get_remote_metaflow_config_for_perimeter(
|
|
101
123
|
fg="red",
|
102
124
|
)
|
103
125
|
sys.exit(1)
|
126
|
+
|
127
|
+
|
128
|
+
def get_ob_config_file_path(config_dir: str, profile: str) -> str:
|
129
|
+
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
130
|
+
# If neither are set, use ~/.metaflowconfig
|
131
|
+
obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
|
132
|
+
|
133
|
+
ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
|
134
|
+
return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
|
135
|
+
|
136
|
+
|
137
|
+
def get_perimeter_config_url_if_set_in_ob_config(
|
138
|
+
config_dir: str, profile: str
|
139
|
+
) -> Union[str, None]:
|
140
|
+
file_path = get_ob_config_file_path(config_dir, profile)
|
141
|
+
|
142
|
+
if os.path.exists(file_path):
|
143
|
+
with open(file_path, "r") as f:
|
144
|
+
ob_config = json.loads(f.read())
|
145
|
+
|
146
|
+
if CURRENT_PERIMETER_URL in ob_config:
|
147
|
+
return ob_config[CURRENT_PERIMETER_URL]
|
148
|
+
elif CURRENT_PERIMETER_URL_LEGACY_KEY in ob_config:
|
149
|
+
return ob_config[CURRENT_PERIMETER_URL_LEGACY_KEY]
|
150
|
+
else:
|
151
|
+
raise ValueError(
|
152
|
+
"{} does not contain the key {}".format(
|
153
|
+
file_path, CURRENT_PERIMETER_KEY
|
154
|
+
)
|
155
|
+
)
|
156
|
+
elif "OBP_CONFIG_DIR" in os.environ:
|
157
|
+
raise FileNotFoundError(
|
158
|
+
"Environment variable OBP_CONFIG_DIR is set to {} but this directory does not contain an ob_config.json file.".format(
|
159
|
+
os.environ["OBP_CONFIG_DIR"]
|
160
|
+
)
|
161
|
+
)
|
162
|
+
return None
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import tempfile
|
2
|
+
import os
|
3
|
+
|
4
|
+
"""
|
5
|
+
Writes the given data to file_loc. Ensures that the directory exists
|
6
|
+
and uses a temporary file to ensure that the file is written atomically.
|
7
|
+
"""
|
8
|
+
|
9
|
+
|
10
|
+
def safe_write_to_disk(file_loc, data):
|
11
|
+
# Ensure the directory exists
|
12
|
+
os.makedirs(os.path.dirname(file_loc), exist_ok=True)
|
13
|
+
|
14
|
+
with tempfile.NamedTemporaryFile(
|
15
|
+
"w", dir=os.path.dirname(file_loc), delete=False
|
16
|
+
) as f:
|
17
|
+
f.write(data)
|
18
|
+
tmp_file = f.name
|
19
|
+
os.rename(tmp_file, file_loc)
|