outerbounds 0.3.55rc4__py3-none-any.whl → 0.3.68__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.
@@ -12,7 +12,6 @@ from os import path
12
12
  from pathlib import Path
13
13
  from typing import Any, Callable, Dict, List
14
14
 
15
- import boto3
16
15
  import click
17
16
  import requests
18
17
  from requests.exceptions import HTTPError
@@ -31,7 +30,8 @@ the Outerbounds platform.
31
30
  To remove that package, please try `python -m pip uninstall metaflow -y` or reach out to Outerbounds support.
32
31
  After uninstalling the Metaflow package, please reinstall the Outerbounds package using `python -m pip
33
32
  install outerbounds --force`.
34
- As always, please reach out to Outerbounds support for any questions."""
33
+ As always, please reach out to Outerbounds support for any questions.
34
+ """
35
35
 
36
36
  MISSING_EXTENSIONS_MESSAGE = (
37
37
  "The Outerbounds Platform extensions for Metaflow was not found."
@@ -43,6 +43,8 @@ BAD_EXTENSION_MESSAGE = (
43
43
  "Mis-installation of the Outerbounds Platform extension package has been detected."
44
44
  )
45
45
 
46
+ PERIMETER_CONFIG_URL_KEY = "OB_CURRENT_PERIMETER_MF_CONFIG_URL"
47
+
46
48
 
47
49
  class Narrator:
48
50
  def __init__(self, verbose):
@@ -54,11 +56,11 @@ class Narrator:
54
56
 
55
57
  def section_ok(self):
56
58
  if not self.verbose:
57
- click.secho("\U0001F600", err=True)
59
+ click.secho("\U00002705", err=True)
58
60
 
59
61
  def section_not_ok(self):
60
62
  if not self.verbose:
61
- click.secho("\U0001F641", err=True)
63
+ click.secho("\U0000274C", err=True)
62
64
 
63
65
  def announce_check(self, name):
64
66
  if self.verbose:
@@ -232,25 +234,33 @@ class ConfigEntrySpec:
232
234
  self.expected = expected
233
235
 
234
236
 
235
- def get_config_specs():
236
- return [
237
- ConfigEntrySpec(
238
- "METAFLOW_DATASTORE_SYSROOT_S3", "s3://[a-z0-9\-]+/metaflow[/]?"
239
- ),
240
- ConfigEntrySpec("METAFLOW_DATATOOLS_S3ROOT", "s3://[a-z0-9\-]+/data[/]?"),
237
+ def get_config_specs(default_datastore: str):
238
+ spec = [
241
239
  ConfigEntrySpec("METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER", "obp", expected="obp"),
242
- ConfigEntrySpec("METAFLOW_DEFAULT_DATASTORE", "s3", expected="s3"),
243
240
  ConfigEntrySpec("METAFLOW_DEFAULT_METADATA", "service", expected="service"),
244
- ConfigEntrySpec(
245
- "METAFLOW_KUBERNETES_NAMESPACE", "jobs\-default", expected="jobs-default"
246
- ),
247
- ConfigEntrySpec("METAFLOW_KUBERNETES_SANDBOX_INIT_SCRIPT", "eval \$\(.*"),
248
- ConfigEntrySpec("METAFLOW_SERVICE_AUTH_KEY", "[a-zA-Z0-9!_\-\.]+"),
249
- ConfigEntrySpec("METAFLOW_SERVICE_URL", "https://metadata\..*"),
250
- ConfigEntrySpec("METAFLOW_UI_URL", "https://ui\..*"),
251
- ConfigEntrySpec("OBP_AUTH_SERVER", "auth\..*"),
241
+ ConfigEntrySpec("METAFLOW_KUBERNETES_NAMESPACE", r"jobs-.*"),
242
+ ConfigEntrySpec("METAFLOW_KUBERNETES_SANDBOX_INIT_SCRIPT", r"eval \$\(.*"),
243
+ ConfigEntrySpec("METAFLOW_SERVICE_AUTH_KEY", r"[a-zA-Z0-9!_\-\.]+"),
244
+ ConfigEntrySpec("METAFLOW_SERVICE_URL", r"https://metadata\..*"),
245
+ ConfigEntrySpec("METAFLOW_UI_URL", r"https://ui\..*"),
246
+ ConfigEntrySpec("OBP_AUTH_SERVER", r"auth\..*"),
252
247
  ]
253
248
 
249
+ if default_datastore == "s3":
250
+ spec.extend(
251
+ [
252
+ ConfigEntrySpec(
253
+ "METAFLOW_DATASTORE_SYSROOT_S3",
254
+ r"s3://[a-z0-9\-]+/metaflow(-[a-z0-9\-]+)?[/]?",
255
+ ),
256
+ ConfigEntrySpec(
257
+ "METAFLOW_DATATOOLS_S3ROOT",
258
+ r"s3://[a-z0-9\-]+/data(-[a-z0-9\-]+)?[/]?",
259
+ ),
260
+ ]
261
+ )
262
+ return spec
263
+
254
264
 
255
265
  def check_metaflow_config(narrator: Narrator) -> CommandStatus:
256
266
  narrator.announce_section("local Metaflow config")
@@ -261,8 +271,20 @@ def check_metaflow_config(narrator: Narrator) -> CommandStatus:
261
271
  mitigation="",
262
272
  )
263
273
 
264
- config = metaflowconfig.init_config()
265
- for spec in get_config_specs():
274
+ profile = os.environ.get("METAFLOW_PROFILE")
275
+ config_dir = os.path.expanduser(
276
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
277
+ )
278
+
279
+ config = metaflowconfig.init_config(config_dir, profile)
280
+
281
+ if "OBP_METAFLOW_CONFIG_URL" in config:
282
+ # If the config is fetched from a remote source, not much to check
283
+ narrator.announce_check("config entry OBP_METAFLOW_CONFIG_URL")
284
+ narrator.ok()
285
+ return check_status
286
+
287
+ for spec in get_config_specs(config.get("METAFLOW_DEFAULT_DATASTORE", "")):
266
288
  narrator.announce_check("config entry " + spec.name)
267
289
  if spec.name not in config:
268
290
  reason = "Missing"
@@ -304,7 +326,12 @@ def check_metaflow_token(narrator: Narrator) -> CommandStatus:
304
326
  mitigation="",
305
327
  )
