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.
Files changed (53) hide show
  1. outerbounds/_vendor/PyYAML.LICENSE +20 -0
  2. outerbounds/_vendor/__init__.py +0 -0
  3. outerbounds/_vendor/_yaml/__init__.py +34 -0
  4. outerbounds/_vendor/click/__init__.py +73 -0
  5. outerbounds/_vendor/click/_compat.py +626 -0
  6. outerbounds/_vendor/click/_termui_impl.py +717 -0
  7. outerbounds/_vendor/click/_textwrap.py +49 -0
  8. outerbounds/_vendor/click/_winconsole.py +279 -0
  9. outerbounds/_vendor/click/core.py +2998 -0
  10. outerbounds/_vendor/click/decorators.py +497 -0
  11. outerbounds/_vendor/click/exceptions.py +287 -0
  12. outerbounds/_vendor/click/formatting.py +301 -0
  13. outerbounds/_vendor/click/globals.py +68 -0
  14. outerbounds/_vendor/click/parser.py +529 -0
  15. outerbounds/_vendor/click/py.typed +0 -0
  16. outerbounds/_vendor/click/shell_completion.py +580 -0
  17. outerbounds/_vendor/click/termui.py +787 -0
  18. outerbounds/_vendor/click/testing.py +479 -0
  19. outerbounds/_vendor/click/types.py +1073 -0
  20. outerbounds/_vendor/click/utils.py +580 -0
  21. outerbounds/_vendor/click.LICENSE +28 -0
  22. outerbounds/_vendor/vendor_any.txt +2 -0
  23. outerbounds/_vendor/yaml/__init__.py +471 -0
  24. outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
  25. outerbounds/_vendor/yaml/composer.py +146 -0
  26. outerbounds/_vendor/yaml/constructor.py +862 -0
  27. outerbounds/_vendor/yaml/cyaml.py +177 -0
  28. outerbounds/_vendor/yaml/dumper.py +138 -0
  29. outerbounds/_vendor/yaml/emitter.py +1239 -0
  30. outerbounds/_vendor/yaml/error.py +94 -0
  31. outerbounds/_vendor/yaml/events.py +104 -0
  32. outerbounds/_vendor/yaml/loader.py +62 -0
  33. outerbounds/_vendor/yaml/nodes.py +51 -0
  34. outerbounds/_vendor/yaml/parser.py +629 -0
  35. outerbounds/_vendor/yaml/reader.py +208 -0
  36. outerbounds/_vendor/yaml/representer.py +378 -0
  37. outerbounds/_vendor/yaml/resolver.py +245 -0
  38. outerbounds/_vendor/yaml/scanner.py +1555 -0
  39. outerbounds/_vendor/yaml/serializer.py +127 -0
  40. outerbounds/_vendor/yaml/tokens.py +129 -0
  41. outerbounds/command_groups/cli.py +1 -1
  42. outerbounds/command_groups/local_setup_cli.py +1 -5
  43. outerbounds/command_groups/perimeters_cli.py +135 -25
  44. outerbounds/command_groups/workstations_cli.py +2 -2
  45. outerbounds/utils/kubeconfig.py +2 -2
  46. outerbounds/utils/metaflowconfig.py +68 -9
  47. outerbounds/utils/utils.py +19 -0
  48. outerbounds/vendor.py +159 -0
  49. {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/METADATA +13 -7
  50. outerbounds-0.3.89.dist-info/RECORD +57 -0
  51. outerbounds-0.3.68.dist-info/RECORD +0 -15
  52. {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/WHEEL +0 -0
  53. {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
@@ -1,4 +1,4 @@
1
- import click
1
+ from outerbounds._vendor import click
2
2
  from . import local_setup_cli
3
3
  from . import workstations_cli
4
4
  from . import perimeters_cli
@@ -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 pathlib import Path
13
- from typing import Any, Callable, Dict, List
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 kubeconfig, metaflowconfig
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")
@@ -1,5 +1,5 @@
1
- import click
2
- import yaml
1
+ from outerbounds._vendor import click
2
+ from outerbounds._vendor import yaml
3
3
  import requests
4
4
  import base64
5
5
  import datetime
@@ -1,6 +1,6 @@
1
1
  import os
2
- import yaml
3
- from yaml.scanner import ScannerError
2
+ from outerbounds._vendor import yaml
3
+ from outerbounds._vendor.yaml.scanner import ScannerError
4
4
  import subprocess
5
5
  from os import path
6
6
  import platform
@@ -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
- # This is new remote-metaflow config; fetch it from the URL
14
- if "OBP_METAFLOW_CONFIG_URL" in config:
15
- remote_config = init_config_from_url(
16
- config_dir, profile, config["OBP_METAFLOW_CONFIG_URL"]
17
- )
18
- remote_config["OBP_METAFLOW_CONFIG_URL"] = config["OBP_METAFLOW_CONFIG_URL"]
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
- # Legacy config, use from filesystem
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)