outerbounds 0.3.55rc4__py3-none-any.whl → 0.3.89__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) 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 +249 -33
  43. outerbounds/command_groups/perimeters_cli.py +168 -33
  44. outerbounds/command_groups/workstations_cli.py +29 -16
  45. outerbounds/utils/kubeconfig.py +2 -2
  46. outerbounds/utils/metaflowconfig.py +111 -21
  47. outerbounds/utils/schema.py +6 -0
  48. outerbounds/utils/utils.py +19 -0
  49. outerbounds/vendor.py +159 -0
  50. {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/METADATA +14 -6
  51. outerbounds-0.3.89.dist-info/RECORD +57 -0
  52. outerbounds-0.3.55rc4.dist-info/RECORD +0 -15
  53. {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/WHEEL +0 -0
  54. {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/entry_points.txt +0 -0
@@ -1,36 +1,33 @@
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 boto3
16
- import click
5
+ from typing import Any, Dict
6
+ from outerbounds._vendor import click
17
7
  import requests
18
- from requests.exceptions import HTTPError
19
8
 
20
- from ..utils import kubeconfig, metaflowconfig
9
+ from ..utils import metaflowconfig
10
+ from ..utils.utils import safe_write_to_disk
21
11
  from ..utils.schema import (
22
12
  CommandStatus,
23
13
  OuterboundsCommandResponse,
24
14
  OuterboundsCommandStatus,
25
15
  )
26
16
 
17
+ from .local_setup_cli import PERIMETER_CONFIG_URL_KEY
18
+
27
19
 
28
20
  @click.group()
29
21
  def cli(**kwargs):
30
22
  pass
31
23
 
32
24
 
33
- @cli.command(help="Switch current perimeter", hidden=True)
25
+ @click.group(help="Manage perimeters")
26
+ def perimeter(**kwargs):
27
+ pass
28
+
29
+
30
+ @perimeter.command(help="Switch current perimeter")
34
31
  @click.option(
35
32
  "-d",
36
33
  "--config-dir",
@@ -59,7 +56,7 @@ def cli(**kwargs):
59
56
  help="Force change the existing perimeter",
60
57
  default=False,
61
58
  )
62
- def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=False):
59
+ def switch(config_dir=None, profile=None, output="", id=None, force=False):
63
60
  switch_perimeter_response = OuterboundsCommandResponse()
64
61
 
65
62
  switch_perimeter_step = CommandStatus(
@@ -75,7 +72,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
75
72
  id, perimeters, output, switch_perimeter_response, switch_perimeter_step
76
73
  )
77
74
 
78
- path_to_config = get_ob_config_file_path(config_dir, profile)
75
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
79
76
 
80
77
  import fcntl
81
78
 
@@ -94,7 +91,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
94
91
 
95
92
  ob_config_dict = {
96
93
  "OB_CURRENT_PERIMETER": str(id),
97
- "OB_CURRENT_PERIMETER_URL": perimeters[id]["remote_config_url"],
94
+ PERIMETER_CONFIG_URL_KEY: perimeters[id]["remote_config_url"],
98
95
  }
99
96
 
100
97
  # Now that we have the lock, we can safely write to the file
@@ -119,12 +116,34 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
119
116
  )
120
117
 
121
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
+
122
142
  if output == "json":
123
143
  click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
124
- return
125
144
 
126
145
 
127
- @cli.command(help="Show current perimeter", hidden=True)
146
+ @perimeter.command(help="Show current perimeter")
128
147
  @click.option(
129
148
  "-d",
130
149
  "--config-dir",
@@ -146,7 +165,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
146
165
  help="Show output in the specified format.",
147
166
  type=click.Choice(["json", ""]),
148
167
  )
149
- def show_current_perimeter(config_dir=None, profile=None, output=""):
168
+ def show_current(config_dir=None, profile=None, output=""):
150
169
  show_current_perimeter_response = OuterboundsCommandResponse()
151
170
 
152
171
  show_current_perimeter_step = CommandStatus(
@@ -192,7 +211,7 @@ def show_current_perimeter(config_dir=None, profile=None, output=""):
192
211
  click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
193
212
 
194
213
 
195
- @cli.command(help="List all available perimeters", hidden=True)
214
+ @perimeter.command(help="List all available perimeters")
196
215
  @click.option(
197
216
  "-d",
198
217
  "--config-dir",
@@ -213,13 +232,29 @@ def show_current_perimeter(config_dir=None, profile=None, output=""):
213
232
  help="Show output in the specified format.",
214
233
  type=click.Choice(["json", ""]),
215
234
  )
216
- def list_perimeters(config_dir=None, profile=None, output=""):
235
+ def list(config_dir=None, profile=None, output=""):
217
236
  list_perimeters_response = OuterboundsCommandResponse()
218
237
 
219
238
  list_perimeters_step = CommandStatus(
220
239
  "ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
221
240
  )
222
241
 
242
+ if "WORKSTATION_ID" in os.environ and (
243
+ "OBP_DEFAULT_PERIMETER" not in os.environ
244
+ or "OBP_DEFAULT_PERIMETER_URL" not in os.environ
245
+ ):
246
+ list_perimeters_response.update(
247
+ OuterboundsCommandStatus.NOT_SUPPORTED,
248
+ 500,
249
+ "Perimeters are not supported on old workstations.",
250
+ )
251
+ click.secho(
252
+ "Perimeters are not supported on old workstations.", err=True, fg="red"
253
+ )
254
+ if output == "json":
255
+ click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
256
+ return
257
+
223
258
  ob_config_dict = get_ob_config_or_fail_command(
224
259
  config_dir, profile, output, list_perimeters_response, list_perimeters_step
225
260
  )
@@ -254,6 +289,58 @@ def list_perimeters(config_dir=None, profile=None, output=""):
254
289
  click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
255
290
 
256
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
+
257
344
  def get_list_perimeters_api_response(config_dir, profile):
258
345
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
259
346
  api_url = metaflowconfig.get_sanitized_url_from_config(
@@ -267,15 +354,6 @@ def get_list_perimeters_api_response(config_dir, profile):
267
354
  return perimeters_response.json()["perimeters"]
268
355
 
269
356
 
270
- def get_ob_config_file_path(config_dir: str, profile: str) -> str:
271
- # If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
272
- # If neither are set, use ~/.metaflowconfig
273
- obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
274
-
275
- ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
276
- return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
277
-
278
-
279
357
  def get_perimeters_from_api_or_fail_command(
280
358
  config_dir: str,
281
359
  profile: str,
@@ -310,7 +388,7 @@ def get_ob_config_or_fail_command(
310
388
  command_response: OuterboundsCommandResponse,
311
389
  command_step: CommandStatus,
312
390
  ) -> Dict[str, str]:
313
- path_to_config = get_ob_config_file_path(config_dir, profile)
391
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
314
392
 
315
393
  if not os.path.exists(path_to_config):
316
394
  click.secho(
@@ -350,6 +428,20 @@ def get_ob_config_or_fail_command(
350
428
  return ob_config_dict
351
429
 
352
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
+
353
445
  def confirm_user_has_access_to_perimeter_or_fail(
354
446
  perimeter_id: str,
355
447
  perimeters: Dict[str, Any],
@@ -372,3 +464,46 @@ def confirm_user_has_access_to_perimeter_or_fail(
372
464
  if output == "json":
373
465
  click.echo(json.dumps(command_response.as_dict(), indent=4))
374
466
  sys.exit(1)
467
+
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
+
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
@@ -19,6 +19,10 @@ from ..utils.schema import (
19
19
  OuterboundsCommandStatus,
20
20
  )
21
21
  from tempfile import NamedTemporaryFile
22
+ from .perimeters_cli import (
23
+ get_perimeters_from_api_or_fail_command,
24
+ confirm_user_has_access_to_perimeter_or_fail,
25
+ )
22
26
 
23
27
  KUBECTL_INSTALL_MITIGATION = "Please install kubectl manually from https://kubernetes.io/docs/tasks/tools/#kubectl"
24
28
 
@@ -89,7 +93,7 @@ def generate_workstation_token(config_dir=None, profile=None):
89
93
  @click.option(
90
94
  "-p",
91
95
  "--profile",
92
- default="",
96
+ default=os.environ.get("METAFLOW_PROFILE", ""),
93
97
  help="The named metaflow profile in which your workstation exists",
94
98
  )
95
99
  @click.option(
@@ -111,9 +115,6 @@ def configure_cloud_workstation(config_dir=None, profile=None, binary=None, outp
111
115
  "ConfigureKubeConfig", OuterboundsCommandStatus.OK, "Kubeconfig is configured"
112
116
  )
113
117
  try:
114
- if not profile:
115
- profile = metaflowconfig.get_metaflow_profile()
116
-
117
118
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
118
119
  config_dir, profile
119
120
  )
@@ -193,7 +194,7 @@ def configure_cloud_workstation(config_dir=None, profile=None, binary=None, outp
193
194
  @click.option(
194
195
  "-p",
195
196
  "--profile",
196
- default="",
197
+ default=os.environ.get("METAFLOW_PROFILE", ""),
197
198
  help="The named metaflow profile in which your workstation exists",
198
199
  )
199
200
  @click.option(
@@ -213,8 +214,6 @@ def list_workstations(config_dir=None, profile=None, output="json"):
213
214
  list_response.add_or_update_data("workstations", [])
214
215
 
215
216
  try:
216
- if not profile:
217
- profile = metaflowconfig.get_metaflow_profile()
218
217
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
219
218
  config_dir, profile
220
219
  )
@@ -260,7 +259,7 @@ def list_workstations(config_dir=None, profile=None, output="json"):
260
259
  @click.option(
261
260
  "-w",
262
261
  "--workstation",
263
- default="",
262
+ default=os.environ.get("METAFLOW_PROFILE", ""),
264
263
  help="The ID of the workstation to hibernate",
265
264
  )
266
265
  def hibernate_workstation(config_dir=None, profile=None, workstation=None):
@@ -308,7 +307,7 @@ def hibernate_workstation(config_dir=None, profile=None, workstation=None):
308
307
  @click.option(
309
308
  "-p",
310
309
  "--profile",
311
- default="",
310
+ default=os.environ.get("METAFLOW_PROFILE", ""),
312
311
  help="The named metaflow profile in which your workstation exists",
313
312
  )
314
313
  @click.option(
@@ -322,9 +321,6 @@ def restart_workstation(config_dir=None, profile=None, workstation=None):
322
321
  click.secho("Please specify a workstation ID", fg="red")
323
322
  return
324
323
  try:
325
- if not profile:
326
- profile = metaflowconfig.get_metaflow_profile()
327
-
328
324
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
329
325
  config_dir, profile
330
326
  )
@@ -516,7 +512,7 @@ def add_to_path(program_path, platform):
516
512
  with open(path_to_rc_file, "a+") as f: # Open bashrc file
517
513
  if program_path not in f.read():
518
514
  f.write("\n# Added by Outerbounds\n")
519
- f.write(program_path)
515
+ f.write(f"export PATH=$PATH:{program_path}")
520
516
 
521
517
 
522
518
  def to_windows_path(path):
@@ -559,7 +555,24 @@ def show_relevant_links(config_dir=None, profile=None, perimeter_id="", output="
559
555
  show_links_response.add_or_update_data("links", [])
560
556
  links = []
561
557
  try:
562
- metaflow_config = metaflowconfig.init_config(config_dir, profile)
558
+ if not perimeter_id:
559
+ metaflow_config = metaflowconfig.init_config(config_dir, profile)
560
+ else:
561
+ perimeters_dict = get_perimeters_from_api_or_fail_command(
562
+ config_dir, profile, output, show_links_response, show_links_step
563
+ )
564
+ confirm_user_has_access_to_perimeter_or_fail(
565
+ perimeter_id,
566
+ perimeters_dict,
567
+ output,
568
+ show_links_response,
569
+ show_links_step,
570
+ )
571
+
572
+ metaflow_config = metaflowconfig.init_config_from_url(
573
+ config_dir, profile, perimeters_dict[perimeter_id]["remote_config_url"]
574
+ )
575
+
563
576
  links.append(
564
577
  {
565
578
  "id": "metaflow-ui-url",
@@ -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,17 +1,66 @@
1
+ from outerbounds._vendor import click
1
2
  import json
2
3
  import os
3
4
  import requests
4
5
  from os import path
5
- import requests
6
+ from typing import Dict, Union
7
+ import sys
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
+
22
+
23
+ def init_config(config_dir, profile) -> Dict[str, str]:
24
+ global CACHED_REMOTE_METAFLOW_CONFIG
25
+ config = read_metaflow_config_from_filesystem(config_dir, profile)
26
+
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
6
39
 
40
+ CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url] = remote_config
41
+ return remote_config
42
+
43
+ return config
44
+
45
+
46
+ def init_config_from_url(config_dir, profile, url) -> Dict[str, str]:
47
+ config = read_metaflow_config_from_filesystem(config_dir, profile)
7
48
 
8
- def init_config(config_dir="", profile="") -> dict:
9
- profile = profile or os.environ.get("METAFLOW_PROFILE")
10
- config_dir = config_dir or os.path.expanduser(
11
- os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
49
+ if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
50
+ raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
51
+
52
+ config_response = requests.get(
53
+ url,
54
+ headers={"x-api-key": f'{config["METAFLOW_SERVICE_AUTH_KEY"]}'},
12
55
  )
56
+ config_response.raise_for_status()
57
+ remote_config = config_response.json()["config"]
58
+ return remote_config
59
+
13
60
 
61
+ def read_metaflow_config_from_filesystem(config_dir, profile) -> Dict[str, str]:
14
62
  config_filename = f"config_{profile}.json" if profile else "config.json"
63
+
15
64
  path_to_config = os.path.join(config_dir, config_filename)
16
65
 
17
66
  if os.path.exists(path_to_config):
@@ -19,22 +68,6 @@ def init_config(config_dir="", profile="") -> dict:
19
68
  config = json.load(json_file)
20
69
  else:
21
70
  raise Exception("Unable to locate metaflow config at '%s')" % (path_to_config))
22
-
23
- # This is new remote-metaflow config; fetch it from the URL
24
- if "OBP_METAFLOW_CONFIG_URL" in config:
25
- if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
26
- raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
27
-
28
- config_response = requests.get(
29
- config["OBP_METAFLOW_CONFIG_URL"],
30
- headers={"x-api-key": f'{config["METAFLOW_SERVICE_AUTH_KEY"]}'},
31
- )
32
- config_response.raise_for_status()
33
- remote_config = config_response.json()["config"]
34
- remote_config["METAFLOW_SERVICE_AUTH_KEY"] = config["METAFLOW_SERVICE_AUTH_KEY"]
35
- return remote_config
36
-
37
- # Legacy config, use from filesystem
38
71
  return config
39
72
 
40
73
 
@@ -70,3 +103,60 @@ def get_sanitized_url_from_config(config_dir: str, profile: str, key: str) -> st
70
103
 
71
104
  url_in_config = url_in_config.rstrip("/")
72
105
  return url_in_config
106
+
107
+
108
+ def get_remote_metaflow_config_for_perimeter(
109
+ origin_token: str, perimeter: str, api_server: str
110
+ ):
111
+ try:
112
+ response = requests.get(
113
+ f"{api_server}/v1/perimeters/{perimeter}/metaflowconfigs/default",
114
+ headers={"x-api-key": origin_token},
115
+ )
116
+ response.raise_for_status()
117
+ config = response.json()["config"]
118
+ config["METAFLOW_SERVICE_AUTH_KEY"] = origin_token
119
+ return config
120
+ except Exception as e:
121
+ click.secho(
122
+ f"Failed to get metaflow config from {api_server}. Error: {str(e)}",
123
+ fg="red",
124
+ )
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
@@ -5,6 +5,7 @@ class OuterboundsCommandStatus(Enum):
5
5
  OK = "OK"
6
6
  FAIL = "FAIL"
7
7
  WARN = "WARN"
8
+ NOT_SUPPORTED = "NOT_SUPPORTED"
8
9
 
9
10
 
10
11
  class CommandStatus:
@@ -39,6 +40,11 @@ class OuterboundsCommandResponse:
39
40
  self.metadata = {}
40
41
  self._data = {}
41
42
 
43
+ def update(self, status, code, message):
44
+ self.status = status
45
+ self._code = code
46
+ self._message = message
47
+
42
48
  def add_or_update_metadata(self, key, value):
43
49
  self.metadata[key] = value
44
50
 
@@ -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)