outerbounds 0.3.88__py3-none-any.whl → 0.3.90__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,