outerbounds 0.3.56__py3-none-any.whl → 0.3.57rc0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,12 @@
1
1
  import click
2
2
  from . import local_setup_cli
3
3
  from . import workstations_cli
4
+ from . import perimeters_cli
4
5
 
5
6
 
6
7
  @click.command(
7
- cls=click.CommandCollection, sources=[local_setup_cli.cli, workstations_cli.cli]
8
+ cls=click.CommandCollection,
9
+ sources=[local_setup_cli.cli, workstations_cli.cli, perimeters_cli.cli],
8
10
  )
9
11
  def cli(**kwargs):
10
12
  pass
@@ -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):
@@ -235,20 +237,20 @@ class ConfigEntrySpec:
235
237
  def get_config_specs():
236
238
  return [
237
239
  ConfigEntrySpec(
238
- "METAFLOW_DATASTORE_SYSROOT_S3", "s3://[a-z0-9\-]+/metaflow[/]?"
240
+ "METAFLOW_DATASTORE_SYSROOT_S3", r"s3://[a-z0-9\-]+/metaflow[/]?"
239
241
  ),
240
- ConfigEntrySpec("METAFLOW_DATATOOLS_S3ROOT", "s3://[a-z0-9\-]+/data[/]?"),
242
+ ConfigEntrySpec("METAFLOW_DATATOOLS_S3ROOT", r"s3://[a-z0-9\-]+/data[/]?"),
241
243
  ConfigEntrySpec("METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER", "obp", expected="obp"),
242
244
  ConfigEntrySpec("METAFLOW_DEFAULT_DATASTORE", "s3", expected="s3"),
243
245
  ConfigEntrySpec("METAFLOW_DEFAULT_METADATA", "service", expected="service"),
244
246
  ConfigEntrySpec(
245
- "METAFLOW_KUBERNETES_NAMESPACE", "jobs\-default", expected="jobs-default"
247
+ "METAFLOW_KUBERNETES_NAMESPACE", r"jobs\-default", expected="jobs-default"
246
248
  ),
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\..*"),
249
+ ConfigEntrySpec("METAFLOW_KUBERNETES_SANDBOX_INIT_SCRIPT", r"eval \$\(.*"),
250
+ ConfigEntrySpec("METAFLOW_SERVICE_AUTH_KEY", r"[a-zA-Z0-9!_\-\.]+"),
251
+ ConfigEntrySpec("METAFLOW_SERVICE_URL", r"https://metadata\..*"),
252
+ ConfigEntrySpec("METAFLOW_UI_URL", r"https://ui\..*"),
253
+ ConfigEntrySpec("OBP_AUTH_SERVER", r"auth\..*"),
252
254
  ]
253
255
 
254
256
 
@@ -586,6 +588,12 @@ class ConfigurationWriter:
586
588
  self.out_dir = out_dir
587
589
  self.profile = profile
588
590
 
591
+ ob_config_dir = path.expanduser(os.getenv("OBP_CONFIG_DIR", out_dir))
592
+ self.ob_config_path = path.join(
593
+ ob_config_dir,
594
+ "ob_config_{}.json".format(profile) if profile else "ob_config.json",
595
+ )
596
+
589
597
  def decode(self):
590
598
  self.decoded_config = deserialize(self.encoded_config)
591
599
 
@@ -648,6 +656,19 @@ class ConfigurationWriter:
648
656
  with open(config_path, "w") as fd:
649
657
  json.dump(self.existing, fd, indent=4)
650
658
 
659
+ # Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
660
+ remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
661
+ if (
662
+ "OBP_PERIMETER" in remote_config
663
+ and "OBP_METAFLOW_CONFIG_URL" in remote_config
664
+ ):
665
+ with open(self.ob_config_path, "w") as fd:
666
+ ob_config_dict = {
667
+ "OB_CURRENT_PERIMETER": remote_config["OBP_PERIMETER"],
668
+ PERIMETER_CONFIG_URL_KEY: remote_config["OBP_METAFLOW_CONFIG_URL"],
669
+ }
670
+ json.dump(ob_config_dict, fd, indent=4)
671
+
651
672
  def confirm_overwrite_config(self, config_path):
652
673
  if os.path.exists(config_path):
