outerbounds 0.3.88__py3-none-any.whl → 0.3.90__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.
@@ -640,7 +640,6 @@ class ConfigurationWriter:
640
640
  if config_type == "inline":
641
641
  if "OBP_PERIMETER" in self.decoded_config:
642
642
  self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
643
-
644
643
  if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
645
644
  self.decoded_config = {
646
645
  "OBP_METAFLOW_CONFIG_URL": self.decoded_config[
@@ -701,8 +700,6 @@ class ConfigurationWriter:
701
700
  with open(config_path, "w") as fd:
702
701
  json.dump(self.existing, fd, indent=4)
703
702
 
704
- # Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
705
- remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
706
703
  if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
707
704
  with open(self.ob_config_path, "w") as fd:
708
705
  ob_config_dict = {
@@ -1,21 +1,15 @@
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 zlib
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 pathlib import Path
13
- from typing import Any, Callable, Dict, List
6
+ from typing import Any, Dict
14
7
  from outerbounds._vendor import click
15
8
  import requests
16
- from requests.exceptions import HTTPError
9
+ import configparser
17
10
 
18
- from ..utils import kubeconfig, metaflowconfig
11
+ from ..utils import metaflowconfig
12
+ from ..utils.utils import safe_write_to_disk
19
13
  from ..utils.schema import (
20
14
  CommandStatus,
21
15
  OuterboundsCommandResponse,
@@ -80,7 +74,7 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
80
74
  id, perimeters, output, switch_perimeter_response, switch_perimeter_step
81
75
  )
82
76
 
83
- path_to_config = get_ob_config_file_path(config_dir, profile)
77
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
84
78
 
85
79
  import fcntl
86
80
 
@@ -124,9 +118,31 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
124
118
  )
125
119
 
126
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
+
127
144
  if output == "json":
128
145
  click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
129
- return
130
146
 
131
147
 
132
148
  @perimeter.command(help="Show current perimeter")
@@ -275,6 +291,58 @@ def list(config_dir=None, profile=None, output=""):
275
291
  click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
276
292
 
277
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
+
278
346
  def get_list_perimeters_api_response(config_dir, profile):
279
347
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
280
348
  api_url = metaflowconfig.get_sanitized_url_from_config(
@@ -288,15 +356,6 @@ def get_list_perimeters_api_response(config_dir, profile):
288
356
  return perimeters_response.json()["perimeters"]
289
357
 
290
358
 
291
- def get_ob_config_file_path(config_dir: str, profile: str) -> str:
292
- # If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
293
- # If neither are set, use ~/.metaflowconfig
294
- obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
295
-
296
- ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
297
- return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
298
-
299
-
300
359
  def get_perimeters_from_api_or_fail_command(
301
360
  config_dir: str,
302
361
  profile: str,
@@ -331,7 +390,7 @@ def get_ob_config_or_fail_command(
331
390
  command_response: OuterboundsCommandResponse,
332
391
  command_step: CommandStatus,
333
392
  ) -> Dict[str, str]:
334
- path_to_config = get_ob_config_file_path(config_dir, profile)
393
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
335
394
 
336
395
  if not os.path.exists(path_to_config):
337
396
  click.secho(
@@ -371,6 +430,23 @@ def get_ob_config_or_fail_command(
371
430
  return ob_config_dict
372
431
 
373
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
+
374
450
  def confirm_user_has_access_to_perimeter_or_fail(
375
451
  perimeter_id: str,
376
452
  perimeters: Dict[str, Any],
@@ -395,4 +471,93 @@ def confirm_user_has_access_to_perimeter_or_fail(
395
471
  sys.exit(1)
396
472
 
397
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
+ profile_name = "profile outerbounds"
522
+ if profile_name not in aws_config:
523
+ aws_config[profile_name] = {}
524
+
525
+ aws_config[profile_name]["role_arn"] = token_info["role_arn"]
526
+ aws_config[profile_name]["web_identity_token_file"] = token_file_loc
527
+
528
+ aws_config_string = StringIO()
529
+ aws_config.write(aws_config_string)
530
+
531
+ safe_write_to_disk(token_file_loc, token_info["token"])
532
+ safe_write_to_disk(config_file_loc, aws_config_string.getvalue())
533
+
534
+
535
+ def get_aws_auth_credentials(config_dir, profile): # pragma: no cover
536
+ token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
537
+ auth_server_url = metaflowconfig.get_sanitized_url_from_config(
538
+ config_dir, profile, "OBP_AUTH_SERVER"
539
+ )
540
+
541
+ response = requests.get(
542
+ "{}/generate/aws".format(auth_server_url), headers={"x-api-key": token}
543
+ )
544
+ response.raise_for_status()
545
+
546
+ return response.json()
547
+
548
+
549
+ def get_gcp_auth_credentials(config_dir, profile):
550
+ token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
551
+ auth_server_url = metaflowconfig.get_sanitized_url_from_config(
552
+ config_dir, profile, "OBP_AUTH_SERVER"
553
+ )
554
+
555
+ response = requests.get(
556
+ "{}/generate/gcp".format(auth_server_url), headers={"x-api-key": token}
557
+ )
558
+ response.raise_for_status()
559
+
560
+ return response.json()
561
+
562
+
398
563
  cli.add_command(perimeter, name="perimeter")
@@ -3,21 +3,43 @@ import json
3
3
  import os
4
4
  import requests
5
5
  from os import path
6
- from typing import Dict
6
+ from typing import Dict, Union
7
7
  import sys
8
8
 
9
+ """
10
+ key: perimeter specific URL to fetch the remote metaflow config from
11
+ value: the remote metaflow config
12
+ """
13
+ CACHED_REMOTE_METAFLOW_CONFIG: Dict[str, Dict[str, str]] = {}
14
+
15
+
16
+ CURRENT_PERIMETER_KEY = "OB_CURRENT_PERIMETER"
17
+ CURRENT_PERIMETER_URL = "OB_CURRENT_PERIMETER_MF_CONFIG_URL"
18
+ CURRENT_PERIMETER_URL_LEGACY_KEY = (
19
+ "OB_CURRENT_PERIMETER_URL" # For backwards compatibility with workstations.
20
+ )
21
+
9
22
 
10
23
  def init_config(config_dir, profile) -> Dict[str, str]:
24
+ global CACHED_REMOTE_METAFLOW_CONFIG
11
25
  config = read_metaflow_config_from_filesystem(config_dir, profile)
12
26
 
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"]
27
+ # Either user has an ob_config.json file with the perimeter URL
28
+ # or the default config on the filesystem has the config URL in it.
29
+ perimeter_specifc_url = get_perimeter_config_url_if_set_in_ob_config(
30
+ config_dir, profile
31
+ ) or config.get("OBP_METAFLOW_CONFIG_URL", "")
32
+
33
+ if perimeter_specifc_url != "":
34
+ if perimeter_specifc_url in CACHED_REMOTE_METAFLOW_CONFIG:
35
+ return CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url]
36
+
37
+ remote_config = init_config_from_url(config_dir, profile, perimeter_specifc_url)
38
+ remote_config["OBP_METAFLOW_CONFIG_URL"] = perimeter_specifc_url
39
+
40
+ CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url] = remote_config
19
41
  return remote_config
20
- # Legacy config, use from filesystem
42
+
21
43
  return config
22
44
 
23
45
 
@@ -101,3 +123,40 @@ def get_remote_metaflow_config_for_perimeter(
101
123
  fg="red",
102
124
  )
103
125
  sys.exit(1)
126
+
127
+
128
+ def get_ob_config_file_path(config_dir: str, profile: str) -> str:
129
+ # If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
130
+ # If neither are set, use ~/.metaflowconfig
131
+ obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
132
+
133
+ ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
134
+ return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
135
+
136
+
137
+ def get_perimeter_config_url_if_set_in_ob_config(
138
+ config_dir: str, profile: str
139
+ ) -> Union[str, None]:
140
+ file_path = get_ob_config_file_path(config_dir, profile)
141
+
142
+ if os.path.exists(file_path):
143
+ with open(file_path, "r") as f:
144
+ ob_config = json.loads(f.read())
145
+
146
+ if CURRENT_PERIMETER_URL in ob_config:
147
+ return ob_config[CURRENT_PERIMETER_URL]
148
+ elif CURRENT_PERIMETER_URL_LEGACY_KEY in ob_config:
149
+ return ob_config[CURRENT_PERIMETER_URL_LEGACY_KEY]
150
+ else:
151
+ raise ValueError(
152
+ "{} does not contain the key {}".format(
153
+ file_path, CURRENT_PERIMETER_KEY
154
+ )
155
+ )
156
+ elif "OBP_CONFIG_DIR" in os.environ:
157
+ raise FileNotFoundError(
158
+ "Environment variable OBP_CONFIG_DIR is set to {} but this directory does not contain an ob_config.json file.".format(
159
+ os.environ["OBP_CONFIG_DIR"]
160
+ )
161
+ )
162
+ return None
@@ -0,0 +1,19 @@
1
+ import tempfile
2
+ import os
3
+
4
+ """
5
+ Writes the given data to file_loc. Ensures that the directory exists
6
+ and uses a temporary file to ensure that the file is written atomically.
7
+ """
8
+
9
+
10
+ def safe_write_to_disk(file_loc, data):
11
+ # Ensure the directory exists
12
+ os.makedirs(os.path.dirname(file_loc), exist_ok=True)
13
+
14
+ with tempfile.NamedTemporaryFile(
15
+ "w", dir=os.path.dirname(file_loc), delete=False
16
+ ) as f:
17
+ f.write(data)
18
+ tmp_file = f.name
19
+ os.rename(tmp_file, file_loc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.88
3
+ Version: 0.3.90
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -42,15 +42,16 @@ outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7
42
42
  outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
43
43
  outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
44
44
  outerbounds/command_groups/cli.py,sha256=sorDdQvmTPqIwfvgtuNLILelimXu5CknFnWQFsYFGHs,286
45
- outerbounds/command_groups/local_setup_cli.py,sha256=somQMeLgRSK8BAje2rN6LeY-lszXmwBpNLvDCk293h8,36554
46
- outerbounds/command_groups/perimeters_cli.py,sha256=vU1LykO9T2Xj4Fn8hyhaCUR4q454q1aVoU4XhhrgAS4,12781
45
+ outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
46
+ outerbounds/command_groups/perimeters_cli.py,sha256=mrJfFIRYFOjuiz-9h4OKg2JT8Utmbs72z6wvPzDss3s,18685
47
47
  outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
48
48
  outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
50
- outerbounds/utils/metaflowconfig.py,sha256=Ja1eufYm4UANRNpk7qlxg_UUudToDOVCjaWg88N-xhQ,3538
50
+ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-EKUsAw,5770
51
51
  outerbounds/utils/schema.py,sha256=cNlgjmteLPbDzSEUSQDsq8txdhMGyezSmM83jU3aa0w,2329
52
+ outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
52
53
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
53
- outerbounds-0.3.88.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
54
- outerbounds-0.3.88.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
55
- outerbounds-0.3.88.dist-info/METADATA,sha256=Pg9IyVEEVQpR91fgtQL43o0fBVwNzGUBJ0GSprAT4DY,1632
56
- outerbounds-0.3.88.dist-info/RECORD,,
54
+ outerbounds-0.3.90.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
55
+ outerbounds-0.3.90.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
56
+ outerbounds-0.3.90.dist-info/METADATA,sha256=an69uvqpICHuiBfJVZJJrZU474ehFkjnnilY1jRLYFg,1632
57
+ outerbounds-0.3.90.dist-info/RECORD,,