outerbounds 0.3.55rc3__py3-none-any.whl → 0.3.133__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) 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/apps_cli.py +450 -0
  42. outerbounds/command_groups/cli.py +9 -5
  43. outerbounds/command_groups/local_setup_cli.py +249 -33
  44. outerbounds/command_groups/perimeters_cli.py +231 -33
  45. outerbounds/command_groups/tutorials_cli.py +111 -0
  46. outerbounds/command_groups/workstations_cli.py +88 -15
  47. outerbounds/utils/kubeconfig.py +2 -2
  48. outerbounds/utils/metaflowconfig.py +111 -21
  49. outerbounds/utils/schema.py +8 -2
  50. outerbounds/utils/utils.py +19 -0
  51. outerbounds/vendor.py +159 -0
  52. {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/METADATA +17 -6
  53. outerbounds-0.3.133.dist-info/RECORD +59 -0
  54. {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/WHEEL +1 -1
  55. outerbounds-0.3.55rc3.dist-info/RECORD +0 -15
  56. {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/entry_points.txt +0 -0
@@ -1,36 +1,35 @@
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
4
+ from io import StringIO
11
5
  from os import path
12
- from pathlib import Path
13
- from typing import Any, Callable, Dict, List
14
-
15
- import boto3
16
- import click
6
+ from typing import Any, Dict
7
+ from outerbounds._vendor import click
17
8
  import requests
18
- from requests.exceptions import HTTPError
9
+ import configparser
19
10
 
20
- from ..utils import kubeconfig, metaflowconfig
11
+ from ..utils import metaflowconfig
12
+ from ..utils.utils import safe_write_to_disk
21
13
  from ..utils.schema import (
22
14
  CommandStatus,
23
15
  OuterboundsCommandResponse,
24
16
  OuterboundsCommandStatus,
25
17
  )
26
18
 
19
+ from .local_setup_cli import PERIMETER_CONFIG_URL_KEY
20
+
27
21
 
28
22
  @click.group()
29
23
  def cli(**kwargs):
30
24
  pass
31
25
 
32
26
 
33
- @cli.command(help="Switch current perimeter", hidden=True)
27
+ @click.group(help="Manage perimeters")
28
+ def perimeter(**kwargs):
29
+ pass
30
+
31
+
32
+ @perimeter.command(help="Switch current perimeter")
34
33
  @click.option(
35
34
  "-d",
36
35
  "--config-dir",
@@ -59,7 +58,7 @@ def cli(**kwargs):
59
58
  help="Force change the existing perimeter",
60
59
  default=False,
61
60
  )
62
- def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=False):
61
+ def switch(config_dir=None, profile=None, output="", id=None, force=False):
63
62
  switch_perimeter_response = OuterboundsCommandResponse()
64
63
 
65
64
  switch_perimeter_step = CommandStatus(
@@ -75,7 +74,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
75
74
  id, perimeters, output, switch_perimeter_response, switch_perimeter_step
76
75
  )
77
76
 
78
- path_to_config = get_ob_config_file_path(config_dir, profile)
77
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
79
78
 
80
79
  import fcntl
81
80
 
@@ -94,7 +93,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
94
93
 
95
94
  ob_config_dict = {
96
95
  "OB_CURRENT_PERIMETER": str(id),
97
- "OB_CURRENT_PERIMETER_URL": perimeters[id]["remote_config_url"],
96
+ PERIMETER_CONFIG_URL_KEY: perimeters[id]["remote_config_url"],
98
97
  }
99
98
 
100
99
  # Now that we have the lock, we can safely write to the file
@@ -119,12 +118,34 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
119
118
  )
120
119
 
121
120
  switch_perimeter_response.add_step(switch_perimeter_step)
121
+
122
+ ensure_cloud_creds_step = CommandStatus(
123
+ "EnsureCloudCredentials",
124
+ OuterboundsCommandStatus.OK,
125
+ "Cloud credentials were successfully updated.",
126
+ )
127
+
128
+ try:
129
+ ensure_cloud_credentials_for_shell(config_dir, profile)
130
+ except:
131
+ click.secho(
132
+ "Failed to update cloud credentials.",
133
+ fg="red",
134
+ err=True,
135
+ )
136
+ ensure_cloud_creds_step.update(
137
+ status=OuterboundsCommandStatus.FAIL,
138
+ reason="Failed to update cloud credentials.",
139
+ mitigation="",
140
+ )
141
+
142
+ switch_perimeter_response.add_step(ensure_cloud_creds_step)
143
+
122
144
  if output == "json":
123
145
  click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
124
- return
125
146
 
126
147
 
127
- @cli.command(help="Show current perimeter", hidden=True)
148
+ @perimeter.command(help="Show current perimeter")
128
149
  @click.option(
129
150
  "-d",
130
151
  "--config-dir",
@@ -146,7 +167,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
146
167
  help="Show output in the specified format.",
147
168
  type=click.Choice(["json", ""]),
148
169
  )
149
- def show_current_perimeter(config_dir=None, profile=None, output=""):
170
+ def show_current(config_dir=None, profile=None, output=""):
150
171
  show_current_perimeter_response = OuterboundsCommandResponse()
151
172
 
152
173
  show_current_perimeter_step = CommandStatus(
@@ -192,7 +213,7 @@ def show_current_perimeter(config_dir=None, profile=None, output=""):
192
213
  click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
193
214
 
194
215
 
195
- @cli.command(help="List all available perimeters", hidden=True)
216
+ @perimeter.command(help="List all available perimeters")
196
217
  @click.option(
197
218
  "-d",
198
219
  "--config-dir",
@@ -213,13 +234,29 @@ def show_current_perimeter(config_dir=None, profile=None, output=""):
213
234
  help="Show output in the specified format.",
214
235
  type=click.Choice(["json", ""]),
215
236
  )
216
- def list_perimeters(config_dir=None, profile=None, output=""):
237
+ def list(config_dir=None, profile=None, output=""):
217
238
  list_perimeters_response = OuterboundsCommandResponse()
218
239
 
219
240
  list_perimeters_step = CommandStatus(
220
241
  "ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
221
242
  )
222
243
 
244
+ if "WORKSTATION_ID" in os.environ and (
245
+ "OBP_DEFAULT_PERIMETER" not in os.environ
246
+ or "OBP_DEFAULT_PERIMETER_URL" not in os.environ
247
+ ):
248
+ list_perimeters_response.update(
249
+ OuterboundsCommandStatus.NOT_SUPPORTED,
250
+ 500,
251
+ "Perimeters are not supported on old workstations.",
252
+ )
253
+ click.secho(
254
+ "Perimeters are not supported on old workstations.", err=True, fg="red"
255
+ )
256
+ if output == "json":
257
+ click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
258
+ return
259
+
223
260
  ob_config_dict = get_ob_config_or_fail_command(
224
261
  config_dir, profile, output, list_perimeters_response, list_perimeters_step
225
262
  )
@@ -254,6 +291,58 @@ def list_perimeters(config_dir=None, profile=None, output=""):
254
291
  click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
255
292
 
256
293
 
294
+ @perimeter.command(
295
+ help="Ensure credentials for cloud are synced with perimeter", hidden=True
296
+ )
297
+ @click.option(
298
+ "-d",
299
+ "--config-dir",
300
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
301
+ help="Path to Metaflow configuration directory",
302
+ show_default=True,
303
+ )
304
+ @click.option(
305
+ "-p",
306
+ "--profile",
307
+ default=os.environ.get("METAFLOW_PROFILE", ""),
308
+ help="The named metaflow profile in which your workstation exists",
309
+ )
310
+ @click.option(
311
+ "-o",
312
+ "--output",
313
+ default="",
314
+ help="Show output in the specified format.",
315
+ type=click.Choice(["json", ""]),
316
+ )
317
+ def ensure_cloud_creds(config_dir=None, profile=None, output=""):
318
+ ensure_cloud_creds_step = CommandStatus(
319
+ "EnsureCloudCredentials",
320
+ OuterboundsCommandStatus.OK,
321
+ "Cloud credentials were successfully updated.",
322
+ )
323
+
324
+ ensure_cloud_creds_response = OuterboundsCommandResponse()
325
+
326
+ try:
327
+ ensure_cloud_credentials_for_shell(config_dir, profile)
328
+ click.secho("Cloud credentials updated successfully.", fg="green", err=True)
329
+ except:
330
+ click.secho(
331
+ "Failed to update cloud credentials.",
332
+ fg="red",
333
+ err=True,
334
+ )
335
+ ensure_cloud_creds_step.update(
336
+ status=OuterboundsCommandStatus.FAIL,
337
+ reason="Failed to update cloud credentials.",
338
+ mitigation="",
339
+ )
340
+
341
+ ensure_cloud_creds_response.add_step(ensure_cloud_creds_step)
342
+ if output == "json":
343
+ click.echo(json.dumps(ensure_cloud_creds_response.as_dict(), indent=4))
344
+
345
+
257
346
  def get_list_perimeters_api_response(config_dir, profile):
258
347
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
259
348
  api_url = metaflowconfig.get_sanitized_url_from_config(
@@ -267,15 +356,6 @@ def get_list_perimeters_api_response(config_dir, profile):
267
356
  return perimeters_response.json()["perimeters"]
268
357
 
269
358
 
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
359
  def get_perimeters_from_api_or_fail_command(
280
360
  config_dir: str,
281
361
  profile: str,
@@ -310,7 +390,7 @@ def get_ob_config_or_fail_command(
310
390
  command_response: OuterboundsCommandResponse,
311
391
  command_step: CommandStatus,
312
392
  ) -> Dict[str, str]:
313
- path_to_config = get_ob_config_file_path(config_dir, profile)
393
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
314
394
 
315
395
  if not os.path.exists(path_to_config):
316
396
  click.secho(
@@ -350,6 +430,23 @@ def get_ob_config_or_fail_command(
350
430
  return ob_config_dict
351
431
 
352
432
 
433
+ def ensure_cloud_credentials_for_shell(config_dir, profile):
434
+ if "WORKSTATION_ID" not in os.environ:
435
+ # Naive check to see if we're running in workstation. No need to ensure anything
436
+ # if this is not a workstation.
437
+ return
438
+
439
+ mf_config = metaflowconfig.init_config(config_dir, profile)
440
+
441
+ # Currently we only support GCP. TODO: utkarsh to add support for AWS and Azure
442
+ if "METAFLOW_DEFAULT_GCP_CLIENT_PROVIDER" in mf_config:
443
+ # This is a GCP deployment.
444
+ ensure_gcp_cloud_creds(config_dir, profile)
445
+ elif "METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER" in mf_config:
446
+ # This is an AWS deployment.
447
+ ensure_aws_cloud_creds(config_dir, profile)
448
+
449
+
353
450
  def confirm_user_has_access_to_perimeter_or_fail(
354
451
  perimeter_id: str,
355
452
  perimeters: Dict[str, Any],
@@ -372,3 +469,104 @@ def confirm_user_has_access_to_perimeter_or_fail(
372
469
  if output == "json":
373
470
  click.echo(json.dumps(command_response.as_dict(), indent=4))
374
471
  sys.exit(1)
472
+
473
+
474
+ def ensure_gcp_cloud_creds(config_dir, profile):
475
+ token_info = get_gcp_auth_credentials(config_dir, profile)
476
+ auth_url = metaflowconfig.get_sanitized_url_from_config(
477
+ config_dir, profile, "OBP_AUTH_SERVER"
478
+ )
479
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
480
+
481
+ try:
482
+ # GOOGLE_APPLICATION_CREDENTIALS is a well known gcloud environment variable
483
+ credentials_file_loc = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
484
+ except KeyError:
485
+ # This is most likely an old workstation when these params weren't set. Do nothing.
486
+ # Alternatively, user might have deliberately unset it to use their own auth.
487
+ return
488
+
489
+ credentials_json = {
490
+ "type": "external_account",
491
+ "audience": f"//iam.googleapis.com/projects/{token_info['gcpProjectNumber']}/locations/global/workloadIdentityPools/{token_info['gcpWorkloadIdentityPool']}/providers/{token_info['gcpWorkloadIdentityPoolProvider']}",
492
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
493
+ "token_url": "https://sts.googleapis.com/v1/token",
494
+ "service_account_impersonation_url": f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{token_info['gcpServiceAccountEmail']}:generateAccessToken",
495
+ "credential_source": {
496
+ "url": f"{auth_url}/generate/gcp",
497
+ "headers": {"x-api-key": metaflow_token},
498
+ "format": {"type": "json", "subject_token_field_name": "token"},
499
+ },
500
+ }
501
+
502
+ safe_write_to_disk(credentials_file_loc, json.dumps(credentials_json))
503
+
504
+
505
+ def ensure_aws_cloud_creds(config_dir, profile):
506
+ token_info = get_aws_auth_credentials(config_dir, profile)
507
+
508
+ try:
509
+ token_file_loc = os.environ["OBP_AWS_WEB_IDENTITY_TOKEN_FILE"]
510
+
511
+ # AWS_CONFIG_FILE is a well known aws cli environment variable
512
+ config_file_loc = os.environ["AWS_CONFIG_FILE"]
513
+ except KeyError:
514
+ # This is most likely an old workstation when these params weren't set. Do nothing.
515
+ # Alternatively, user might have deliberately unset it to use their own auth.
516
+ return
517
+
518
+ aws_config = configparser.ConfigParser()
519
+ aws_config.read(config_file_loc)
520
+
521
+ aws_config["profile task"] = {
522
+ "role_arn": token_info["role_arn"],
523
+ "web_identity_token_file": token_file_loc,
524
+ }
525
+
526
+ if token_info.get("cspr_role_arn"):
527
+ # If CSPR role is present, then we need to use the task role (in the task profile)
528
+ # to assume the CSPR role.
529
+ aws_config["profile outerbounds"] = {
530
+ "role_arn": token_info["cspr_role_arn"],
531
+ "source_profile": "task",
532
+ }
533
+ else:
534
+ # If no CSPR role is present, just use the task profile as the outerbounds profile.
535
+ aws_config["profile outerbounds"] = aws_config["profile task"]
536
+
537
+ aws_config_string = StringIO()
538
+ aws_config.write(aws_config_string)
539
+
540
+ safe_write_to_disk(token_file_loc, token_info["token"])
541
+ safe_write_to_disk(config_file_loc, aws_config_string.getvalue())
542
+
543
+
544
+ def get_aws_auth_credentials(config_dir, profile): # pragma: no cover
545
+ token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
546
+ auth_server_url = metaflowconfig.get_sanitized_url_from_config(
547
+ config_dir, profile, "OBP_AUTH_SERVER"
548
+ )
549
+
550
+ response = requests.get(
551
+ "{}/generate/aws".format(auth_server_url), headers={"x-api-key": token}
552
+ )
553
+ response.raise_for_status()
554
+
555
+ return response.json()
556
+
557
+
558
+ def get_gcp_auth_credentials(config_dir, profile):
559
+ token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
560
+ auth_server_url = metaflowconfig.get_sanitized_url_from_config(
561
+ config_dir, profile, "OBP_AUTH_SERVER"
562
+ )
563
+
564
+ response = requests.get(
565
+ "{}/generate/gcp".format(auth_server_url), headers={"x-api-key": token}
566
+ )
567
+ response.raise_for_status()
568
+
569
+ return response.json()
570
+
571
+
572
+ cli.add_command(perimeter, name="perimeter")
@@ -0,0 +1,111 @@
1
+ import os
2
+ from outerbounds._vendor import click
3
+ import requests
4
+
5
+ import tarfile
6
+ import hashlib
7
+ import tempfile
8
+
9
+
10
+ @click.group()
11
+ def cli(**kwargs):
12
+ pass
13
+
14
+
15
+ @click.group(help="Manage tutorials curated by Outerbounds.", hidden=True)
16
+ def tutorials(**kwargs):
17
+ pass
18
+
19
+
20
+ @tutorials.command(help="Pull Outerbounds tutorials.")
21
+ @click.option(
22
+ "--url",
23
+ required=True,
24
+ help="URL to pull the tutorials from.",
25
+ type=str,
26
+ )
27
+ @click.option(
28
+ "--destination-dir",
29
+ help="Show output in the specified format.",
30
+ type=str,
31
+ required=True,
32
+ )
33
+ @click.option(
34
+ "--force-overwrite",
35
+ is_flag=True,
36
+ help="Overwrite all existing files across all tutorials.",
37
+ type=bool,
38
+ required=False,
39
+ default=False,
40
+ )
41
+ def pull(url="", destination_dir="", force_overwrite=False):
42
+ try:
43
+ secure_download_and_extract(
44
+ url, destination_dir, force_overwrite=force_overwrite
45
+ )
46
+ click.secho("Tutorials pulled successfully.", fg="green", err=True)
47
+ except Exception as e:
48
+ print(e)
49
+ click.secho(f"Failed to pull tutorials: {e}", fg="red", err=True)
50
+
51
+
52
+ def secure_download_and_extract(
53
+ url, dest_dir, expected_hash=None, force_overwrite=False
54
+ ):
55
+ """
56
+ Download a tar.gz file from a URL, verify its integrity, and extract its contents.
57
+
58
+ :param url: URL of the tar.gz file to download
59
+ :param dest_dir: Destination directory to extract the contents
60
+ :param expected_hash: Expected SHA256 hash of the file (optional)
61
+ """
62
+
63
+ with tempfile.TemporaryDirectory() as temp_dir:
64
+ temp_file = os.path.join(
65
+ temp_dir, hashlib.md5(url.encode()).hexdigest() + ".tar.gz"
66
+ )
67
+
68
+ # Download the file
69
+ try:
70
+ response = requests.get(url, stream=True, verify=True)
71
+ response.raise_for_status()
72
+
73
+ with open(temp_file, "wb") as f:
74
+ for chunk in response.iter_content(chunk_size=8192):
75
+ f.write(chunk)
76
+ except requests.exceptions.RequestException as e:
77
+ raise Exception(f"Failed to download file: {e}")
78
+
79
+ if expected_hash:
80
+ with open(temp_file, "rb") as f:
81
+ file_hash = hashlib.sha256(f.read()).hexdigest()
82
+ if file_hash != expected_hash:
83
+ raise Exception("File integrity check failed")
84
+
85
+ os.makedirs(dest_dir, exist_ok=True)
86
+
87
+ try:
88
+ with tarfile.open(temp_file, "r:gz") as tar:
89
+ # Keep track of new journeys to extract.
90
+ to_extract = []
91
+ members = tar.getmembers()
92
+ for member in members:
93
+ member_path = os.path.join(dest_dir, member.name)
94
+ # Check for any files trying to write outside the destination
95
+ if not os.path.abspath(member_path).startswith(
96
+ os.path.abspath(dest_dir)
97
+ ):
98
+ raise Exception("Attempted path traversal in tar file")
99
+ if not os.path.exists(member_path):
100
+ # The user might have modified the existing files, leave them untouched.
101
+ to_extract.append(member)
102
+
103
+ if force_overwrite:
104
+ tar.extractall(path=dest_dir)
105
+ else:
106
+ tar.extractall(path=dest_dir, members=to_extract)
107
+ except tarfile.TarError as e:
108
+ raise Exception(f"Failed to extract tar file: {e}")
109
+
110
+
111
+ cli.add_command(tutorials, name="tutorials")
@@ -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,8 +512,85 @@ 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):
523
519
  return os.path.normpath(path).replace(os.sep, "\\")
520
+
521
+
522
+ @cli.command(help="Show relevant links for a deployment & perimeter", hidden=True)
523
+ @click.option(
524
+ "-d",
525
+ "--config-dir",
526
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
527
+ help="Path to Metaflow configuration directory",
528
+ show_default=True,
529
+ )
530
+ @click.option(
531
+ "-p",
532
+ "--profile",
533
+ default="",
534
+ help="The named metaflow profile in which your workstation exists",
535
+ )
536
+ @click.option(
537
+ "--perimeter-id",
538
+ default="",
539
+ help="The id of the perimeter to use",
540
+ )
541
+ @click.option(
542
+ "-o",
543
+ "--output",
544
+ default="",
545
+ help="Show output in the specified format.",
546
+ type=click.Choice(["json", ""]),
547
+ )
548
+ def show_relevant_links(config_dir=None, profile=None, perimeter_id="", output=""):
549
+ show_links_response = OuterboundsCommandResponse()
550
+ show_links_step = CommandStatus(
551
+ "showRelevantLinks",
552
+ OuterboundsCommandStatus.OK,
553
+ "Relevant links successfully fetched!",
554
+ )
555
+ show_links_response.add_or_update_data("links", [])
556
+ links = []
557
+ try:
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
+
576
+ links.append(
577
+ {
578
+ "id": "metaflow-ui-url",
579
+ "url": metaflow_config["METAFLOW_UI_URL"],
580
+ "label": "Metaflow UI URL",
581
+ }
582
+ )
583
+ show_links_response.add_or_update_data("links", links)
584
+ if output == "json":
585
+ click.echo(json.dumps(show_links_response.as_dict(), indent=4))
586
+ except Exception as e:
587
+ show_links_step.update(
588
+ OuterboundsCommandStatus.FAIL, "Failed to show relevant links", ""
589
+ )
590
+ show_links_response.add_step(show_links_step)
591
+ if output == "json":
592
+ show_links_response.add_or_update_data("error", str(e))
593
+ click.echo(json.dumps(show_links_response.as_dict(), indent=4))
594
+ else:
595
+ click.secho("Failed to show relevant links", fg="red", err=True)
596
+ click.secho("Error: {}".format(str(e)), fg="red", err=True)
@@ -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