653
674
  if not click.confirm(
@@ -0,0 +1,392 @@
1
+ import base64
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import re
6
+ import subprocess
7
+ import sys
8
+ import zlib
9
+ from base64 import b64decode, b64encode
10
+ from importlib.machinery import PathFinder
11
+ from os import path
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, List
14
+
15
+ import boto3
16
+ import click
17
+ import requests
18
+ from requests.exceptions import HTTPError
19
+
20
+ from ..utils import kubeconfig, metaflowconfig
21
+ from ..utils.schema import (
22
+ CommandStatus,
23
+ OuterboundsCommandResponse,
24
+ OuterboundsCommandStatus,
25
+ )
26
+
27
+ from .local_setup_cli import PERIMETER_CONFIG_URL_KEY
28
+
29
+
30
+ @click.group()
31
+ def cli(**kwargs):
32
+ pass
33
+
34
+
35
+ @cli.command(help="Switch current perimeter")
36
+ @click.option(
37
+ "-d",
38
+ "--config-dir",
39
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
40
+ help="Path to Metaflow configuration directory",
41
+ show_default=True,
42
+ )
43
+ @click.option(
44
+ "-p",
45
+ "--profile",
46
+ default=os.environ.get("METAFLOW_PROFILE", ""),
47
+ help="The named metaflow profile in which your workstation exists",
48
+ )
49
+ @click.option(
50
+ "-o",
51
+ "--output",
52
+ default="",
53
+ help="Show output in the specified format.",
54
+ type=click.Choice(["json", ""]),
55
+ )
56
+ @click.option("--id", default="", type=str, help="Perimeter name to switch to")
57
+ @click.option(
58
+ "-f",
59
+ "--force",
60
+ is_flag=True,
61
+ help="Force change the existing perimeter",
62
+ default=False,
63
+ )
64
+ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=False):
65
+ switch_perimeter_response = OuterboundsCommandResponse()
66
+
67
+ switch_perimeter_step = CommandStatus(
68
+ "SwitchPerimeter",
69
+ OuterboundsCommandStatus.OK,
70
+ "Perimeter was successfully switched!",
71
+ )
72
+
73
+ perimeters = get_perimeters_from_api_or_fail_command(
74
+ config_dir, profile, output, switch_perimeter_response, switch_perimeter_step
75
+ )
76
+ confirm_user_has_access_to_perimeter_or_fail(
77
+ id, perimeters, output, switch_perimeter_response, switch_perimeter_step
78
+ )
79
+
80
+ path_to_config = get_ob_config_file_path(config_dir, profile)
81
+
82
+ import fcntl
83
+
84
+ try:
85
+ if os.path.exists(path_to_config):
86
+ if not force:
87
+ fd = os.open(path_to_config, os.O_WRONLY)
88
+ # Try to acquire an exclusive lock
89
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
90
+ else:
91
+ click.secho(
92
+ "Force flag is set. Perimeter will be switched, but can have unintended consequences on other running processes.",
93
+ fg="yellow",
94
+ err=True,
95
+ )
96
+
97
+ ob_config_dict = {
98
+ "OB_CURRENT_PERIMETER": str(id),
99
+ PERIMETER_CONFIG_URL_KEY: perimeters[id]["remote_config_url"],
100
+ }
101
+
102
+ # Now that we have the lock, we can safely write to the file
103
+ with open(path_to_config, "w") as file:
104
+ json.dump(ob_config_dict, file, indent=4)
105
+
106
+ click.secho("Perimeter switched to {}".format(id), fg="green", err=True)
107
+ except BlockingIOError:
108
+ # This exception is raised if the file is already locked (non-blocking mode)
109
+ # Note that its the metaflow package (the extension actually) that acquires a shared read lock
110
+ # on the file whenever a process imports metaflow.
111
+ # In the future we might want to get smarter about it and show which process is holding the lock.
112
+ click.secho(
113
+ "Can't switch perimeter while Metaflow is in use. Please make sure there are no running python processes or notebooks using metaflow.",
114
+ fg="red",
115
+ err=True,
116
+ )
117
+ switch_perimeter_step.update(
118
+ status=OuterboundsCommandStatus.FAIL,
119
+ reason="Can't switch perimeter while Metaflow is in use.",
120
+ mitigation="Please make sure there are no running python processes or notebooks using metaflow.",
121
+ )
122
+
123
+ switch_perimeter_response.add_step(switch_perimeter_step)
124
+ if output == "json":
125
+ click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
126
+ return
127
+
128
+
129
+ @cli.command(help="Show current perimeter")
130
+ @click.option(
131
+ "-d",
132
+ "--config-dir",
133
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
134
+ help="Path to Metaflow configuration directory",
135
+ show_default=True,
136
+ )
137
+ @click.option(
138
+ "-p",
139
+ "--profile",
140
+ default=os.environ.get("METAFLOW_PROFILE", ""),
141
+ help="Configure a named profile. Activate the profile by setting "
142
+ "`METAFLOW_PROFILE` environment variable.",
143
+ )
144
+ @click.option(
145
+ "-o",
146
+ "--output",
147
+ default="",
148
+ help="Show output in the specified format.",
149
+ type=click.Choice(["json", ""]),
150
+ )
151
+ def show_current_perimeter(config_dir=None, profile=None, output=""):
152
+ show_current_perimeter_response = OuterboundsCommandResponse()
153
+
154
+ show_current_perimeter_step = CommandStatus(
155
+ "ShowCurrentPerimeter",
156
+ OuterboundsCommandStatus.OK,
157
+ "Current Perimeter Fetch Successful.",
158
+ )
159
+
160
+ ob_config_dict = get_ob_config_or_fail_command(
161
+ config_dir,
162
+ profile,
163
+ output,
164
+ show_current_perimeter_response,
165
+ show_current_perimeter_step,
166
+ )
167
+
168
+ perimeters = get_perimeters_from_api_or_fail_command(
169
+ config_dir,
170
+ profile,
171
+ output,
172
+ show_current_perimeter_response,
173
+ show_current_perimeter_step,
174
+ )
175
+ confirm_user_has_access_to_perimeter_or_fail(
176
+ ob_config_dict["OB_CURRENT_PERIMETER"],
177
+ perimeters,
178
+ output,
179
+ show_current_perimeter_response,
180
+ show_current_perimeter_step,
181
+ )
182
+
183
+ click.secho(
184
+ "Current Perimeter: {}".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
185
+ fg="green",
186
+ err=True,
187
+ )
188
+
189
+ show_current_perimeter_response.add_or_update_data(
190
+ "current_perimeter", ob_config_dict["OB_CURRENT_PERIMETER"]
191
+ )
192
+
193
+ if output == "json":
194
+ click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
195
+
196
+
197
+ @cli.command(help="List all available perimeters")
198
+ @click.option(
199
+ "-d",
200
+ "--config-dir",
201
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
202
+ help="Path to Metaflow configuration directory",
203
+ show_default=True,
204
+ )
205
+ @click.option(
206
+ "-p",
207
+ "--profile",
208
+ default=os.environ.get("METAFLOW_PROFILE", ""),
209
+ help="The named metaflow profile in which your workstation exists",
210
+ )
211
+ @click.option(
212
+ "-o",
213
+ "--output",
214
+ default="",
215
+ help="Show output in the specified format.",
216
+ type=click.Choice(["json", ""]),
217
+ )
218
+ def list_perimeters(config_dir=None, profile=None, output=""):
219
+ list_perimeters_response = OuterboundsCommandResponse()
220
+
221
+ list_perimeters_step = CommandStatus(
222
+ "ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
223
+ )
224
+
225
+ if "WORKSTATION_ID" in os.environ and (
226
+ "OBP_DEFAULT_PERIMETER" not in os.environ
227
+ or "OBP_DEFAULT_PERIMETER_URL" not in os.environ
228
+ ):
229
+ list_perimeters_response.update(
230
+ OuterboundsCommandStatus.NOT_SUPPORTED,
231
+ 500,
232
+ "Perimeters are not supported on old workstations.",
233
+ )
234
+ click.secho(
235
+ "Perimeters are not supported on old workstations.", err=True, fg="red"
236
+ )
237
+ if output == "json":
238
+ click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
239
+ return
240
+
241
+ ob_config_dict = get_ob_config_or_fail_command(
242
+ config_dir, profile, output, list_perimeters_response, list_perimeters_step
243
+ )
244
+ active_perimeter = ob_config_dict["OB_CURRENT_PERIMETER"]
245
+
246
+ perimeters = get_perimeters_from_api_or_fail_command(
247
+ config_dir, profile, output, list_perimeters_response, list_perimeters_step
248
+ )
249
+
250
+ perimeter_list = []
251
+ for perimeter in perimeters.values():
252
+ status = "OK"
253
+ perimeter_list.append(
254
+ {
255
+ "id": perimeter["perimeter"],
256
+ "active": perimeter["perimeter"] == active_perimeter,
257
+ "status": status,
258
+ }
259
+ )
260
+ if perimeter["perimeter"] != active_perimeter:
261
+ click.secho("Perimeter: {}".format(perimeter["perimeter"]), err=True)
262
+ else:
263
+ click.secho(
264
+ "Perimeter: {} (active)".format(perimeter["perimeter"]),
265
+ fg="green",
266
+ err=True,
267
+ )
268
+
269
+ list_perimeters_response.add_or_update_data("perimeters", perimeter_list)
270
+
271
+ if output == "json":
272
+ click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
273
+
274
+
275
+ def get_list_perimeters_api_response(config_dir, profile):
276
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
277
+ api_url = metaflowconfig.get_sanitized_url_from_config(
278
+ config_dir, profile, "OBP_API_SERVER"
279
+ )
280
+ perimeters_response = requests.get(
281
+ f"{api_url}/v1/me/perimeters?privilege=Execute",
282
+ headers={"x-api-key": metaflow_token},
283
+ )
284
+ perimeters_response.raise_for_status()
285
+ return perimeters_response.json()["perimeters"]
286
+
287
+
288
+ def get_ob_config_file_path(config_dir: str, profile: str) -> str:
289
+ # If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
290
+ # If neither are set, use ~/.metaflowconfig
291
+ obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
292
+
293
+ ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
294
+ return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
295
+
296
+
297
+ def get_perimeters_from_api_or_fail_command(
298
+ config_dir: str,
299
+ profile: str,
300
+ output: str,
301
+ command_response: OuterboundsCommandResponse,
302
+ command_step: CommandStatus,
303
+ ) -> Dict[str, Dict[str, str]]:
304
+ try:
305
+ perimeters = get_list_perimeters_api_response(config_dir, profile)
306
+ except:
307
+ click.secho(
308
+ "Failed to fetch perimeters from API.",
309
+ fg="red",
310
+ err=True,
311
+ )
312
+ command_step.update(
313
+ status=OuterboundsCommandStatus.FAIL,
314
+ reason="Failed to fetch perimeters from API",
315
+ mitigation="",
316
+ )
317
+ command_response.add_step(command_step)
318
+ if output == "json":
319
+ click.echo(json.dumps(command_response.as_dict(), indent=4))
320
+ sys.exit(1)
321
+ return {p["perimeter"]: p for p in perimeters}
322
+
323
+
324
+ def get_ob_config_or_fail_command(
325
+ config_dir: str,
326
+ profile: str,
327
+ output: str,
328
+ command_response: OuterboundsCommandResponse,
329
+ command_step: CommandStatus,
330
+ ) -> Dict[str, str]:
331
+ path_to_config = get_ob_config_file_path(config_dir, profile)
332
+
333
+ if not os.path.exists(path_to_config):
334
+ click.secho(
335
+ "Config file not found at {}".format(path_to_config), fg="red", err=True
336
+ )
337
+ command_step.update(
338
+ status=OuterboundsCommandStatus.FAIL,
339
+ reason="Config file not found",
340
+ mitigation="Please make sure the config file exists at {}".format(
341
+ path_to_config
342
+ ),
343
+ )
344
+ command_response.add_step(command_step)
345
+ if output == "json":
346
+ click.echo(json.dumps(command_response.as_dict(), indent=4))
347
+ sys.exit(1)
348
+
349
+ with open(path_to_config, "r") as file:
350
+ ob_config_dict = json.load(file)
351
+
352
+ if "OB_CURRENT_PERIMETER" not in ob_config_dict:
353
+ click.secho(
354
+ "OB_CURRENT_PERIMETER not found in Config file: {}".format(path_to_config),
355
+ fg="red",
356
+ err=True,
357
+ )
358
+ command_step.update(
359
+ status=OuterboundsCommandStatus.FAIL,
360
+ reason="OB_CURRENT_PERIMETER not found in Config file: {}",
361
+ mitigation="",
362
+ )
363
+ command_response.add_step(command_step)
364
+ if output == "json":
365
+ click.echo(json.dumps(command_response.as_dict(), indent=4))
366
+ sys.exit(1)
367
+
368
+ return ob_config_dict
369
+
370
+
371
+ def confirm_user_has_access_to_perimeter_or_fail(
372
+ perimeter_id: str,
373
+ perimeters: Dict[str, Any],
374
+ output: str,
375
+ command_response: OuterboundsCommandResponse,
376
+ command_step: CommandStatus,
377
+ ):
378
+ if perimeter_id not in perimeters:
379
+ click.secho(
380
+ f"You do not have access to perimeter {perimeter_id} or it does not exist.",
381
+ fg="red",
382
+ err=True,
383
+ )
384
+ command_step.update(
385
+ status=OuterboundsCommandStatus.FAIL,
386
+ reason=f"You do not have access to perimeter {perimeter_id} or it does not exist.",
387
+ mitigation="",
388
+ )
389
+ command_response.add_step(command_step)
390
+ if output == "json":
391
+ click.echo(json.dumps(command_response.as_dict(), indent=4))
392
+ sys.exit(1)
@@ -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(
@@ -110,7 +114,6 @@ def configure_cloud_workstation(config_dir=None, profile=None, binary=None, outp
110
114
  kubeconfig_configure_step = CommandStatus(
111
115
  "ConfigureKubeConfig", OuterboundsCommandStatus.OK, "Kubeconfig is configured"
112
116
  )
113
-
114
117
  try:
115
118
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
116
119
  config_dir, profile
@@ -191,10 +194,25 @@ def configure_cloud_workstation(config_dir=None, profile=None, binary=None, outp
191
194
  @click.option(
192
195
  "-p",
193
196
  "--profile",
194
- default="",
197
+ default=os.environ.get("METAFLOW_PROFILE", ""),
195
198
  help="The named metaflow profile in which your workstation exists",
196
199
  )
197
- def list_workstations(config_dir=None, profile=None):
200
+ @click.option(
201
+ "-o",
202
+ "--output",
203
+ default="json",
204
+ help="Show output in the specified format.",
205
+ type=click.Choice(["json"]),
206
+ )
207
+ def list_workstations(config_dir=None, profile=None, output="json"):
208
+ list_response = OuterboundsCommandResponse()
209
+ list_step = CommandStatus(
210
+ "listWorkstations",
211
+ OuterboundsCommandStatus.OK,
212
+ "Workstation list successfully fetched!",
213
+ )
214
+ list_response.add_or_update_data("workstations", [])
215
+
198
216
  try:
199
217
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
200
218
  config_dir, profile
@@ -205,17 +223,23 @@ def list_workstations(config_dir=None, profile=None):
205
223
  workstations_response = requests.get(
206
224
  f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
207
225
  )
208
- try:
209
- workstations_response.raise_for_status()
210
- click.echo(json.dumps(workstations_response.json(), indent=4))
211
- except HTTPError:
212
- click.secho("Failed to generate workstation token.", fg="red")
213
- click.secho(
214
- "Error: {}".format(json.dumps(workstations_response.json(), indent=4))
215
- )
226
+ workstations_response.raise_for_status()
227
+ list_response.add_or_update_data(
228
+ "workstations", workstations_response.json()["workstations"]
229
+ )
230
+ if output == "json":
231
+ click.echo(json.dumps(list_response.as_dict(), indent=4))
216
232
  except Exception as e:
217
- click.secho("Failed to list workstations", fg="red")
218
- click.secho("Error: {}".format(str(e)))
233
+ list_step.update(
234
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations", ""
235
+ )
236
+ list_response.add_step(list_step)
237
+ if output == "json":
238
+ list_response.add_or_update_data("error", str(e))
239
+ click.echo(json.dumps(list_response.as_dict(), indent=4))
240
+ else:
241
+ click.secho("Failed to list workstations", fg="red", err=True)
242
+ click.secho("Error: {}".format(str(e)), fg="red", err=True)
219
243
 
220
244
 
221
245
  @cli.command(help="Hibernate workstation", hidden=True)
@@ -235,7 +259,7 @@ def list_workstations(config_dir=None, profile=None):
235
259
  @click.option(
236
260
  "-w",
237
261
  "--workstation",
238
- default="",
262
+ default=os.environ.get("METAFLOW_PROFILE", ""),
239
263
  help="The ID of the workstation to hibernate",
240
264
  )
241
265
  def hibernate_workstation(config_dir=None, profile=None, workstation=None):
@@ -243,6 +267,8 @@ def hibernate_workstation(config_dir=None, profile=None, workstation=None):
243
267
  click.secho("Please specify a workstation ID", fg="red")
244
268
  return
245
269
  try:
270
+ if not profile:
271
+ profile = metaflowconfig.get_metaflow_profile()
246
272
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
247
273
  config_dir, profile
248
274
  )
@@ -267,7 +293,7 @@ def hibernate_workstation(config_dir=None, profile=None, workstation=None):
267
293
  )
268
294
  except Exception as e:
269
295
  click.secho("Failed to hibernate workstation", fg="red")
270
- click.secho("Error: {}".format(str(e)))
296
+ click.secho("Error: {}".format(str(e)), fg="red")
271
297
 
272
298
 
273
299
  @cli.command(help="Restart workstation to the int", hidden=True)
@@ -281,7 +307,7 @@ def hibernate_workstation(config_dir=None, profile=None, workstation=None):
281
307
  @click.option(
282
308
  "-p",
283
309
  "--profile",
284
- default="",
310
+ default=os.environ.get("METAFLOW_PROFILE", ""),
285
311
  help="The named metaflow profile in which your workstation exists",
286
312
  )
287
313
  @click.option(
@@ -319,7 +345,7 @@ def restart_workstation(config_dir=None, profile=None, workstation=None):
319
345
  )
320
346
  except Exception as e:
321
347
  click.secho("Failed to restart workstation", fg="red")
322
- click.secho("Error: {}".format(str(e)))
348
+ click.secho("Error: {}".format(str(e)), fg="red")
323
349
 
324
350
 
325
351
  @cli.command(help="Install dependencies needed by workstations", hidden=True)
@@ -486,8 +512,85 @@ def add_to_path(program_path, platform):
486
512
  with open(path_to_rc_file, "a+") as f: # Open bashrc file
487
513
  if program_path not in f.read():
488
514
  f.write("\n# Added by Outerbounds\n")
489
- f.write(program_path)
515
+ f.write(f"export PATH=$PATH:{program_path}")
490
516
 
491
517
 
492
518
  def to_windows_path(path):
493
519
  return os.path.normpath(path).replace(os.sep, "\\")
520
+
521
+
522
+ @cli.command(help="Show relevant links for a deployment & perimeter", hidden=True)
523
+ @click.option(
524
+ "-d",
525
+ "--config-dir",
526
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
527
+ help="Path to Metaflow configuration directory",
528
+ show_default=True,
529
+ )
530
+ @click.option(
531
+ "-p",
532
+ "--profile",
533
+ default="",
534
+ help="The named metaflow profile in which your workstation exists",
535
+ )
536
+ @click.option(
537
+ "--perimeter-id",
538
+ default="",
539
+ help="The id of the perimeter to use",
540
+ )
541
+ @click.option(
542
+ "-o",
543
+ "--output",
544
+ default="",
545
+ help="Show output in the specified format.",
546
+ type=click.Choice(["json", ""]),
547
+ )
548
+ def show_relevant_links(config_dir=None, profile=None, perimeter_id="", output=""):
549
+ show_links_response = OuterboundsCommandResponse()
550
+ show_links_step = CommandStatus(
551
+ "showRelevantLinks",
552
+ OuterboundsCommandStatus.OK,
553
+ "Relevant links successfully fetched!",
554
+ )
555
+ show_links_response.add_or_update_data("links", [])
556
+ links = []
557
+ try:
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
+
576
+ links.append(
577
+ {
578
+ "id": "metaflow-ui-url",
579
+ "url": metaflow_config["METAFLOW_UI_URL"],
580
+ "label": "Metaflow UI URL",
581
+ }
582
+ )
583
+ show_links_response.add_or_update_data("links", links)
584
+ if output == "json":
585
+ click.echo(json.dumps(show_links_response.as_dict(), indent=4))
586
+ except Exception as e:
587
+ show_links_step.update(
588
+ OuterboundsCommandStatus.FAIL, "Failed to show relevant links", ""
589
+ )
590
+ show_links_response.add_step(show_links_step)
591
+ if output == "json":
592
+ show_links_response.add_or_update_data("error", str(e))
593
+ click.echo(json.dumps(show_links_response.as_dict(), indent=4))
594
+ else:
595
+ click.secho("Failed to show relevant links", fg="red", err=True)
596
+ click.secho("Error: {}".format(str(e)), fg="red", err=True)
@@ -1,11 +1,42 @@
1
1
  import json
2
2
  import os
3
3
  import requests
4
+ from os import path
5
+ import requests
6
+
7
+
8
+ def init_config(config_dir="", profile="") -> dict:
9
+ config = read_metaflow_config_from_filesystem(config_dir, profile)
10
+
11
+ # This is new remote-metaflow config; fetch it from the URL
12
+ if "OBP_METAFLOW_CONFIG_URL" in config:
13
+ remote_config = init_config_from_url(
14
+ config_dir, profile, config["OBP_METAFLOW_CONFIG_URL"]
15
+ )
16
+ remote_config["OBP_METAFLOW_CONFIG_URL"] = config["OBP_METAFLOW_CONFIG_URL"]
17
+ return remote_config
18
+ # Legacy config, use from filesystem
19
+ return config
20
+
21
+
22
+ def init_config_from_url(config_dir, profile, url) -> dict:
23
+ config = read_metaflow_config_from_filesystem(config_dir, profile)
24
+
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
+ 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
+ return remote_config
4
35
 
5
36
 
6
- def init_config() -> dict:
7
- profile = os.environ.get("METAFLOW_PROFILE")
8
- config_dir = os.path.expanduser(
37
+ def read_metaflow_config_from_filesystem(config_dir="", profile="") -> dict:
38
+ profile = profile or os.environ.get("METAFLOW_PROFILE")
39
+ config_dir = config_dir or os.path.expanduser(
9
40
  os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
10
41
  )
11
42
 
@@ -17,22 +48,6 @@ def init_config() -> dict:
17
48
  config = json.load(json_file)
18
49
  else:
19
50
  raise Exception("Unable to locate metaflow config at '%s')" % (path_to_config))
20
-
21
- # This is new remote-metaflow config; fetch it from the URL
22
- if "OBP_METAFLOW_CONFIG_URL" in config:
23
- if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
24
- raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
25
-
26
- config_response = requests.get(
27
- config["OBP_METAFLOW_CONFIG_URL"],
28
- headers={"x-api-key": f'{config["METAFLOW_SERVICE_AUTH_KEY"]}'},
29
- )
30
- config_response.raise_for_status()
31
- remote_config = config_response.json()["config"]
32
- remote_config["METAFLOW_SERVICE_AUTH_KEY"] = config["METAFLOW_SERVICE_AUTH_KEY"]
33
- return remote_config
34
-
35
- # Legacy config, use from filesystem
36
51
  return config
37
52
 
38
53
 
@@ -44,13 +59,10 @@ def get_metaflow_token_from_config(config_dir: str, profile: str) -> str:
44
59
  config_dir (str): Path to the config directory
45
60
  profile (str): The named metaflow profile
46
61
  """
47
- config_filename = f"config_{profile}.json" if profile else "config.json"
48
- config_path = os.path.join(config_dir, config_filename)
49
- with open(config_path) as json_file:
50
- config = json.load(json_file)
51
- if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
52
- raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
53
- return config["METAFLOW_SERVICE_AUTH_KEY"]
62
+ config = init_config(config_dir, profile)
63
+ if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
64
+ raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
65
+ return config["METAFLOW_SERVICE_AUTH_KEY"]
54
66
 
55
67
 
56
68
  def get_sanitized_url_from_config(config_dir: str, profile: str, key: str) -> str:
@@ -62,16 +74,12 @@ def get_sanitized_url_from_config(config_dir: str, profile: str, key: str) -> st
62
74
  profile (str): The named metaflow profile
63
75
  key (str): The key to look up in the config file
64
76
  """
65
- config_filename = f"config_{profile}.json" if profile else "config.json"
66
- config_path = os.path.join(config_dir, config_filename)
67
-
68
- with open(config_path) as json_file:
69
- config = json.load(json_file)
70
- if key not in config:
71
- raise Exception(f"Key {key} not found in config file {config_path}")
72
- url_in_config = config[key]
73
- if not url_in_config.startswith("https://"):
74
- url_in_config = f"https://{url_in_config}"
75
-
76
- url_in_config = url_in_config.rstrip("/")
77
- return url_in_config
77
+ config = init_config(config_dir, profile)
78
+ if key not in config:
79
+ raise Exception(f"Key {key} not found in config")
80
+ url_in_config = config[key]
81
+ if not url_in_config.startswith("https://"):
82
+ url_in_config = f"https://{url_in_config}"
83
+
84
+ url_in_config = url_in_config.rstrip("/")
85
+ return url_in_config
@@ -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:
@@ -37,10 +38,19 @@ class OuterboundsCommandResponse:
37
38
  self._message = ""
38
39
  self._steps = []
39
40
  self.metadata = {}
41
+ self._data = {}
42
+
43
+ def update(self, status, code, message):
44
+ self.status = status
45
+ self._code = code
46
+ self._message = message
40
47
 
41
48
  def add_or_update_metadata(self, key, value):
42
49
  self.metadata[key] = value
43
50
 
51
+ def add_or_update_data(self, key, value):
52
+ self._data[key] = value
53
+
44
54
  def add_step(self, step: CommandStatus):
45
55
  self._steps.append(step)
46
56
  self._process_step_status(step)
@@ -59,10 +69,11 @@ class OuterboundsCommandResponse:
59
69
  self._message = "We found one or more warnings with your installation."
60
70
 
61
71
  def as_dict(self):
72
+ self._data["steps"] = [step.as_dict() for step in self._steps]
62
73
  return {
63
74
  "status": self.status.value,
64
75
  "code": self._code,
65
76
  "message": self._message,
66
- "steps": [step.as_dict() for step in self._steps],
67
77
  "metadata": self.metadata,
78
+ "data": self._data,
68
79
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.56
3
+ Version: 0.3.57rc0
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -24,8 +24,7 @@ Requires-Dist: google-api-core (>=2.16.1,<3.0.0) ; extra == "gcp"
24
24
  Requires-Dist: google-auth (>=2.27.0,<3.0.0) ; extra == "gcp"
25
25
  Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
26
26
  Requires-Dist: ob-metaflow (==2.11.4.1)
27
- Requires-Dist: ob-metaflow-extensions (==1.1.46)
28
- Requires-Dist: ob-metaflow-stubs (==2.11.4.1)
27
+ Requires-Dist: ob-metaflow-extensions (==1.1.47rc0)
29
28
  Requires-Dist: opentelemetry-distro (==0.41b0)
30
29
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.20.0)
31
30
  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=KKxcSRCysnRQv5Eh22jCeROVTr4yit0iQ3MSvcfP2Xs,29643
6
+ outerbounds/command_groups/perimeters_cli.py,sha256=HfmPa0LSu1YwauQrZ1sPI7kdIkqtlCxWQcHfBmmq3E8,12661
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=pXm7-b5UpSdWIN7AMIq8jIa8zB7QpXPpFSsAlaSZNEM,3014
11
+ outerbounds/utils/schema.py,sha256=cNlgjmteLPbDzSEUSQDsq8txdhMGyezSmM83jU3aa0w,2329
12
+ outerbounds-0.3.57rc0.dist-info/METADATA,sha256=REMwidDMNfoUzcaVTitN1qLzmIbK1EQPere7vpbSVFQ,1367
13
+ outerbounds-0.3.57rc0.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
14
+ outerbounds-0.3.57rc0.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
15
+ outerbounds-0.3.57rc0.dist-info/RECORD,,
@@ -1,14 +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=61VsBlPG2ykP_786eCyllqeM8DMhPAOfj2FhktrSd7k,207
5
- outerbounds/command_groups/local_setup_cli.py,sha256=g_kkrlDGzYvZTm184pW6QwotpkcqBamB14kH_Kv8TbM,28685
6
- outerbounds/command_groups/workstations_cli.py,sha256=VgydQzCas3mlAFyzZuanjl1E8Zh7pBrbKbbP6t6N2WU,18237
7
- outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- outerbounds/utils/kubeconfig.py,sha256=l1mUP1j9VIq3fsffi5bJ1Nk-hYlwd1dIqkpj7DvVS1E,7936
9
- outerbounds/utils/metaflowconfig.py,sha256=6u9D4x-pQVCPKnmGkTg9uSSHrq4mGnWQl7TurwyV2e8,2945
10
- outerbounds/utils/schema.py,sha256=nBuarFbdZu0LGhG0YkJ6pEIvdglfM_TO_W_Db2vksb0,2017
11
- outerbounds-0.3.56.dist-info/METADATA,sha256=iYALnqAuZVDKxhiQf02jv_XGCv1NVBnCcHX1MHPn4gw,1407
12
- outerbounds-0.3.56.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
13
- outerbounds-0.3.56.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
14
- outerbounds-0.3.56.dist-info/RECORD,,