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.
- outerbounds/_vendor/PyYAML.LICENSE +20 -0
- outerbounds/_vendor/__init__.py +0 -0
- outerbounds/_vendor/_yaml/__init__.py +34 -0
- outerbounds/_vendor/click/__init__.py +73 -0
- outerbounds/_vendor/click/_compat.py +626 -0
- outerbounds/_vendor/click/_termui_impl.py +717 -0
- outerbounds/_vendor/click/_textwrap.py +49 -0
- outerbounds/_vendor/click/_winconsole.py +279 -0
- outerbounds/_vendor/click/core.py +2998 -0
- outerbounds/_vendor/click/decorators.py +497 -0
- outerbounds/_vendor/click/exceptions.py +287 -0
- outerbounds/_vendor/click/formatting.py +301 -0
- outerbounds/_vendor/click/globals.py +68 -0
- outerbounds/_vendor/click/parser.py +529 -0
- outerbounds/_vendor/click/py.typed +0 -0
- outerbounds/_vendor/click/shell_completion.py +580 -0
- outerbounds/_vendor/click/termui.py +787 -0
- outerbounds/_vendor/click/testing.py +479 -0
- outerbounds/_vendor/click/types.py +1073 -0
- outerbounds/_vendor/click/utils.py +580 -0
- outerbounds/_vendor/click.LICENSE +28 -0
- outerbounds/_vendor/vendor_any.txt +2 -0
- outerbounds/_vendor/yaml/__init__.py +471 -0
- outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
- outerbounds/_vendor/yaml/composer.py +146 -0
- outerbounds/_vendor/yaml/constructor.py +862 -0
- outerbounds/_vendor/yaml/cyaml.py +177 -0
- outerbounds/_vendor/yaml/dumper.py +138 -0
- outerbounds/_vendor/yaml/emitter.py +1239 -0
- outerbounds/_vendor/yaml/error.py +94 -0
- outerbounds/_vendor/yaml/events.py +104 -0
- outerbounds/_vendor/yaml/loader.py +62 -0
- outerbounds/_vendor/yaml/nodes.py +51 -0
- outerbounds/_vendor/yaml/parser.py +629 -0
- outerbounds/_vendor/yaml/reader.py +208 -0
- outerbounds/_vendor/yaml/representer.py +378 -0
- outerbounds/_vendor/yaml/resolver.py +245 -0
- outerbounds/_vendor/yaml/scanner.py +1555 -0
- outerbounds/_vendor/yaml/serializer.py +127 -0
- outerbounds/_vendor/yaml/tokens.py +129 -0
- outerbounds/command_groups/apps_cli.py +450 -0
- outerbounds/command_groups/cli.py +9 -5
- outerbounds/command_groups/local_setup_cli.py +249 -33
- outerbounds/command_groups/perimeters_cli.py +231 -33
- outerbounds/command_groups/tutorials_cli.py +111 -0
- outerbounds/command_groups/workstations_cli.py +88 -15
- outerbounds/utils/kubeconfig.py +2 -2
- outerbounds/utils/metaflowconfig.py +111 -21
- outerbounds/utils/schema.py +8 -2
- outerbounds/utils/utils.py +19 -0
- outerbounds/vendor.py +159 -0
- {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/METADATA +17 -6
- outerbounds-0.3.133.dist-info/RECORD +59 -0
- {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/WHEEL +1 -1
- outerbounds-0.3.55rc3.dist-info/RECORD +0 -15
- {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
|
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
|
13
|
-
from
|
14
|
-
|
15
|
-
import boto3
|
16
|
-
import click
|
6
|
+
from typing import Any, Dict
|
7
|
+
from outerbounds._vendor import click
|
17
8
|
import requests
|
18
|
-
|
9
|
+
import configparser
|
19
10
|
|
20
|
-
from ..utils import
|
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
|
-
@
|
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
|
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
|
-
|
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
|
-
@
|
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
|
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
|
-
@
|
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
|
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)
|
outerbounds/utils/kubeconfig.py
CHANGED