outerbounds 0.3.93__py3-none-any.whl → 0.3.94__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- validate_port_exists = CommandStatus(
74
- "ValidatePortExists",
88
+ validate_request = CommandStatus(
89
+ "ValidateRequest",
75
90
  OuterboundsCommandStatus.OK,
76
- "Port exists on workstation",
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
- start_app_response.add_step(validate_port_exists)
133
- if named_port["enabled"] and named_port["name"] == name:
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
- f"App {name} started on port {port}!",
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
- except:
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(
@@ -256,6 +286,13 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
256
286
  err=True,
257
287
  )
258
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
259
296
 
260
297
  stop_app_response = OuterboundsCommandResponse()
261
298
 
@@ -335,8 +372,13 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
335
372
  ):
336
373
  stop_app_response.add_step(validate_port_exists)
337
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
+ )
338
380
  click.secho(
339
- f"App {named_port['name']} stopped on port {port}!",
381
+ already_stopped_message,
340
382
  fg="green",
341
383
  err=True,
342
384
  )
@@ -354,26 +396,26 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
354
396
  f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
355
397
  headers={"x-api-key": metaflow_token},
356
398
  json={
357
- "port": port,
399
+ "port": named_port["port"],
358
400
  "name": named_port["name"],
359
401
  "enabled": False,
360
402
  },
361
403
  )
362
404
  response.raise_for_status()
363
405
  click.secho(
364
- f"App stopped on port {port}!",
406
+ f"App {named_port['name']} stopped on port {named_port['port']}!",
365
407
  fg="green",
366
408
  err=True,
367
409
  )
368
- except:
410
+ except Exception as e:
369
411
  click.secho(
370
- f"Failed to stop app on port {port}!",
412
+ f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
371
413
  fg="red",
372
414
  err=True,
373
415
  )
374
416
  stop_app_step.update(
375
417
  OuterboundsCommandStatus.FAIL,
376
- f"Failed to stop app on port {port}!",
418
+ f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
377
419
  "",
378
420
  )
379
421
 
@@ -387,12 +429,10 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
387
429
  return
388
430
 
389
431
  err_message = (
390
- f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}"
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']}"
391
435
  )
392
- if port == -1:
393
- err_message = (
394
- f"App {name} not found on workstation {os.environ['WORKSTATION_ID']}"
395
- )
396
436
 
397
437
  click.secho(
398
438
  err_message,
@@ -532,4 +572,21 @@ def list(config_dir=None, profile=None, output=""):
532
572
  click.echo(json.dumps(list_app_response.as_dict(), indent=4))
533
573
 
534
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
+
535
592
  cli.add_command(app, name="app")
@@ -5,7 +5,6 @@ import os
5
5
  import re
6
6
  import subprocess
7
7
  import sys
8
- import time
9
8
  import zlib
10
9
  from base64 import b64decode, b64encode
11
10
  from importlib.machinery import PathFinder
@@ -16,10 +15,6 @@ from outerbounds._vendor import click
16
15
  import requests
17
16
  from requests.exceptions import HTTPError
18
17
 
19
- from google.oauth2 import service_account
20
- import google.auth
21
- import google.auth.jwt
22
-
23
18
  from ..utils import kubeconfig, metaflowconfig
24
19
  from ..utils.schema import (
25
20
  CommandStatus,
@@ -767,48 +762,6 @@ def get_gha_jwt(audience: str):
767
762
  sys.exit(1)
768
763
 
769
764
 
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
-
812
765
  def get_origin_token(
813
766
  service_principal_name: str,
814
767
  deployment: str,
@@ -979,17 +932,6 @@ def configure(
979
932
  is_flag=True,
980
933
  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.",
981
934
  )
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
- )
993
935
  @click.option(
994
936
  "-d",
995
937
  "--config-dir",
@@ -1021,31 +963,18 @@ def service_principal_configure(
1021
963
  perimeter: str,
1022
964
  jwt_token="",
1023
965
  github_actions=False,
1024
- gcp=False,
1025
- gcp_service_account_toke_file_path="",
1026
966
  config_dir=None,
1027
967
  profile=None,
1028
968
  echo=None,
1029
969
  force=False,
1030
970
  ):
1031
971
  audience = f"https://{deployment_domain}"
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)
972
+ if jwt_token == "" and github_actions:
973
+ jwt_token = get_gha_jwt(audience)
1040
974
 
1041
975
  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:
1047
976
  click.secho(
1048
- "No JWT token provided. Please provider either a valid jwt token or set --github-actions or --gcp flag.",
977
+ "No JWT token provided. Please provider either a valid jwt token or set --github-actions",
1049
978
  fg="red",
1050
979
  )
1051
980
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.93
3
+ Version: 0.3.94
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -41,9 +41,9 @@ 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=k3zsBh7GupBqM5sbEU8OHSC1TBVdTBJybvPX4DX5Ack,20052
44
+ outerbounds/command_groups/apps_cli.py,sha256=sgRPYMdL_c7UUcFqy1pTQno0Q2aT1LHT7POB_VUuKoE,22612
45
45
  outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
46
- outerbounds/command_groups/local_setup_cli.py,sha256=AgK4fe-q1uRm20OsMGFGDezheGOPMBoESC98JT9HzqQ,39046
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
48
  outerbounds/command_groups/tutorials_cli.py,sha256=UInFyiMqtscHFfi8YQwiY_6Sdw9quJOtRu5OukEBccw,3522
49
49
  outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
@@ -53,7 +53,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
53
53
  outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
54
54
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
55
55
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
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,,
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,,