outerbounds 0.3.55rc3__py3-none-any.whl → 0.3.133__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 (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