306
328
 
307
- config = metaflowconfig.init_config()
329
+ profile = os.environ.get("METAFLOW_PROFILE")
330
+ config_dir = os.path.expanduser(
331
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
332
+ )
333
+
334
+ config = metaflowconfig.init_config(config_dir, profile)
308
335
  try:
309
336
  if "OBP_AUTH_SERVER" in config:
310
337
  k8s_response = requests.get(
@@ -363,7 +390,13 @@ def check_workstation_api_accessible(narrator: Narrator) -> CommandStatus:
363
390
  )
364
391
 
365
392
  try:
366
- config = metaflowconfig.init_config()
393
+ profile = os.environ.get("METAFLOW_PROFILE")
394
+ config_dir = os.path.expanduser(
395
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
396
+ )
397
+
398
+ config = metaflowconfig.init_config(config_dir, profile)
399
+
367
400
  missing_keys = []
368
401
  if "METAFLOW_SERVICE_AUTH_KEY" not in config:
369
402
  missing_keys.append("METAFLOW_SERVICE_AUTH_KEY")
@@ -422,7 +455,13 @@ def check_kubeconfig_valid_for_workstations(narrator: Narrator) -> CommandStatus
422
455
  )
423
456
 
424
457
  try:
425
- config = metaflowconfig.init_config()
458
+ profile = os.environ.get("METAFLOW_PROFILE")
459
+ config_dir = os.path.expanduser(
460
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
461
+ )
462
+
463
+ config = metaflowconfig.init_config(config_dir, profile)
464
+
426
465
  missing_keys = []
427
466
  if "METAFLOW_SERVICE_AUTH_KEY" not in config:
428
467
  missing_keys.append("METAFLOW_SERVICE_AUTH_KEY")
@@ -585,6 +624,7 @@ class ConfigurationWriter:
585
624
  self.decoded_config = None
586
625
  self.out_dir = out_dir
587
626
  self.profile = profile
627
+ self.selected_perimeter = None
588
628
 
