outerbounds 0.3.55rc4__py3-none-any.whl → 0.3.89__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.
Files changed (54) hide show
  1. outerbounds/_vendor/PyYAML.LICENSE +20 -0
  2. outerbounds/_vendor/__init__.py +0 -0
  3. outerbounds/_vendor/_yaml/__init__.py +34 -0
  4. outerbounds/_vendor/click/__init__.py +73 -0
  5. outerbounds/_vendor/click/_compat.py +626 -0
  6. outerbounds/_vendor/click/_termui_impl.py +717 -0
  7. outerbounds/_vendor/click/_textwrap.py +49 -0
  8. outerbounds/_vendor/click/_winconsole.py +279 -0
  9. outerbounds/_vendor/click/core.py +2998 -0
  10. outerbounds/_vendor/click/decorators.py +497 -0
  11. outerbounds/_vendor/click/exceptions.py +287 -0
  12. outerbounds/_vendor/click/formatting.py +301 -0
  13. outerbounds/_vendor/click/globals.py +68 -0
  14. outerbounds/_vendor/click/parser.py +529 -0
  15. outerbounds/_vendor/click/py.typed +0 -0
  16. outerbounds/_vendor/click/shell_completion.py +580 -0
  17. outerbounds/_vendor/click/termui.py +787 -0
  18. outerbounds/_vendor/click/testing.py +479 -0
  19. outerbounds/_vendor/click/types.py +1073 -0
  20. outerbounds/_vendor/click/utils.py +580 -0
  21. outerbounds/_vendor/click.LICENSE +28 -0
  22. outerbounds/_vendor/vendor_any.txt +2 -0
  23. outerbounds/_vendor/yaml/__init__.py +471 -0
  24. outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
  25. outerbounds/_vendor/yaml/composer.py +146 -0
  26. outerbounds/_vendor/yaml/constructor.py +862 -0
  27. outerbounds/_vendor/yaml/cyaml.py +177 -0
  28. outerbounds/_vendor/yaml/dumper.py +138 -0
  29. outerbounds/_vendor/yaml/emitter.py +1239 -0
  30. outerbounds/_vendor/yaml/error.py +94 -0
  31. outerbounds/_vendor/yaml/events.py +104 -0
  32. outerbounds/_vendor/yaml/loader.py +62 -0
  33. outerbounds/_vendor/yaml/nodes.py +51 -0
  34. outerbounds/_vendor/yaml/parser.py +629 -0
  35. outerbounds/_vendor/yaml/reader.py +208 -0
  36. outerbounds/_vendor/yaml/representer.py +378 -0
  37. outerbounds/_vendor/yaml/resolver.py +245 -0
  38. outerbounds/_vendor/yaml/scanner.py +1555 -0
  39. outerbounds/_vendor/yaml/serializer.py +127 -0
  40. outerbounds/_vendor/yaml/tokens.py +129 -0
  41. outerbounds/command_groups/cli.py +1 -1
  42. outerbounds/command_groups/local_setup_cli.py +249 -33
  43. outerbounds/command_groups/perimeters_cli.py +168 -33
  44. outerbounds/command_groups/workstations_cli.py +29 -16
  45. outerbounds/utils/kubeconfig.py +2 -2
  46. outerbounds/utils/metaflowconfig.py +111 -21
  47. outerbounds/utils/schema.py +6 -0
  48. outerbounds/utils/utils.py +19 -0
  49. outerbounds/vendor.py +159 -0
  50. {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/METADATA +14 -6
  51. outerbounds-0.3.89.dist-info/RECORD +57 -0
  52. outerbounds-0.3.55rc4.dist-info/RECORD +0 -15
  53. {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/WHEEL +0 -0
  54. {outerbounds-0.3.55rc4.dist-info → outerbounds-0.3.89.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,127 @@
1
+ __all__ = ["Serializer", "SerializerError"]
2
+
3
+ from .error import YAMLError
4
+ from .events import *
5
+ from .nodes import *
6
+
7
+
8
+ class SerializerError(YAMLError):
9
+ pass
10
+
11
+
12
+ class Serializer:
13
+
14
+ ANCHOR_TEMPLATE = "id%03d"
15
+
16
+ def __init__(
17
+ self,
18
+ encoding=None,
19
+ explicit_start=None,
20
+ explicit_end=None,
21
+ version=None,
22
+ tags=None,
23
+ ):
24
+ self.use_encoding = encoding
25
+ self.use_explicit_start = explicit_start
26
+ self.use_explicit_end = explicit_end
27
+ self.use_version = version
28
+ self.use_tags = tags
29
+ self.serialized_nodes = {}
30
+ self.anchors = {}
31
+ self.last_anchor_id = 0
32
+ self.closed = None
33
+
34
+ def open(self):
35
+ if self.closed is None:
36
+ self.emit(StreamStartEvent(encoding=self.use_encoding))
37
+ self.closed = False
38
+ elif self.closed:
39
+ raise SerializerError("serializer is closed")
40
+ else:
41
+ raise SerializerError("serializer is already opened")
42
+
43
+ def close(self):
44
+ if self.closed is None:
45
+ raise SerializerError("serializer is not opened")
46
+ elif not self.closed:
47
+ self.emit(StreamEndEvent())
48
+ self.closed = True
49
+
50
+ # def __del__(self):
51
+ # self.close()
52
+
53
+ def serialize(self, node):
54
+ if self.closed is None:
55
+ raise SerializerError("serializer is not opened")
56
+ elif self.closed:
57
+ raise SerializerError("serializer is closed")
58
+ self.emit(
59
+ DocumentStartEvent(
60
+ explicit=self.use_explicit_start,
61
+ version=self.use_version,
62
+ tags=self.use_tags,
63
+ )
64
+ )
65
+ self.anchor_node(node)
66
+ self.serialize_node(node, None, None)
67
+ self.emit(DocumentEndEvent(explicit=self.use_explicit_end))
68
+ self.serialized_nodes = {}
69
+ self.anchors = {}
70
+ self.last_anchor_id = 0
71
+
72
+ def anchor_node(self, node):
73
+ if node in self.anchors:
74
+ if self.anchors[node] is None:
75
+ self.anchors[node] = self.generate_anchor(node)
76
+ else:
77
+ self.anchors[node] = None
78
+ if isinstance(node, SequenceNode):
79
+ for item in node.value:
80
+ self.anchor_node(item)
81
+ elif isinstance(node, MappingNode):
82
+ for key, value in node.value:
83
+ self.anchor_node(key)
84
+ self.anchor_node(value)
85
+
86
+ def generate_anchor(self, node):
87
+ self.last_anchor_id += 1
88
+ return self.ANCHOR_TEMPLATE % self.last_anchor_id
89
+
90
+ def serialize_node(self, node, parent, index):
91
+ alias = self.anchors[node]
92
+ if node in self.serialized_nodes:
93
+ self.emit(AliasEvent(alias))
94
+ else:
95
+ self.serialized_nodes[node] = True
96
+ self.descend_resolver(parent, index)
97
+ if isinstance(node, ScalarNode):
98
+ detected_tag = self.resolve(ScalarNode, node.value, (True, False))
99
+ default_tag = self.resolve(ScalarNode, node.value, (False, True))
100
+ implicit = (node.tag == detected_tag), (node.tag == default_tag)
101
+ self.emit(
102
+ ScalarEvent(alias, node.tag, implicit, node.value, style=node.style)
103
+ )
104
+ elif isinstance(node, SequenceNode):
105
+ implicit = node.tag == self.resolve(SequenceNode, node.value, True)
106
+ self.emit(
107
+ SequenceStartEvent(
108
+ alias, node.tag, implicit, flow_style=node.flow_style
109
+ )
110
+ )
111
+ index = 0
112
+ for item in node.value:
113
+ self.serialize_node(item, node, index)
114
+ index += 1
115
+ self.emit(SequenceEndEvent())
116
+ elif isinstance(node, MappingNode):
117
+ implicit = node.tag == self.resolve(MappingNode, node.value, True)
118
+ self.emit(
119
+ MappingStartEvent(
120
+ alias, node.tag, implicit, flow_style=node.flow_style
121
+ )
122
+ )
123
+ for key, value in node.value:
124
+ self.serialize_node(key, node, None)
125
+ self.serialize_node(value, node, key)
126
+ self.emit(MappingEndEvent())
127
+ self.ascend_resolver()
@@ -0,0 +1,129 @@
1
+ class Token(object):
2
+ def __init__(self, start_mark, end_mark):
3
+ self.start_mark = start_mark
4
+ self.end_mark = end_mark
5
+
6
+ def __repr__(self):
7
+ attributes = [key for key in self.__dict__ if not key.endswith("_mark")]
8
+ attributes.sort()
9
+ arguments = ", ".join(
10
+ ["%s=%r" % (key, getattr(self, key)) for key in attributes]
11
+ )
12
+ return "%s(%s)" % (self.__class__.__name__, arguments)
13
+
14
+
15
+ # class BOMToken(Token):
16
+ # id = '<byte order mark>'
17
+
18
+
19
+ class DirectiveToken(Token):
20
+ id = "<directive>"
21
+
22
+ def __init__(self, name, value, start_mark, end_mark):
23
+ self.name = name
24
+ self.value = value
25
+ self.start_mark = start_mark
26
+ self.end_mark = end_mark
27
+
28
+
29
+ class DocumentStartToken(Token):
30
+ id = "<document start>"
31
+
32
+
33
+ class DocumentEndToken(Token):
34
+ id = "<document end>"
35
+
36
+
37
+ class StreamStartToken(Token):
38
+ id = "<stream start>"
39
+
40
+ def __init__(self, start_mark=None, end_mark=None, encoding=None):
41
+ self.start_mark = start_mark
42
+ self.end_mark = end_mark
43
+ self.encoding = encoding
44
+
45
+
46
+ class StreamEndToken(Token):
47
+ id = "<stream end>"
48
+
49
+
50
+ class BlockSequenceStartToken(Token):
51
+ id = "<block sequence start>"
52
+
53
+
54
+ class BlockMappingStartToken(Token):
55
+ id = "<block mapping start>"
56
+
57
+
58
+ class BlockEndToken(Token):
59
+ id = "<block end>"
60
+
61
+
62
+ class FlowSequenceStartToken(Token):
63
+ id = "["
64
+
65
+
66
+ class FlowMappingStartToken(Token):
67
+ id = "{"
68
+
69
+
70
+ class FlowSequenceEndToken(Token):
71
+ id = "]"
72
+
73
+
74
+ class FlowMappingEndToken(Token):
75
+ id = "}"
76
+
77
+
78
+ class KeyToken(Token):
79
+ id = "?"
80
+
81
+
82
+ class ValueToken(Token):
83
+ id = ":"
84
+
85
+
86
+ class BlockEntryToken(Token):
87
+ id = "-"
88
+
89
+
90
+ class FlowEntryToken(Token):
91
+ id = ","
92
+
93
+
94
+ class AliasToken(Token):
95
+ id = "<alias>"
96
+
97
+ def __init__(self, value, start_mark, end_mark):
98
+ self.value = value
99
+ self.start_mark = start_mark
100
+ self.end_mark = end_mark
101
+
102
+
103
+ class AnchorToken(Token):
104
+ id = "<anchor>"
105
+
106
+ def __init__(self, value, start_mark, end_mark):
107
+ self.value = value
108
+ self.start_mark = start_mark
109
+ self.end_mark = end_mark
110
+
111
+
112
+ class TagToken(Token):
113
+ id = "<tag>"
114
+
115
+ def __init__(self, value, start_mark, end_mark):
116
+ self.value = value
117
+ self.start_mark = start_mark
118
+ self.end_mark = end_mark
119
+
120
+
121
+ class ScalarToken(Token):
122
+ id = "<scalar>"
123
+
124
+ def __init__(self, value, plain, start_mark, end_mark, style=None):
125
+ self.value = value
126
+ self.plain = plain
127
+ self.start_mark = start_mark
128
+ self.end_mark = end_mark
129
+ self.style = style
@@ -1,4 +1,4 @@
1
- import click
1
+ from outerbounds._vendor import click
2
2
  from . import local_setup_cli
3
3
  from . import workstations_cli
4
4
  from . import perimeters_cli
@@ -11,9 +11,7 @@ from importlib.machinery import PathFinder
11
11
  from os import path
12
12
  from pathlib import Path
13
13
  from typing import Any, Callable, Dict, List
14
-
15
- import boto3
16
- import click
14
+ from outerbounds._vendor import click
17
15
  import requests
18
16
  from requests.exceptions import HTTPError
19
17
 
@@ -31,7 +29,8 @@ the Outerbounds platform.
31
29
  To remove that package, please try `python -m pip uninstall metaflow -y` or reach out to Outerbounds support.
32
30
  After uninstalling the Metaflow package, please reinstall the Outerbounds package using `python -m pip
33
31
  install outerbounds --force`.
34
- As always, please reach out to Outerbounds support for any questions."""
32
+ As always, please reach out to Outerbounds support for any questions.
33
+ """
35
34
 
36
35
  MISSING_EXTENSIONS_MESSAGE = (
37
36
  "The Outerbounds Platform extensions for Metaflow was not found."
@@ -43,6 +42,8 @@ BAD_EXTENSION_MESSAGE = (
43
42
  "Mis-installation of the Outerbounds Platform extension package has been detected."
44
43
  )
45
44
 
45
+ PERIMETER_CONFIG_URL_KEY = "OB_CURRENT_PERIMETER_MF_CONFIG_URL"
46
+
46
47
 
47
48
  class Narrator:
48
49
  def __init__(self, verbose):
@@ -54,11 +55,11 @@ class Narrator:
54
55
 
55
56
  def section_ok(self):
56
57
  if not self.verbose:
57
- click.secho("\U0001F600", err=True)
58
+ click.secho("\U00002705", err=True)
58
59
 
59
60
  def section_not_ok(self):
60
61
  if not self.verbose:
61
- click.secho("\U0001F641", err=True)
62
+ click.secho("\U0000274C", err=True)
62
63
 
63
64
  def announce_check(self, name):
64
65
  if self.verbose:
@@ -232,25 +233,33 @@ class ConfigEntrySpec:
232
233
  self.expected = expected
233
234
 
234
235
 
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[/]?"),
236
+ def get_config_specs(default_datastore: str):
237
+ spec = [
241
238
  ConfigEntrySpec("METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER", "obp", expected="obp"),
242
- ConfigEntrySpec("METAFLOW_DEFAULT_DATASTORE", "s3", expected="s3"),
243
239
  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\..*"),
240
+ ConfigEntrySpec("METAFLOW_KUBERNETES_NAMESPACE", r"jobs-.*"),
241
+ ConfigEntrySpec("METAFLOW_KUBERNETES_SANDBOX_INIT_SCRIPT", r"eval \$\(.*"),
242
+ ConfigEntrySpec("METAFLOW_SERVICE_AUTH_KEY", r"[a-zA-Z0-9!_\-\.]+"),
243
+ ConfigEntrySpec("METAFLOW_SERVICE_URL", r"https://metadata\..*"),
244
+ ConfigEntrySpec("METAFLOW_UI_URL", r"https://ui\..*"),
245
+ ConfigEntrySpec("OBP_AUTH_SERVER", r"auth\..*"),
252
246
  ]
253
247
 
248
+ if default_datastore == "s3":
249
+ spec.extend(
250
+ [
251
+ ConfigEntrySpec(
252
+ "METAFLOW_DATASTORE_SYSROOT_S3",
253
+ r"s3://[a-z0-9\-]+/metaflow(-[a-z0-9\-]+)?[/]?",
254
+ ),
255
+ ConfigEntrySpec(
256
+ "METAFLOW_DATATOOLS_S3ROOT",
257
+ r"s3://[a-z0-9\-]+/data(-[a-z0-9\-]+)?[/]?",
258
+ ),
259
+ ]
260
+ )
261
+ return spec
262
+
254
263
 
255
264
  def check_metaflow_config(narrator: Narrator) -> CommandStatus:
256
265
  narrator.announce_section("local Metaflow config")
@@ -261,8 +270,20 @@ def check_metaflow_config(narrator: Narrator) -> CommandStatus:
261
270
  mitigation="",
262
271
  )
263
272
 
264
- config = metaflowconfig.init_config()
265
- for spec in get_config_specs():
273
+ profile = os.environ.get("METAFLOW_PROFILE")
274
+ config_dir = os.path.expanduser(
275
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
276
+ )
277
+
278
+ config = metaflowconfig.init_config(config_dir, profile)
279
+
280
+ if "OBP_METAFLOW_CONFIG_URL" in config:
281
+ # If the config is fetched from a remote source, not much to check
282
+ narrator.announce_check("config entry OBP_METAFLOW_CONFIG_URL")
283
+ narrator.ok()
284
+ return check_status
285
+
286
+ for spec in get_config_specs(config.get("METAFLOW_DEFAULT_DATASTORE", "")):
266
287
  narrator.announce_check("config entry " + spec.name)
267
288
  if spec.name not in config:
268
289
  reason = "Missing"
@@ -304,7 +325,12 @@ def check_metaflow_token(narrator: Narrator) -> CommandStatus:
304
325
  mitigation="",
305
326
  )
306
327
 
307
- config = metaflowconfig.init_config()
328
+ profile = os.environ.get("METAFLOW_PROFILE")
329
+ config_dir = os.path.expanduser(
330
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
331
+ )
332
+
333
+ config = metaflowconfig.init_config(config_dir, profile)
308
334
  try:
309
335
  if "OBP_AUTH_SERVER" in config:
310
336
  k8s_response = requests.get(
@@ -363,7 +389,13 @@ def check_workstation_api_accessible(narrator: Narrator) -> CommandStatus:
363
389
  )
364
390
 
365
391
  try:
366
- config = metaflowconfig.init_config()
392
+ profile = os.environ.get("METAFLOW_PROFILE")
393
+ config_dir = os.path.expanduser(
394
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
395
+ )
396
+
397
+ config = metaflowconfig.init_config(config_dir, profile)
398
+
367
399
  missing_keys = []
368
400
  if "METAFLOW_SERVICE_AUTH_KEY" not in config:
369
401
  missing_keys.append("METAFLOW_SERVICE_AUTH_KEY")
@@ -422,7 +454,13 @@ def check_kubeconfig_valid_for_workstations(narrator: Narrator) -> CommandStatus
422
454
  )
423
455
 
424
456
  try:
425
- config = metaflowconfig.init_config()
457
+ profile = os.environ.get("METAFLOW_PROFILE")
458
+ config_dir = os.path.expanduser(
459
+ os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
460
+ )
461
+
462
+ config = metaflowconfig.init_config(config_dir, profile)
463
+
426
464
  missing_keys = []
427
465
  if "METAFLOW_SERVICE_AUTH_KEY" not in config:
428
466
  missing_keys.append("METAFLOW_SERVICE_AUTH_KEY")
@@ -585,6 +623,7 @@ class ConfigurationWriter:
585
623
  self.decoded_config = None
586
624
  self.out_dir = out_dir
587
625
  self.profile = profile
626
+ self.selected_perimeter = None
588
627
 
589
628
  ob_config_dir = path.expanduser(os.getenv("OBP_CONFIG_DIR", out_dir))
590
629
  self.ob_config_path = path.join(
@@ -596,8 +635,11 @@ class ConfigurationWriter:
596
635
  self.decoded_config = deserialize(self.encoded_config)
597
636
 
598
637
  def process_decoded_config(self):
638
+ assert self.decoded_config is not None
599
639
  config_type = self.decoded_config.get("OB_CONFIG_TYPE", "inline")
600
640
  if config_type == "inline":
641
+ if "OBP_PERIMETER" in self.decoded_config:
642
+ self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
601
643
  if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
602
644
  self.decoded_config = {
603
645
  "OBP_METAFLOW_CONFIG_URL": self.decoded_config[
@@ -616,6 +658,8 @@ class ConfigurationWriter:
616
658
  f"{str(e)} key is required for aws-ref config type"
617
659
  )
618
660
  try:
661
+ import boto3
662
+
619
663
  client = boto3.client("secretsmanager", region_name=region)
620
664
  response = client.get_secret_value(SecretId=secret_arn)
621
665
  self.decoded_config = json.loads(response["SecretBinary"])
@@ -633,6 +677,7 @@ class ConfigurationWriter:
633
677
  return path.join(self.out_dir, "config_{}.json".format(self.profile))
634
678
 
635
679
  def display(self):
680
+ assert self.decoded_config is not None
636
681
  # Create a copy so we can use the real config later, possibly
637
682
  display_config = dict()
638
683
  for k in self.decoded_config.keys():
@@ -647,6 +692,7 @@ class ConfigurationWriter:
647
692
  return self.confirm_overwrite_config(self.path())
648
693
 
649
694
  def write_config(self):
695
+ assert self.decoded_config is not None
650
696
  config_path = self.path()
651
697
  # TODO config contains auth token - restrict file/dir modes
652
698
  os.makedirs(os.path.dirname(config_path), exist_ok=True)
@@ -654,13 +700,13 @@ class ConfigurationWriter:
654
700
  with open(config_path, "w") as fd:
655
701
  json.dump(self.existing, fd, indent=4)
656
702
 
657
- # Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
658
- remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
659
- if "OBP_PERIMETER" in remote_config and "OBP_PERIMETER_URL" in remote_config:
703
+ if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
660
704
  with open(self.ob_config_path, "w") as fd:
661
705
  ob_config_dict = {
662
- "OB_CURRENT_PERIMETER": remote_config["OBP_PERIMETER"],
663
- "OB_CURRENT_PERIMETER_URL": remote_config["OBP_PERIMETER_URL"],
706
+ "OB_CURRENT_PERIMETER": self.selected_perimeter,
707
+ PERIMETER_CONFIG_URL_KEY: self.decoded_config[
708
+ "OBP_METAFLOW_CONFIG_URL"
709
+ ],
664
710
  }
665
711
  json.dump(ob_config_dict, fd, indent=4)
666
712
 
@@ -686,6 +732,64 @@ class ConfigurationWriter:
686
732
  return True
687
733
 
688
734
 
735
+ def get_gha_jwt(audience: str):
736
+ # These are specific environment variables that are set by GitHub Actions.
737
+ if (
738
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN" in os.environ
739
+ and "ACTIONS_ID_TOKEN_REQUEST_URL" in os.environ
740
+ ):
741
+ try:
742
+ response = requests.get(
743
+ url=os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"],
744
+ headers={
745
+ "Authorization": f"Bearer {os.environ['ACTIONS_ID_TOKEN_REQUEST_TOKEN']}"
746
+ },
747
+ params={"audience": audience},
748
+ )
749
+ response.raise_for_status()
750
+ return response.json()["value"]
751
+ except Exception as e:
752
+ click.secho(
753
+ "Failed to fetch JWT token from GitHub Actions. Please make sure you are permission 'id-token: write' is set on the GHA jobs level.",
754
+ fg="red",
755
+ )
756
+ sys.exit(1)
757
+
758
+ click.secho(
759
+ "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",
760
+ fg="red",
761
+ )
762
+ sys.exit(1)
763
+
764
+
765
+ def get_origin_token(
766
+ service_principal_name: str,
767
+ deployment: str,
768
+ perimeter: str,
769
+ token: str,
770
+ auth_server: str,
771
+ ):
772
+ try:
773
+ response = requests.get(
774
+ f"{auth_server}/generate/service-principal",
775
+ headers={"x-api-key": token},
776
+ data=json.dumps(
777
+ {
778
+ "servicePrincipalName": service_principal_name,
779
+ "deploymentName": deployment,
780
+ "perimeter": perimeter,
781
+ }
782
+ ),
783
+ )
784
+ response.raise_for_status()
785
+ return response.json()["token"]
786
+ except Exception as e:
787
+ click.secho(
788
+ f"Failed to get origin token from {auth_server}. Error: {str(e)}", fg="red"
789
+ )
790
+ sys.exit(1)
791
+
792
+
689
793
  @click.group(help="The Outerbounds Platform CLI", no_args_is_help=True)
690
794
  def cli(**kwargs):
691
795
  pass
@@ -727,7 +831,6 @@ def check(no_config, verbose, output, workstation=False):
727
831
  ]
728
832
  else:
729
833
  check_names = [
730
- "metaflow_config",
731
834
  "metaflow_token",
732
835
  "kubeconfig",
733
836
  "api_connectivity",
@@ -772,7 +875,7 @@ def check(no_config, verbose, output, workstation=False):
772
875
  )
773
876
  @click.argument("encoded_config", required=True)
774
877
  def configure(
775
- encoded_config=None, config_dir=None, profile=None, echo=None, force=False
878
+ encoded_config: str, config_dir=None, profile=None, echo=None, force=False
776
879
  ):
777
880
  writer = ConfigurationWriter(encoded_config, config_dir, profile)
778
881
  try:
@@ -794,3 +897,116 @@ def configure(
794
897
  except Exception as e:
795
898
  click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
796
899
  click.secho("Error: {}".format(str(e)))
900
+
901
+
902
+ @cli.command(
903
+ help="Authenticate service principals using JWT minted by their IDPs and configure Metaflow"
904
+ )
905
+ @click.option(
906
+ "-n",
907
+ "--name",
908
+ default="",
909
+ help="The name of service principals to authenticate",
910
+ required=True,
911
+ )
912
+ @click.option(
913
+ "--deployment-domain",
914
+ default="",
915
+ help="The full domain of the target Outerbounds Platform deployment (eg. 'foo.obp.outerbounds.com')",
916
+ required=True,
917
+ )
918
+ @click.option(
919
+ "-p",
920
+ "--perimeter",
921
+ default="default",
922
+ help="The name of the perimeter to authenticate the service principal in",
923
+ )
924
+ @click.option(
925
+ "-t",
926
+ "--jwt-token",
927
+ default="",
928
+ help="The JWT token that will be used to authenticate against the OBP Auth Server.",
929
+ )
930
+ @click.option(
931
+ "--github-actions",
932
+ is_flag=True,
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.",
934
+ )
935
+ @click.option(
936
+ "-d",
937
+ "--config-dir",
938
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
939
+ help="Path to Metaflow configuration directory",
940
+ show_default=True,
941
+ )
942
+ @click.option(
943
+ "--profile",
944
+ default="",
945
+ help="Configure a named profile. Activate the profile by setting "
946
+ "`METAFLOW_PROFILE` environment variable.",
947
+ )
948
+ @click.option(
949
+ "-e",
950
+ "--echo",
951
+ is_flag=True,
952
+ help="Print decoded configuration to stdout",
953
+ )
954
+ @click.option(
955
+ "-f",
956
+ "--force",
957
+ is_flag=True,
958
+ help="Force overwrite of existing configuration",
959
+ )
960
+ def service_principal_configure(
961
+ name: str,
962
+ deployment_domain: str,
963
+ perimeter: str,
964
+ jwt_token="",
965
+ github_actions=False,
966
+ config_dir=None,
967
+ profile=None,
968
+ echo=None,
969
+ force=False,
970
+ ):
971
+ audience = f"https://{deployment_domain}"
972
+ if jwt_token == "" and github_actions:
973
+ jwt_token = get_gha_jwt(audience)
974
+
975
+ if jwt_token == "":
976
+ click.secho(
977
+ "No JWT token provided. Please provider either a valid jwt token or set --github-actions",
978
+ fg="red",
979
+ )
980
+ sys.exit(1)
981
+
982
+ auth_server = f"https://auth.{deployment_domain}"
983
+ deployment_name = deployment_domain.split(".")[0]
984
+ origin_token = get_origin_token(
985
+ name, deployment_name, perimeter, jwt_token, auth_server
986
+ )
987
+
988
+ api_server = f"https://api.{deployment_domain}"
989
+ metaflow_config = metaflowconfig.get_remote_metaflow_config_for_perimeter(
990
+ origin_token, perimeter, api_server
991
+ )
992
+
993
+ writer = ConfigurationWriter(serialize(metaflow_config), config_dir, profile)
994
+ try:
995
+ writer.decode()
996
+ except:
997
+ click.secho("Decoding the configuration text failed.", fg="red")
998
+ sys.exit(1)
999
+ try:
1000
+ writer.process_decoded_config()
1001
+ except DecodedConfigProcessingError as e:
1002
+ click.secho("Resolving the configuration remotely failed.", fg="red")
1003
+ click.secho(str(e), fg="magenta")
1004
+ sys.exit(1)
1005
+ try:
1006
+ if echo == True:
1007
+ writer.display()
1008
+ if force or writer.confirm_overwrite():
1009
+ writer.write_config()
1010
+ except Exception as e:
1011
+ click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
1012
+ click.secho("Error: {}".format(str(e)))