outerbounds 0.3.92__py3-none-any.whl → 0.3.94__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 +120 -33
- outerbounds/command_groups/cli.py +2 -4
- outerbounds/command_groups/tutorials_cli.py +111 -0
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.94.dist-info}/METADATA +1 -1
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.94.dist-info}/RECORD +7 -6
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.94.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.92.dist-info → outerbounds-0.3.94.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
|
|
@@ -56,6 +56,21 @@ def app(**kwargs):
|
|
56
56
|
type=click.Choice(["json", ""]),
|
57
57
|
)
|
58
58
|
def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
59
|
+
if len(name) == 0 or len(name) >= 20:
|
60
|
+
click.secho(
|
61
|
+
"App name should not be more than 20 characters long.",
|
62
|
+
fg="red",
|
63
|
+
err=True,
|
64
|
+
)
|
65
|
+
return
|
66
|
+
elif not name.isalnum() or not name.islower():
|
67
|
+
click.secho(
|
68
|
+
"App name can only contain lowercase alphanumeric characters.",
|
69
|
+
fg="red",
|
70
|
+
err=True,
|
71
|
+
)
|
72
|
+
return
|
73
|
+
|
59
74
|
start_app_response = OuterboundsCommandResponse()
|
60
75
|
|
61
76
|
validate_workstation_step = CommandStatus(
|
@@ -70,10 +85,10 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
70
85
|
"List of workstations fetched.",
|
71
86
|
)
|
72
87
|
|
73
|
-
|
74
|
-
"
|
88
|
+
validate_request = CommandStatus(
|
89
|
+
"ValidateRequest",
|
75
90
|
OuterboundsCommandStatus.OK,
|
76
|
-
"
|
91
|
+
"Start app request is valid.",
|
77
92
|
)
|
78
93
|
|
79
94
|
start_app_step = CommandStatus(
|
@@ -127,12 +142,36 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
127
142
|
for workstation in workstations_json:
|
128
143
|
if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
|
129
144
|
if "named_ports" in workstation["spec"]:
|
145
|
+
try:
|
146
|
+
ensure_app_start_request_is_valid(
|
147
|
+
workstation["spec"]["named_ports"], port, name
|
148
|
+
)
|
149
|
+
except ValueError as e:
|
150
|
+
click.secho(str(e), fg="red", err=True)
|
151
|
+
validate_request.update(
|
152
|
+
OuterboundsCommandStatus.FAIL,
|
153
|
+
str(e),
|
154
|
+
"",
|
155
|
+
)
|
156
|
+
start_app_response.add_step(validate_request)
|
157
|
+
if output == "json":
|
158
|
+
click.echo(
|
159
|
+
json.dumps(start_app_response.as_dict(), indent=4)
|
160
|
+
)
|
161
|
+
return
|
162
|
+
|
163
|
+
start_app_response.add_step(validate_request)
|
164
|
+
|
130
165
|
for named_port in workstation["spec"]["named_ports"]:
|
131
166
|
if int(named_port["port"]) == port:
|
132
|
-
|
133
|
-
|
167
|
+
if named_port["enabled"]:
|
168
|
+
already_running_message = (
|
169
|
+
f"App {name} already running on port {port}!"
|
170
|
+
if named_port["name"] == name
|
171
|
+
else f"App {named_port['name']} already running on port {port} is now renamed to {name}!"
|
172
|
+
)
|
134
173
|
click.secho(
|
135
|
-
|
174
|
+
already_running_message,
|
136
175
|
fg="green",
|
137
176
|
err=True,
|
138
177
|
)
|
@@ -167,7 +206,12 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
167
206
|
fg="green",
|
168
207
|
err=True,
|
169
208
|
)
|
170
|
-
|
209
|
+
click.secho(
|
210
|
+
f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{name}/",
|
211
|
+
fg="green",
|
212
|
+
err=True,
|
213
|
+
)
|
214
|
+
except Exception:
|
171
215
|
click.secho(
|
172
216
|
f"Failed to start app {name} on port {port}!",
|
173
217
|
fg="red",
|
@@ -187,20 +231,6 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
187
231
|
)
|
188
232
|
)
|
189
233
|
return
|
190
|
-
|
191
|
-
click.secho(
|
192
|
-
f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
|
193
|
-
fg="red",
|
194
|
-
err=True,
|
195
|
-
)
|
196
|
-
validate_port_exists.update(
|
197
|
-
OuterboundsCommandStatus.FAIL,
|
198
|
-
f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
|
199
|
-
"",
|
200
|
-
)
|
201
|
-
start_app_response.add_step(validate_port_exists)
|
202
|
-
if output == "json":
|
203
|
-
click.echo(json.dumps(start_app_response.as_dict(), indent=4))
|
204
234
|
except Exception as e:
|
205
235
|
click.secho(f"Failed to start app {name} on port {port}!", fg="red", err=True)
|
206
236
|
start_app_step.update(
|
@@ -229,10 +259,18 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
229
259
|
)
|
230
260
|
@click.option(
|
231
261
|
"--port",
|
232
|
-
required=
|
262
|
+
required=False,
|
263
|
+
default=-1,
|
233
264
|
help="Port number where you want to start your app.",
|
234
265
|
type=int,
|
235
266
|
)
|
267
|
+
@click.option(
|
268
|
+
"--name",
|
269
|
+
required=False,
|
270
|
+
help="Name of your app",
|
271
|
+
default="",
|
272
|
+
type=str,
|
273
|
+
)
|
236
274
|
@click.option(
|
237
275
|
"-o",
|
238
276
|
"--output",
|
@@ -240,7 +278,22 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
|
|
240
278
|
help="Show output in the specified format.",
|
241
279
|
type=click.Choice(["json", ""]),
|
242
280
|
)
|
243
|
-
def stop(config_dir=None, profile=None, port=-1, output=""):
|
281
|
+
def stop(config_dir=None, profile=None, port=-1, name="", output=""):
|
282
|
+
if port == -1 and not name:
|
283
|
+
click.secho(
|
284
|
+
"Please provide either a port number or a name to stop the app.",
|
285
|
+
fg="red",
|
286
|
+
err=True,
|
287
|
+
)
|
288
|
+
return
|
289
|
+
elif port > 0 and name:
|
290
|
+
click.secho(
|
291
|
+
"Please provide either a port number or a name to stop the app, not both.",
|
292
|
+
fg="red",
|
293
|
+
err=True,
|
294
|
+
)
|
295
|
+
return
|
296
|
+
|
244
297
|
stop_app_response = OuterboundsCommandResponse()
|
245
298
|
|
246
299
|
validate_workstation_step = CommandStatus(
|
@@ -313,11 +366,21 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
|
|
313
366
|
if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
|
314
367
|
if "named_ports" in workstation["spec"]:
|
315
368
|
for named_port in workstation["spec"]["named_ports"]:
|
316
|
-
if
|
369
|
+
if (
|
370
|
+
int(named_port["port"]) == port
|
371
|
+
or named_port["name"] == name
|
372
|
+
):
|
317
373
|
stop_app_response.add_step(validate_port_exists)
|
318
374
|
if not named_port["enabled"]:
|
375
|
+
already_stopped_message = (
|
376
|
+
f"No deployed app named {named_port['name']} found."
|
377
|
+
if name
|
378
|
+
else f"There is no app deployed on port {port}"
|
379
|
+
)
|
319
380
|
click.secho(
|
320
|
-
|
381
|
+
already_stopped_message,
|
382
|
+
fg="green",
|
383
|
+
err=True,
|
321
384
|
)
|
322
385
|
stop_app_response.add_step(stop_app_step)
|
323
386
|
if output == "json":
|
@@ -333,26 +396,26 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
|
|
333
396
|
f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
|
334
397
|
headers={"x-api-key": metaflow_token},
|
335
398
|
json={
|
336
|
-
"port": port,
|
399
|
+
"port": named_port["port"],
|
337
400
|
"name": named_port["name"],
|
338
401
|
"enabled": False,
|
339
402
|
},
|
340
403
|
)
|
341
404
|
response.raise_for_status()
|
342
405
|
click.secho(
|
343
|
-
f"App stopped on port {port}!",
|
406
|
+
f"App {named_port['name']} stopped on port {named_port['port']}!",
|
344
407
|
fg="green",
|
345
408
|
err=True,
|
346
409
|
)
|
347
|
-
except:
|
410
|
+
except Exception as e:
|
348
411
|
click.secho(
|
349
|
-
f"Failed to stop app on port {port}!",
|
412
|
+
f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
|
350
413
|
fg="red",
|
351
414
|
err=True,
|
352
415
|
)
|
353
416
|
stop_app_step.update(
|
354
417
|
OuterboundsCommandStatus.FAIL,
|
355
|
-
f"Failed to stop app on port {port}!",
|
418
|
+
f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
|
356
419
|
"",
|
357
420
|
)
|
358
421
|
|
@@ -365,14 +428,21 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
|
|
365
428
|
)
|
366
429
|
return
|
367
430
|
|
431
|
+
err_message = (
|
432
|
+
(f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}")
|
433
|
+
if port != -1
|
434
|
+
else f"App {name} not found on workstation {os.environ['WORKSTATION_ID']}"
|
435
|
+
)
|
436
|
+
|
368
437
|
click.secho(
|
369
|
-
|
438
|
+
err_message,
|
370
439
|
fg="red",
|
371
440
|
err=True,
|
372
441
|
)
|
442
|
+
|
373
443
|
validate_port_exists.update(
|
374
444
|
OuterboundsCommandStatus.FAIL,
|
375
|
-
|
445
|
+
err_message,
|
376
446
|
"",
|
377
447
|
)
|
378
448
|
stop_app_response.add_step(validate_port_exists)
|
@@ -502,4 +572,21 @@ def list(config_dir=None, profile=None, output=""):
|
|
502
572
|
click.echo(json.dumps(list_app_response.as_dict(), indent=4))
|
503
573
|
|
504
574
|
|
575
|
+
def ensure_app_start_request_is_valid(existing_named_ports, port: int, name: str):
|
576
|
+
existing_apps_by_port = {np["port"]: np for np in existing_named_ports}
|
577
|
+
|
578
|
+
if port not in existing_apps_by_port:
|
579
|
+
raise ValueError(f"Port {port} not found on workstation")
|
580
|
+
|
581
|
+
for existing_named_port in existing_named_ports:
|
582
|
+
if (
|
583
|
+
name == existing_named_port["name"]
|
584
|
+
and existing_named_port["port"] != port
|
585
|
+
and existing_named_port["enabled"]
|
586
|
+
):
|
587
|
+
raise ValueError(
|
588
|
+
f"App with name '{name}' is already deployed on port {existing_named_port['port']}"
|
589
|
+
)
|
590
|
+
|
591
|
+
|
505
592
|
cli.add_command(app, name="app")
|
@@ -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):
|
@@ -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=
|
44
|
+
outerbounds/command_groups/apps_cli.py,sha256=sgRPYMdL_c7UUcFqy1pTQno0Q2aT1LHT7POB_VUuKoE,22612
|
45
|
+
outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
|
46
46
|
outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
|
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.94.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
|
57
|
+
outerbounds-0.3.94.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
58
|
+
outerbounds-0.3.94.dist-info/METADATA,sha256=ipH_wW6z5ITcDg1vBorq7clnY8ajMt-WMBgOcfNbxyA,1632
|
59
|
+
outerbounds-0.3.94.dist-info/RECORD,,
|
File without changes
|
File without changes
|