589
629
  ob_config_dir = path.expanduser(os.getenv("OBP_CONFIG_DIR", out_dir))
590
630
  self.ob_config_path = path.join(
@@ -596,8 +636,12 @@ class ConfigurationWriter:
596
636
  self.decoded_config = deserialize(self.encoded_config)
597
637
 
598
638
  def process_decoded_config(self):
639
+ assert self.decoded_config is not None
599
640
  config_type = self.decoded_config.get("OB_CONFIG_TYPE", "inline")
600
641
  if config_type == "inline":
642
+ if "OBP_PERIMETER" in self.decoded_config:
643
+ self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
644
+
601
645
  if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
602
646
  self.decoded_config = {
603
647
  "OBP_METAFLOW_CONFIG_URL": self.decoded_config[
@@ -616,6 +660,8 @@ class ConfigurationWriter:
616
660
  f"{str(e)} key is required for aws-ref config type"
617
661
  )
618
662
  try:
663
+ import boto3
664
+
619
665
  client = boto3.client("secretsmanager", region_name=region)
620
666
  response = client.get_secret_value(SecretId=secret_arn)
621
667
  self.decoded_config = json.loads(response["SecretBinary"])
@@ -633,6 +679,7 @@ class ConfigurationWriter:
633
679
  return path.join(self.out_dir, "config_{}.json".format(self.profile))
634
680
 
635
681
  def display(self):
682
+ assert self.decoded_config is not None
636
683
  # Create a copy so we can use the real config later, possibly
637
684
  display_config = dict()
638
685
  for k in self.decoded_config.keys():
@@ -647,6 +694,7 @@ class ConfigurationWriter:
647
694
  return self.confirm_overwrite_config(self.path())
648
695
 
649
696
  def write_config(self):
697
+ assert self.decoded_config is not None
650
698
  config_path = self.path()
651
699
  # TODO config contains auth token - restrict file/dir modes
652
700
  os.makedirs(os.path.dirname(config_path), exist_ok=True)
@@ -656,11 +704,13 @@ class ConfigurationWriter:
656
704
 
657
705
  # Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
658
706
  remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
659
- if "OBP_PERIMETER" in remote_config and "OBP_PERIMETER_URL" in remote_config:
707
+ if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
660
708
  with open(self.ob_config_path, "w") as fd:
661
709
  ob_config_dict = {
662
- "OB_CURRENT_PERIMETER": remote_config["OBP_PERIMETER"],
663
- "OB_CURRENT_PERIMETER_URL": remote_config["OBP_PERIMETER_URL"],
710
+ "OB_CURRENT_PERIMETER": self.selected_perimeter,
711
+ PERIMETER_CONFIG_URL_KEY: self.decoded_config[
712
+ "OBP_METAFLOW_CONFIG_URL"
713
+ ],
664
714
  }
665
715
  json.dump(ob_config_dict, fd, indent=4)
666
716
 
@@ -686,6 +736,64 @@ class ConfigurationWriter:
686
736
  return True
687
737
 
688
738
 
739
+ def get_gha_jwt(audience: str):
740
+ # These are specific environment variables that are set by GitHub Actions.
741
+ if (
742
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN" in os.environ
743
+ and "ACTIONS_ID_TOKEN_REQUEST_URL" in os.environ
744
+ ):
745
+ try:
746
+ response = requests.get(
747
+ url=os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"],
748
+ headers={
749
+ "Authorization": f"Bearer {os.environ['ACTIONS_ID_TOKEN_REQUEST_TOKEN']}"
750
+ },
751
+ params={"audience": audience},
752
+ )
753
+ response.raise_for_status()
754
+ return response.json()["value"]
755
+ except Exception as e:
756
+ click.secho(
757
+ "Failed to fetch JWT token from GitHub Actions. Please make sure you are permission 'id-token: write' is set on the GHA jobs level.",
758
+ fg="red",
759
+ )
760
+ sys.exit(1)
761
+
762
+ click.secho(
763
+ "The --github-actions flag was set, but we didn't not find '$ACTIONS_ID_TOKEN_REQUEST_TOKEN' and '$ACTIONS_ID_TOKEN_REQUEST_URL' environment variables. Please make sure you are running this command in a GitHub Actions environment and with correct permissions as per https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers",
764
+ fg="red",
765
+ )
766
+ sys.exit(1)
767
+
768
+
769
+ def get_origin_token(
770
+ service_principal_name: str,
771
+ deployment: str,
772
+ perimeter: str,
773
+ token: str,
774
+ auth_server: str,
775
+ ):
776
+ try:
777
+ response = requests.get(
778
+ f"{auth_server}/generate/service-principal",
779
+ headers={"x-api-key": token},
780
+ data=json.dumps(
781
+ {
782
+ "servicePrincipalName": service_principal_name,
783
+ "deploymentName": deployment,
784
+ "perimeter": perimeter,
785
+ }
786
+ ),
787
+ )
788
+ response.raise_for_status()
789
+ return response.json()["token"]
790
+ except Exception as e:
791
+ click.secho(
792
+ f"Failed to get origin token from {auth_server}. Error: {str(e)}", fg="red"
793
+ )
794
+ sys.exit(1)
795
+
796
+
689
797
  @click.group(help="The Outerbounds Platform CLI", no_args_is_help=True)
690
798
  def cli(**kwargs):
691
799
  pass
@@ -727,7 +835,6 @@ def check(no_config, verbose, output, workstation=False):
727
835
  ]
728
836
  else:
729
837
  check_names = [
730
- "metaflow_config",
731
838
  "metaflow_token",
732
839
  "kubeconfig",
733
840
  "api_connectivity",
@@ -772,7 +879,7 @@ def check(no_config, verbose, output, workstation=False):
772
879
  )
773
880
  @click.argument("encoded_config", required=True)
774
881
  def configure(
775
- encoded_config=None, config_dir=None, profile=None, echo=None, force=False
882
+ encoded_config: str, config_dir=None, profile=None, echo=None, force=False
776
883
  ):
777
884
  writer = ConfigurationWriter(encoded_config, config_dir, profile)
778
885
  try:
@@ -794,3 +901,116 @@ def configure(
794
901
  except Exception as e:
795
902
  click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
796
903
  click.secho("Error: {}".format(str(e)))
904
+
905
+
906
+ @cli.command(
907
+ help="Authenticate service principals using JWT minted by their IDPs and configure Metaflow"
908
+ )
909
+ @click.option(
910
+ "-n",
911
+ "--name",
912
+ default="",
913
+ help="The name of service principals to authenticate",
914
+ required=True,
915
+ )
916
+ @click.option(
917
+ "--deployment-domain",
918
+ default="",
919
+ help="The full domain of the target Outerbounds Platform deployment (eg. 'foo.obp.outerbounds.com')",
920
+ required=True,
921
+ )
922
+ @click.option(
923
+ "-p",
924
+ "--perimeter",
925
+ default="default",
926
+ help="The name of the perimeter to authenticate the service principal in",
927
+ )
928
+ @click.option(
929
+ "-t",
930
+ "--jwt-token",
931
+ default="",
932
+ help="The JWT token that will be used to authenticate against the OBP Auth Server.",
933
+ )
934
+ @click.option(
935
+ "--github-actions",
936
+ is_flag=True,
937
+ 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.",
938
+ )
939
+ @click.option(
940
+ "-d",
941
+ "--config-dir",
942
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
943
+ help="Path to Metaflow configuration directory",
944
+ show_default=True,
945
+ )
946
+ @click.option(
947
+ "--profile",
948
+ default="",
949
+ help="Configure a named profile. Activate the profile by setting "
950
+ "`METAFLOW_PROFILE` environment variable.",
951
+ )
952
+ @click.option(
953
+ "-e",
954
+ "--echo",
955
+ is_flag=True,
956
+ help="Print decoded configuration to stdout",
957
+ )
958
+ @click.option(
959
+ "-f",
960
+ "--force",
961
+ is_flag=True,
962
+ help="Force overwrite of existing configuration",
963
+ )
964
+ def service_principal_configure(
965
+ name: str,
966
+ deployment_domain: str,
967
+ perimeter: str,
968
+ jwt_token="",
969
+ github_actions=False,
970
+ config_dir=None,
971
+ profile=None,
972
+ echo=None,
973
+ force=False,
974
+ ):
975
+ audience = f"https://{deployment_domain}"
976
+ if jwt_token == "" and github_actions:
977
+ jwt_token = get_gha_jwt(audience)
978
+
979
+ if jwt_token == "":
980
+ click.secho(
981
+ "No JWT token provided. Please provider either a valid jwt token or set --github-actions",
982
+ fg="red",
983
+ )
984
+ sys.exit(1)
985
+
986
+ auth_server = f"https://auth.{deployment_domain}"
987
+ deployment_name = deployment_domain.split(".")[0]
988
+ origin_token = get_origin_token(
989
+ name, deployment_name, perimeter, jwt_token, auth_server
990
+ )
991
+
992
+ api_server = f"https://api.{deployment_domain}"
993
+ metaflow_config = metaflowconfig.get_remote_metaflow_config_for_perimeter(
994
+ origin_token, perimeter, api_server
995
+ )
996
+
997
+ writer = ConfigurationWriter(serialize(metaflow_config), config_dir, profile)
998
+ try:
999
+ writer.decode()
1000
+ except:
1001
+ click.secho("Decoding the configuration text failed.", fg="red")
1002
+ sys.exit(1)
1003
+ try:
1004
+ writer.process_decoded_config()
1005
+ except DecodedConfigProcessingError as e:
1006
+ click.secho("Resolving the configuration remotely failed.", fg="red")
1007
+ click.secho(str(e), fg="magenta")
1008
+ sys.exit(1)
1009
+ try:
1010
+ if echo == True:
1011
+ writer.display()
1012
+ if force or writer.confirm_overwrite():
1013
+ writer.write_config()
1014
+ except Exception as e:
1015
+ click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
1016
+ click.secho("Error: {}".format(str(e)))
@@ -12,7 +12,6 @@ from os import path
12
12
  from pathlib import Path
13
13
  from typing import Any, Callable, Dict, List
14
14
 
15
- import boto3
16
15
  import click
17
16
  import requests
18
17
  from requests.exceptions import HTTPError
@@ -24,13 +23,20 @@ from ..utils.schema import (
24
23
  OuterboundsCommandStatus,
25
24
  )
26
25
 
26
+ from .local_setup_cli import PERIMETER_CONFIG_URL_KEY
27
+
27
28
 
28
29
  @click.group()
29
30
  def cli(**kwargs):
30
31
  pass
31
32
 
32
33
 
33
- @cli.command(help="Switch current perimeter", hidden=True)
34
+ @click.group(help="Manage perimeters")
35
+ def perimeter(**kwargs):
36
+ pass
37
+
38
+
39
+ @perimeter.command(help="Switch current perimeter")
34
40
  @click.option(
35
41
  "-d",
36
42
  "--config-dir",
@@ -59,7 +65,7 @@ def cli(**kwargs):
59
65
  help="Force change the existing perimeter",
60
66
  default=False,
61
67
  )
62
- def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=False):
68
+ def switch(config_dir=None, profile=None, output="", id=None, force=False):
63
69
  switch_perimeter_response = OuterboundsCommandResponse()
64
70
 
65
71
  switch_perimeter_step = CommandStatus(
@@ -94,7 +100,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
94
100
 
95
101
  ob_config_dict = {
96
102
  "OB_CURRENT_PERIMETER": str(id),
97
- "OB_CURRENT_PERIMETER_URL": perimeters[id]["remote_config_url"],
103
+ PERIMETER_CONFIG_URL_KEY: perimeters[id]["remote_config_url"],
98
104
  }
99
105
 
100
106
  # Now that we have the lock, we can safely write to the file
@@ -124,7 +130,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
124
130
  return
125
131
 
126
132
 
127
- @cli.command(help="Show current perimeter", hidden=True)
133
+ @perimeter.command(help="Show current perimeter")
128
134
  @click.option(
129
135
  "-d",
130
136
  "--config-dir",
@@ -146,7 +152,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa
146
152
  help="Show output in the specified format.",
147
153
  type=click.Choice(["json", ""]),
148
154
  )
149
- def show_current_perimeter(config_dir=None, profile=None, output=""):
155
+ def show_current(config_dir=None, profile=None, output=""):
150
156
  show_current_perimeter_response = OuterboundsCommandResponse()
151
157
 
152
158
  show_current_perimeter_step = CommandStatus(
@@ -192,7 +198,7 @@ def show_current_perimeter(config_dir=None, profile=None, output=""):
192
198
  click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
193
199
 
194
200
 
195
- @cli.command(help="List all available perimeters", hidden=True)
201
+ @perimeter.command(help="List all available perimeters")
196
202
  @click.option(
197
203
  "-d",
198
204
  "--config-dir",
@@ -213,13 +219,29 @@ def show_current_perimeter(config_dir=None, profile=None, output=""):
213
219
  help="Show output in the specified format.",
214
220
  type=click.Choice(["json", ""]),
215
221
  )
216
- def list_perimeters(config_dir=None, profile=None, output=""):
222
+ def list(config_dir=None, profile=None, output=""):
217
223
  list_perimeters_response = OuterboundsCommandResponse()
218
224
 
219
225
  list_perimeters_step = CommandStatus(
220
226
  "ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
221
227
  )
222
228
 
229
+ if "WORKSTATION_ID" in os.environ and (
230
+ "OBP_DEFAULT_PERIMETER" not in os.environ
231
+ or "OBP_DEFAULT_PERIMETER_URL" not in os.environ
232
+ ):
233
+ list_perimeters_response.update(
234
+ OuterboundsCommandStatus.NOT_SUPPORTED,
235
+ 500,
236
+ "Perimeters are not supported on old workstations.",
237
+ )
238
+ click.secho(
239
+ "Perimeters are not supported on old workstations.", err=True, fg="red"
240
+ )
241
+ if output == "json":
242
+ click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
243
+ return
244
+
223
245
  ob_config_dict = get_ob_config_or_fail_command(
224
246
  config_dir, profile, output, list_perimeters_response, list_perimeters_step
225
247
  )
@@ -372,3 +394,6 @@ def confirm_user_has_access_to_perimeter_or_fail(
372
394
  if output == "json":
373
395
  click.echo(json.dumps(command_response.as_dict(), indent=4))
374
396
  sys.exit(1)
397
+
398
+
399
+ cli.add_command(perimeter, name="perimeter")
@@ -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,7 +512,7 @@ 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):
@@ -559,7 +555,24 @@ def show_relevant_links(config_dir=None, profile=None, perimeter_id="", output="
559
555
  show_links_response.add_or_update_data("links", [])
560
556
  links = []
561
557
  try:
562
- metaflow_config = metaflowconfig.init_config(config_dir, profile)
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
+
563
576
  links.append(
564
577
  {
565
578
  "id": "metaflow-ui-url",
@@ -1,17 +1,44 @@
1
+ import click
1
2
  import json
2
3
  import os
3
4
  import requests
4
5
  from os import path
5
- import requests
6
+ from typing import Dict
7
+ import sys
8
+
9
+
10
+ def init_config(config_dir, profile) -> Dict[str, str]:
11
+ config = read_metaflow_config_from_filesystem(config_dir, profile)
12
+
13
+ # This is new remote-metaflow config; fetch it from the URL
14
+ if "OBP_METAFLOW_CONFIG_URL" in config:
15
+ remote_config = init_config_from_url(
16
+ config_dir, profile, config["OBP_METAFLOW_CONFIG_URL"]
17
+ )
18
+ remote_config["OBP_METAFLOW_CONFIG_URL"] = config["OBP_METAFLOW_CONFIG_URL"]
19
+ return remote_config
20
+ # Legacy config, use from filesystem
21
+ return config
22
+
6
23
 
24
+ def init_config_from_url(config_dir, profile, url) -> Dict[str, str]:
25
+ config = read_metaflow_config_from_filesystem(config_dir, profile)
26
+
27
+ if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
28
+ raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
7
29
 
8
- def init_config(config_dir="", profile="") -> dict:
9
- profile = profile or os.environ.get("METAFLOW_PROFILE")
10
- config_dir = config_dir or os.path.expanduser(
11
- os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
30
+ config_response = requests.get(
31
+ url,
32
+ headers={"x-api-key": f'{config["METAFLOW_SERVICE_AUTH_KEY"]}'},
12
33
  )
34
+ config_response.raise_for_status()
35
+ remote_config = config_response.json()["config"]
36
+ return remote_config
13
37
 
38
+
39
+ def read_metaflow_config_from_filesystem(config_dir, profile) -> Dict[str, str]:
14
40
  config_filename = f"config_{profile}.json" if profile else "config.json"
41
+
15
42
  path_to_config = os.path.join(config_dir, config_filename)
16
43
 
17
44
  if os.path.exists(path_to_config):
@@ -19,22 +46,6 @@ def init_config(config_dir="", profile="") -> dict:
19
46
  config = json.load(json_file)
20
47
  else:
21
48
  raise Exception("Unable to locate metaflow config at '%s')" % (path_to_config))
22
-
23
- # This is new remote-metaflow config; fetch it from the URL
24
- if "OBP_METAFLOW_CONFIG_URL" in config:
25
- if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
26
- raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
27
-
28
- config_response = requests.get(
29
- config["OBP_METAFLOW_CONFIG_URL"],
30
- headers={"x-api-key": f'{config["METAFLOW_SERVICE_AUTH_KEY"]}'},
31
- )
32
- config_response.raise_for_status()
33
- remote_config = config_response.json()["config"]
34
- remote_config["METAFLOW_SERVICE_AUTH_KEY"] = config["METAFLOW_SERVICE_AUTH_KEY"]
35
- return remote_config
36
-
37
- # Legacy config, use from filesystem
38
49
  return config
39
50
 
40
51
 
@@ -70,3 +81,23 @@ def get_sanitized_url_from_config(config_dir: str, profile: str, key: str) -> st
70
81
 
71
82
  url_in_config = url_in_config.rstrip("/")
72
83
  return url_in_config
84
+
85
+
86
+ def get_remote_metaflow_config_for_perimeter(
87
+ origin_token: str, perimeter: str, api_server: str
88
+ ):
89
+ try:
90
+ response = requests.get(
91
+ f"{api_server}/v1/perimeters/{perimeter}/metaflowconfigs/default",
92
+ headers={"x-api-key": origin_token},
93
+ )
94
+ response.raise_for_status()
95
+ config = response.json()["config"]
96
+ config["METAFLOW_SERVICE_AUTH_KEY"] = origin_token
97
+ return config
98
+ except Exception as e:
99
+ click.secho(
100
+ f"Failed to get metaflow config from {api_server}. Error: {str(e)}",
101
+ fg="red",
102
+ )
103
+ sys.exit(1)
@@ -5,6 +5,7 @@ class OuterboundsCommandStatus(Enum):
5
5
  OK = "OK"
6
6
  FAIL = "FAIL"
7
7
  WARN = "WARN"
8
+ NOT_SUPPORTED = "NOT_SUPPORTED"
8
9
 
9
10
 
10
11
  class CommandStatus:
@@ -39,6 +40,11 @@ class OuterboundsCommandResponse:
39
40
  self.metadata = {}
40
41
  self._data = {}
41
42
 
43
+ def update(self, status, code, message):
44
+ self.status = status
45
+ self._code = code
46
+ self._message = message
47
+
42
48
  def add_or_update_metadata(self, key, value):
43
49
  self.metadata[key] = value
44
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.55rc4
3
+ Version: 0.3.68
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -17,14 +17,16 @@ Provides-Extra: azure
17
17
  Provides-Extra: gcp
18
18
  Requires-Dist: PyYAML (>=6.0,<7.0)
19
19
  Requires-Dist: azure-identity (>=1.15.0,<2.0.0) ; extra == "azure"
20
+ Requires-Dist: azure-keyvault-secrets (>=4.7.0,<5.0.0) ; extra == "azure"
20
21
  Requires-Dist: azure-storage-blob (>=12.9.0,<13.0.0) ; extra == "azure"
21
22
  Requires-Dist: boto3
22
23
  Requires-Dist: click (>=8.1.3,<9.0.0)
23
24
  Requires-Dist: google-api-core (>=2.16.1,<3.0.0) ; extra == "gcp"
24
25
  Requires-Dist: google-auth (>=2.27.0,<3.0.0) ; extra == "gcp"
25
26
  Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
26
- Requires-Dist: ob-metaflow (==2.11.0.4)
27
- Requires-Dist: ob-metaflow-extensions (==1.1.45rc2)
27
+ Requires-Dist: ob-metaflow (==2.11.16.1)
28
+ Requires-Dist: ob-metaflow-extensions (==1.1.59)
29
+ Requires-Dist: ob-metaflow-stubs (==3.6)
28
30
  Requires-Dist: opentelemetry-distro (==0.41b0)
29
31
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.20.0)
30
32
  Requires-Dist: opentelemetry-instrumentation-requests (==0.41b0)
@@ -0,0 +1,15 @@
1
+ outerbounds/__init__.py,sha256=GPdaubvAYF8pOFWJ3b-sPMKCpyfpteWVMZWkmaYhxRw,32
2
+ outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
3
+ outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
4
+ outerbounds/command_groups/cli.py,sha256=H4LxcYTmsY9DQUrReSRLjvbg9s9Ro7s-eUrcMqEJ_9A,261
5
+ outerbounds/command_groups/local_setup_cli.py,sha256=Xqb-tsAkYgc90duC_6COSR9MsDpMNiKigQxlXUUYfN0,36530
6
+ outerbounds/command_groups/perimeters_cli.py,sha256=OxbxYQnHZDLRb3SFaVpD2mjp8W8s1fvK1Wc4htyRuGw,12757
7
+ outerbounds/command_groups/workstations_cli.py,sha256=b5lt8_g2B0zCoUoNriTRv32IPB6E4mI2sUhubDT7Yjo,21966
8
+ outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ outerbounds/utils/kubeconfig.py,sha256=l1mUP1j9VIq3fsffi5bJ1Nk-hYlwd1dIqkpj7DvVS1E,7936
10
+ outerbounds/utils/metaflowconfig.py,sha256=JkhT2yOGpN7t2R2p9uaUJRDJU9fqFPwn4DcojjVnJMI,3513
11
+ outerbounds/utils/schema.py,sha256=cNlgjmteLPbDzSEUSQDsq8txdhMGyezSmM83jU3aa0w,2329
12
+ outerbounds-0.3.68.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
13
+ outerbounds-0.3.68.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
14
+ outerbounds-0.3.68.dist-info/METADATA,sha256=IkAr4Uwjo_zdK1OW1BSMlNZZ7n7-MgOyy39ZL6Y_Raw,1477
15
+ outerbounds-0.3.68.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- outerbounds/__init__.py,sha256=GPdaubvAYF8pOFWJ3b-sPMKCpyfpteWVMZWkmaYhxRw,32
2
- outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
3
- outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
4
- outerbounds/command_groups/cli.py,sha256=H4LxcYTmsY9DQUrReSRLjvbg9s9Ro7s-eUrcMqEJ_9A,261
5
- outerbounds/command_groups/local_setup_cli.py,sha256=0sEi3V0sqoCW0RI2z3xMLNnit0pCsigWQ07m1mhDBso,29524
6
- outerbounds/command_groups/perimeters_cli.py,sha256=9tOql42d00KfHpZYkLLGEAOiy8iRbIzsknldCyICwU0,12063
7
- outerbounds/command_groups/workstations_cli.py,sha256=xHgETjWfUjiIkKODR-4qIS5pSc4ww3VS1ouZM9no8CY,21312
8
- outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- outerbounds/utils/kubeconfig.py,sha256=l1mUP1j9VIq3fsffi5bJ1Nk-hYlwd1dIqkpj7DvVS1E,7936
10
- outerbounds/utils/metaflowconfig.py,sha256=HgaDmK3F97rppfGUdysS1Zppe28ERTLV_HcB5IuPpV4,2631
11
- outerbounds/utils/schema.py,sha256=Ht_Yf5uoKO0m36WXHZLSPmWPH6EFWXfZDQsiAUquc5k,2160
12
- outerbounds-0.3.55rc4.dist-info/METADATA,sha256=6dAqYfSf245Yt-UAvKyh3BWE53-cYUbcaQMi9Zphd98,1367
13
- outerbounds-0.3.55rc4.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
14
- outerbounds-0.3.55rc4.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
15
- outerbounds-0.3.55rc4.dist-info/RECORD,,