outerbounds 0.3.68__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 +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)
|