outerbounds 0.3.92__py3-none-any.whl → 0.3.93__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/command_groups/apps_cli.py +37 -7
- outerbounds/command_groups/cli.py +2 -4
- outerbounds/command_groups/local_setup_cli.py +74 -3
- outerbounds/command_groups/tutorials_cli.py +111 -0
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.93.dist-info}/METADATA +1 -1
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.93.dist-info}/RECORD +8 -7
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.93.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.93.dist-info}/entry_points.txt +0 -0
@@ -17,7 +17,7 @@ def cli(**kwargs):
|
|
17
17
|
pass
|
18
18
|
|
19
19
|
|
20
|
-
@click.group(help="Manage
|
20
|
+
@click.group(help="Manage apps")
|
21
21
|
def app(**kwargs):
|
22
22
|
pass
|
23
23
|
|
@@ -229,10 +229,18 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
229
229
|
)
|
230
230
|
@click.option(
|
231
231
|
"--port",
|
232
|
-
required=
|
232
|
+
required=False,
|
233
|
+
default=-1,
|
233
234
|
help="Port number where you want to start your app.",
|
234
235
|
type=int,
|
235
236
|
)
|
237
|
+
@click.option(
|
238
|
+
"--name",
|
239
|
+
required=False,
|
240
|
+
help="Name of your app",
|
241
|
+
default="",
|
242
|
+
type=str,
|
243
|
+
)
|
236
244
|
@click.option(
|
237
245
|
"-o",
|
238
246
|
"--output",
|
@@ -240,7 +248,15 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
240
248
|
help="Show output in the specified format.",
|
241
249
|
type=click.Choice(["json", ""]),
|
242
250
|
)
|
243
|
-
def stop(config_dir=None, profile=None, port=-1, output=""):
|
251
|
+
def stop(config_dir=None, profile=None, port=-1, name="", output=""):
|
252
|
+
if port == -1 and not name:
|
253
|
+
click.secho(
|
254
|
+
"Please provide either a port number or a name to stop the app.",
|
255
|
+
fg="red",
|
256
|
+
err=True,
|
257
|
+
)
|
258
|
+
return
|
259
|
+
|
244
260
|
stop_app_response = OuterboundsCommandResponse()
|
245
261
|
|
246
262
|
validate_workstation_step = CommandStatus(
|
@@ -313,11 +329,16 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
|
|
313
329
|
if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
|
314
330
|
if "named_ports" in workstation["spec"]:
|
315
331
|
for named_port in workstation["spec"]["named_ports"]:
|
316
|
-
if
|
332
|
+
if (
|
333
|
+
int(named_port["port"]) == port
|
334
|
+
or named_port["name"] == name
|
335
|
+
):
|
317
336
|
stop_app_response.add_step(validate_port_exists)
|
318
337
|
if not named_port["enabled"]:
|
319
338
|
click.secho(
|
320
|
-
f"App stopped on port {port}!",
|
339
|
+
f"App {named_port['name']} stopped on port {port}!",
|
340
|
+
fg="green",
|
341
|
+
err=True,
|
321
342
|
)
|
322
343
|
stop_app_response.add_step(stop_app_step)
|
323
344
|
if output == "json":
|
@@ -365,14 +386,23 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
|
|
365
386
|
)
|
366
387
|
return
|
367
388
|
|
389
|
+
err_message = (
|
390
|
+
f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}"
|
391
|
+
)
|
392
|
+
if port == -1:
|
393
|
+
err_message = (
|
394
|
+
f"App {name} not found on workstation {os.environ['WORKSTATION_ID']}"
|
395
|
+
)
|
396
|
+
|
368
397
|
click.secho(
|
369
|
-
|
398
|
+
err_message,
|
370
399
|
fg="red",
|
371
400
|
err=True,
|
372
401
|
)
|
402
|
+
|
373
403
|
validate_port_exists.update(
|
374
404
|
OuterboundsCommandStatus.FAIL,
|
375
|
-
|
405
|
+
err_message,
|
376
406
|
"",
|
377
407
|
)
|
378
408
|
stop_app_response.add_step(validate_port_exists)
|
@@ -1,8 +1,5 @@
|
|
1
1
|
from outerbounds._vendor import click
|
2
|
-
from . import local_setup_cli
|
3
|
-
from . import workstations_cli
|
4
|
-
from . import perimeters_cli
|
5
|
-
from . import apps_cli
|
2
|
+
from . import local_setup_cli, workstations_cli, perimeters_cli, apps_cli, tutorials_cli
|
6
3
|
|
7
4
|
|
8
5
|
@click.command(
|
@@ -12,6 +9,7 @@ from . import apps_cli
|
|
12
9
|
workstations_cli.cli,
|
13
10
|
perimeters_cli.cli,
|
14
11
|
apps_cli.cli,
|
12
|
+
tutorials_cli.cli,
|
15
13
|
],
|
16
14
|
)
|
17
15
|
def cli(**kwargs):
|
@@ -5,6 +5,7 @@ import os
|
|
5
5
|
import re
|
6
6
|
import subprocess
|
7
7
|
import sys
|
8
|
+
import time
|
8
9
|
import zlib
|
9
10
|
from base64 import b64decode, b64encode
|
10
11
|
from importlib.machinery import PathFinder
|
@@ -15,6 +16,10 @@ from outerbounds._vendor import click
|
|
15
16
|
import requests
|
16
17
|
from requests.exceptions import HTTPError
|
17
18
|
|
19
|
+
from google.oauth2 import service_account
|
20
|
+
import google.auth
|
21
|
+
import google.auth.jwt
|
22
|
+
|
18
23
|
from ..utils import kubeconfig, metaflowconfig
|
19
24
|
from ..utils.schema import (
|
20
25
|
CommandStatus,
|
@@ -762,6 +767,48 @@ def get_gha_jwt(audience: str):
|
|
762
767
|
sys.exit(1)
|
763
768
|
|
764
769
|
|
770
|
+
def get_gcp_jwt(audience: str, service_account_token_file_path: str = ""):
|
771
|
+
try:
|
772
|
+
if service_account_token_file_path != "":
|
773
|
+
credentials = service_account.Credentials.from_service_account_file(
|
774
|
+
service_account_token_file_path
|
775
|
+
)
|
776
|
+
else:
|
777
|
+
credentials, project = google.auth.default()
|
778
|
+
except Exception as e:
|
779
|
+
click.secho(
|
780
|
+
f"Failed to get Google Cloud service account credentials. Error: {str(e)}",
|
781
|
+
fg="red",
|
782
|
+
)
|
783
|
+
sys.exit(1)
|
784
|
+
|
785
|
+
# Ensure the credentials are service account credentials to sign the JWT
|
786
|
+
if isinstance(credentials, service_account.Credentials):
|
787
|
+
try:
|
788
|
+
payload = {
|
789
|
+
"iat": int(time.time()), # Issued at time
|
790
|
+
"exp": int(time.time()) + 172800, # 48 hours expiration time
|
791
|
+
"sub": credentials.service_account_email,
|
792
|
+
"aud": audience,
|
793
|
+
"sa_email": credentials.service_account_email,
|
794
|
+
}
|
795
|
+
|
796
|
+
signed_jwt = google.auth.jwt.encode(credentials.signer, payload)
|
797
|
+
return signed_jwt.decode("utf-8")
|
798
|
+
except Exception as e:
|
799
|
+
click.secho(
|
800
|
+
f"Failed to sign JWT token using Google Cloud service account credentials. Error: {str(e)}",
|
801
|
+
fg="red",
|
802
|
+
)
|
803
|
+
sys.exit(1)
|
804
|
+
|
805
|
+
click.secho(
|
806
|
+
"The provided credentials are not service account credentials. Please provide a valid service account credentials file or set valid service account credentials in the environment using GOOGLE_APPLICATION_CREDENTIALS.",
|
807
|
+
fg="red",
|
808
|
+
)
|
809
|
+
sys.exit(1)
|
810
|
+
|
811
|
+
|
765
812
|
def get_origin_token(
|
766
813
|
service_principal_name: str,
|
767
814
|
deployment: str,
|
@@ -932,6 +979,17 @@ def configure(
|
|
932
979
|
is_flag=True,
|
933
980
|
help="Set if the command is being run in a GitHub Actions environment. If both --jwt-token and --github-actions are specified the --github-actions flag will be ignored.",
|
934
981
|
)
|
982
|
+
@click.option(
|
983
|
+
"--gcp",
|
984
|
+
is_flag=True,
|
985
|
+
help="Set if the command is being run which a Google Cloud service account credential. If both --jwt-token and --gcp are specified the -gcp flag will be ignored.",
|
986
|
+
)
|
987
|
+
@click.option(
|
988
|
+
"--gcp-service-account-toke-file-path",
|
989
|
+
default="",
|
990
|
+
help="The full path to the Google Cloud service account token file. If --gcp is set and this value is not specified, the default GOOGLE_APPLICATION_CREDENTIALS environment variable will be used.",
|
991
|
+
required=True,
|
992
|
+
)
|
935
993
|
@click.option(
|
936
994
|
"-d",
|
937
995
|
"--config-dir",
|
@@ -963,18 +1021,31 @@ def service_principal_configure(
|
|
963
1021
|
perimeter: str,
|
964
1022
|
jwt_token="",
|
965
1023
|
github_actions=False,
|
1024
|
+
gcp=False,
|
1025
|
+
gcp_service_account_toke_file_path="",
|
966
1026
|
config_dir=None,
|
967
1027
|
profile=None,
|
968
1028
|
echo=None,
|
969
1029
|
force=False,
|
970
1030
|
):
|
971
1031
|
audience = f"https://{deployment_domain}"
|
972
|
-
|
973
|
-
|
1032
|
+
|
1033
|
+
# ensure only one of github_actions or gcp is set
|
1034
|
+
if github_actions and gcp:
|
1035
|
+
click.secho(
|
1036
|
+
"Both --github-actions and --gcp flags cannot be set at the same time.",
|
1037
|
+
fg="red",
|
1038
|
+
)
|
1039
|
+
sys.exit(1)
|
974
1040
|
|
975
1041
|
if jwt_token == "":
|
1042
|
+
if github_actions:
|
1043
|
+
jwt_token = get_gha_jwt(audience)
|
1044
|
+
elif gcp:
|
1045
|
+
jwt_token = get_gcp_jwt(audience, gcp_service_account_toke_file_path)
|
1046
|
+
else:
|
976
1047
|
click.secho(
|
977
|
-
"No JWT token provided. Please provider either a valid jwt token or set --github-actions",
|
1048
|
+
"No JWT token provided. Please provider either a valid jwt token or set --github-actions or --gcp flag.",
|
978
1049
|
fg="red",
|
979
1050
|
)
|
980
1051
|
sys.exit(1)
|
@@ -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")
|
@@ -41,10 +41,11 @@ outerbounds/_vendor/yaml/serializer.py,sha256=8wFZRy9SsQSktF_f9OOroroqsh4qVUe53r
|
|
41
41
|
outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7KwPmA,2578
|
42
42
|
outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
|
43
43
|
outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
|
44
|
-
outerbounds/command_groups/apps_cli.py,sha256=
|
45
|
-
outerbounds/command_groups/cli.py,sha256=
|
46
|
-
outerbounds/command_groups/local_setup_cli.py,sha256=
|
44
|
+
outerbounds/command_groups/apps_cli.py,sha256=k3zsBh7GupBqM5sbEU8OHSC1TBVdTBJybvPX4DX5Ack,20052
|
45
|
+
outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
|
46
|
+
outerbounds/command_groups/local_setup_cli.py,sha256=AgK4fe-q1uRm20OsMGFGDezheGOPMBoESC98JT9HzqQ,39046
|
47
47
|
outerbounds/command_groups/perimeters_cli.py,sha256=mrJfFIRYFOjuiz-9h4OKg2JT8Utmbs72z6wvPzDss3s,18685
|
48
|
+
outerbounds/command_groups/tutorials_cli.py,sha256=UInFyiMqtscHFfi8YQwiY_6Sdw9quJOtRu5OukEBccw,3522
|
48
49
|
outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
|
49
50
|
outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
51
|
outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
|
@@ -52,7 +53,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
|
|
52
53
|
outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
|
53
54
|
outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
|
54
55
|
outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
|
55
|
-
outerbounds-0.3.
|
56
|
-
outerbounds-0.3.
|
57
|
-
outerbounds-0.3.
|
58
|
-
outerbounds-0.3.
|
56
|
+
outerbounds-0.3.93.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
|
57
|
+
outerbounds-0.3.93.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
58
|
+
outerbounds-0.3.93.dist-info/METADATA,sha256=DbAq4c18up77upq4UVgOtzl1AL_cjHkdUC1SBHnx0os,1632
|
59
|
+
outerbounds-0.3.93.dist-info/RECORD,,
|
File without changes
|
File without changes
|