outerbounds 0.3.55rc4__py3-none-any.whl → 0.3.89__py3-none-any.whl

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