outerbounds 0.3.55rc3__py3-none-any.whl → 0.3.133__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.
- outerbounds/_vendor/PyYAML.LICENSE +20 -0
- outerbounds/_vendor/__init__.py +0 -0
- outerbounds/_vendor/_yaml/__init__.py +34 -0
- outerbounds/_vendor/click/__init__.py +73 -0
- outerbounds/_vendor/click/_compat.py +626 -0
- outerbounds/_vendor/click/_termui_impl.py +717 -0
- outerbounds/_vendor/click/_textwrap.py +49 -0
- outerbounds/_vendor/click/_winconsole.py +279 -0
- outerbounds/_vendor/click/core.py +2998 -0
- outerbounds/_vendor/click/decorators.py +497 -0
- outerbounds/_vendor/click/exceptions.py +287 -0
- outerbounds/_vendor/click/formatting.py +301 -0
- outerbounds/_vendor/click/globals.py +68 -0
- outerbounds/_vendor/click/parser.py +529 -0
- outerbounds/_vendor/click/py.typed +0 -0
- outerbounds/_vendor/click/shell_completion.py +580 -0
- outerbounds/_vendor/click/termui.py +787 -0
- outerbounds/_vendor/click/testing.py +479 -0
- outerbounds/_vendor/click/types.py +1073 -0
- outerbounds/_vendor/click/utils.py +580 -0
- outerbounds/_vendor/click.LICENSE +28 -0
- outerbounds/_vendor/vendor_any.txt +2 -0
- outerbounds/_vendor/yaml/__init__.py +471 -0
- outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
- outerbounds/_vendor/yaml/composer.py +146 -0
- outerbounds/_vendor/yaml/constructor.py +862 -0
- outerbounds/_vendor/yaml/cyaml.py +177 -0
- outerbounds/_vendor/yaml/dumper.py +138 -0
- outerbounds/_vendor/yaml/emitter.py +1239 -0
- outerbounds/_vendor/yaml/error.py +94 -0
- outerbounds/_vendor/yaml/events.py +104 -0
- outerbounds/_vendor/yaml/loader.py +62 -0
- outerbounds/_vendor/yaml/nodes.py +51 -0
- outerbounds/_vendor/yaml/parser.py +629 -0
- outerbounds/_vendor/yaml/reader.py +208 -0
- outerbounds/_vendor/yaml/representer.py +378 -0
- outerbounds/_vendor/yaml/resolver.py +245 -0
- outerbounds/_vendor/yaml/scanner.py +1555 -0
- outerbounds/_vendor/yaml/serializer.py +127 -0
- outerbounds/_vendor/yaml/tokens.py +129 -0
- outerbounds/command_groups/apps_cli.py +450 -0
- outerbounds/command_groups/cli.py +9 -5
- outerbounds/command_groups/local_setup_cli.py +249 -33
- outerbounds/command_groups/perimeters_cli.py +231 -33
- outerbounds/command_groups/tutorials_cli.py +111 -0
- outerbounds/command_groups/workstations_cli.py +88 -15
- outerbounds/utils/kubeconfig.py +2 -2
- outerbounds/utils/metaflowconfig.py +111 -21
- outerbounds/utils/schema.py +8 -2
- outerbounds/utils/utils.py +19 -0
- outerbounds/vendor.py +159 -0
- {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/METADATA +17 -6
- outerbounds-0.3.133.dist-info/RECORD +59 -0
- {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/WHEEL +1 -1
- outerbounds-0.3.55rc3.dist-info/RECORD +0 -15
- {outerbounds-0.3.55rc3.dist-info → outerbounds-0.3.133.dist-info}/entry_points.txt +0 -0
| @@ -1,36 +1,35 @@ | |
| 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  | 
| 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  | 
| 13 | 
            -
            from  | 
| 14 | 
            -
             | 
| 15 | 
            -
            import boto3
         | 
| 16 | 
            -
            import click
         | 
| 6 | 
            +
            from typing import Any, Dict
         | 
| 7 | 
            +
            from outerbounds._vendor import click
         | 
| 17 8 | 
             
            import requests
         | 
| 18 | 
            -
             | 
| 9 | 
            +
            import configparser
         | 
| 19 10 |  | 
| 20 | 
            -
            from ..utils import  | 
| 11 | 
            +
            from ..utils import metaflowconfig
         | 
| 12 | 
            +
            from ..utils.utils import safe_write_to_disk
         | 
| 21 13 | 
             
            from ..utils.schema import (
         | 
| 22 14 | 
             
                CommandStatus,
         | 
| 23 15 | 
             
                OuterboundsCommandResponse,
         | 
| 24 16 | 
             
                OuterboundsCommandStatus,
         | 
| 25 17 | 
             
            )
         | 
| 26 18 |  | 
| 19 | 
            +
            from .local_setup_cli import PERIMETER_CONFIG_URL_KEY
         | 
| 20 | 
            +
             | 
| 27 21 |  | 
| 28 22 | 
             
            @click.group()
         | 
| 29 23 | 
             
            def cli(**kwargs):
         | 
| 30 24 | 
             
                pass
         | 
| 31 25 |  | 
| 32 26 |  | 
| 33 | 
            -
            @ | 
| 27 | 
            +
            @click.group(help="Manage perimeters")
         | 
| 28 | 
            +
            def perimeter(**kwargs):
         | 
| 29 | 
            +
                pass
         | 
| 30 | 
            +
             | 
| 31 | 
            +
             | 
| 32 | 
            +
            @perimeter.command(help="Switch current perimeter")
         | 
| 34 33 | 
             
            @click.option(
         | 
| 35 34 | 
             
                "-d",
         | 
| 36 35 | 
             
                "--config-dir",
         | 
| @@ -59,7 +58,7 @@ def cli(**kwargs): | |
| 59 58 | 
             
                help="Force change the existing perimeter",
         | 
| 60 59 | 
             
                default=False,
         | 
| 61 60 | 
             
            )
         | 
| 62 | 
            -
            def  | 
| 61 | 
            +
            def switch(config_dir=None, profile=None, output="", id=None, force=False):
         | 
| 63 62 | 
             
                switch_perimeter_response = OuterboundsCommandResponse()
         | 
| 64 63 |  | 
| 65 64 | 
             
                switch_perimeter_step = CommandStatus(
         | 
| @@ -75,7 +74,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa | |
| 75 74 | 
             
                    id, perimeters, output, switch_perimeter_response, switch_perimeter_step
         | 
| 76 75 | 
             
                )
         | 
| 77 76 |  | 
| 78 | 
            -
                path_to_config = get_ob_config_file_path(config_dir, profile)
         | 
| 77 | 
            +
                path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
         | 
| 79 78 |  | 
| 80 79 | 
             
                import fcntl
         | 
| 81 80 |  | 
| @@ -94,7 +93,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa | |
| 94 93 |  | 
| 95 94 | 
             
                    ob_config_dict = {
         | 
| 96 95 | 
             
                        "OB_CURRENT_PERIMETER": str(id),
         | 
| 97 | 
            -
                         | 
| 96 | 
            +
                        PERIMETER_CONFIG_URL_KEY: perimeters[id]["remote_config_url"],
         | 
| 98 97 | 
             
                    }
         | 
| 99 98 |  | 
| 100 99 | 
             
                    # Now that we have the lock, we can safely write to the file
         | 
| @@ -119,12 +118,34 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa | |
| 119 118 | 
             
                    )
         | 
| 120 119 |  | 
| 121 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 | 
            +
             | 
| 122 144 | 
             
                if output == "json":
         | 
| 123 145 | 
             
                    click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
         | 
| 124 | 
            -
                    return
         | 
| 125 146 |  | 
| 126 147 |  | 
| 127 | 
            -
            @ | 
| 148 | 
            +
            @perimeter.command(help="Show current perimeter")
         | 
| 128 149 | 
             
            @click.option(
         | 
| 129 150 | 
             
                "-d",
         | 
| 130 151 | 
             
                "--config-dir",
         | 
| @@ -146,7 +167,7 @@ def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=Fa | |
| 146 167 | 
             
                help="Show output in the specified format.",
         | 
| 147 168 | 
             
                type=click.Choice(["json", ""]),
         | 
| 148 169 | 
             
            )
         | 
| 149 | 
            -
            def  | 
| 170 | 
            +
            def show_current(config_dir=None, profile=None, output=""):
         | 
| 150 171 | 
             
                show_current_perimeter_response = OuterboundsCommandResponse()
         | 
| 151 172 |  | 
| 152 173 | 
             
                show_current_perimeter_step = CommandStatus(
         | 
| @@ -192,7 +213,7 @@ def show_current_perimeter(config_dir=None, profile=None, output=""): | |
| 192 213 | 
             
                    click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
         | 
| 193 214 |  | 
| 194 215 |  | 
| 195 | 
            -
            @ | 
| 216 | 
            +
            @perimeter.command(help="List all available perimeters")
         | 
| 196 217 | 
             
            @click.option(
         | 
| 197 218 | 
             
                "-d",
         | 
| 198 219 | 
             
                "--config-dir",
         | 
| @@ -213,13 +234,29 @@ def show_current_perimeter(config_dir=None, profile=None, output=""): | |
| 213 234 | 
             
                help="Show output in the specified format.",
         | 
| 214 235 | 
             
                type=click.Choice(["json", ""]),
         | 
| 215 236 | 
             
            )
         | 
| 216 | 
            -
            def  | 
| 237 | 
            +
            def list(config_dir=None, profile=None, output=""):
         | 
| 217 238 | 
             
                list_perimeters_response = OuterboundsCommandResponse()
         | 
| 218 239 |  | 
| 219 240 | 
             
                list_perimeters_step = CommandStatus(
         | 
| 220 241 | 
             
                    "ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
         | 
| 221 242 | 
             
                )
         | 
| 222 243 |  | 
| 244 | 
            +
                if "WORKSTATION_ID" in os.environ and (
         | 
| 245 | 
            +
                    "OBP_DEFAULT_PERIMETER" not in os.environ
         | 
| 246 | 
            +
                    or "OBP_DEFAULT_PERIMETER_URL" not in os.environ
         | 
| 247 | 
            +
                ):
         | 
| 248 | 
            +
                    list_perimeters_response.update(
         | 
| 249 | 
            +
                        OuterboundsCommandStatus.NOT_SUPPORTED,
         | 
| 250 | 
            +
                        500,
         | 
| 251 | 
            +
                        "Perimeters are not supported on old workstations.",
         | 
| 252 | 
            +
                    )
         | 
| 253 | 
            +
                    click.secho(
         | 
| 254 | 
            +
                        "Perimeters are not supported on old workstations.", err=True, fg="red"
         | 
| 255 | 
            +
                    )
         | 
| 256 | 
            +
                    if output == "json":
         | 
| 257 | 
            +
                        click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
         | 
| 258 | 
            +
                    return
         | 
| 259 | 
            +
             | 
| 223 260 | 
             
                ob_config_dict = get_ob_config_or_fail_command(
         | 
| 224 261 | 
             
                    config_dir, profile, output, list_perimeters_response, list_perimeters_step
         | 
| 225 262 | 
             
                )
         | 
| @@ -254,6 +291,58 @@ def list_perimeters(config_dir=None, profile=None, output=""): | |
| 254 291 | 
             
                    click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
         | 
| 255 292 |  | 
| 256 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 | 
            +
             | 
| 257 346 | 
             
            def get_list_perimeters_api_response(config_dir, profile):
         | 
| 258 347 | 
             
                metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
         | 
| 259 348 | 
             
                api_url = metaflowconfig.get_sanitized_url_from_config(
         | 
| @@ -267,15 +356,6 @@ def get_list_perimeters_api_response(config_dir, profile): | |
| 267 356 | 
             
                return perimeters_response.json()["perimeters"]
         | 
| 268 357 |  | 
| 269 358 |  | 
| 270 | 
            -
            def get_ob_config_file_path(config_dir: str, profile: str) -> str:
         | 
| 271 | 
            -
                # If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
         | 
| 272 | 
            -
                # If neither are set, use ~/.metaflowconfig
         | 
| 273 | 
            -
                obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
         | 
| 274 | 
            -
             | 
| 275 | 
            -
                ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
         | 
| 276 | 
            -
                return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
         | 
| 277 | 
            -
             | 
| 278 | 
            -
             | 
| 279 359 | 
             
            def get_perimeters_from_api_or_fail_command(
         | 
| 280 360 | 
             
                config_dir: str,
         | 
| 281 361 | 
             
                profile: str,
         | 
| @@ -310,7 +390,7 @@ def get_ob_config_or_fail_command( | |
| 310 390 | 
             
                command_response: OuterboundsCommandResponse,
         | 
| 311 391 | 
             
                command_step: CommandStatus,
         | 
| 312 392 | 
             
            ) -> Dict[str, str]:
         | 
| 313 | 
            -
                path_to_config = get_ob_config_file_path(config_dir, profile)
         | 
| 393 | 
            +
                path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
         | 
| 314 394 |  | 
| 315 395 | 
             
                if not os.path.exists(path_to_config):
         | 
| 316 396 | 
             
                    click.secho(
         | 
| @@ -350,6 +430,23 @@ def get_ob_config_or_fail_command( | |
| 350 430 | 
             
                return ob_config_dict
         | 
| 351 431 |  | 
| 352 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 | 
            +
             | 
| 353 450 | 
             
            def confirm_user_has_access_to_perimeter_or_fail(
         | 
| 354 451 | 
             
                perimeter_id: str,
         | 
| 355 452 | 
             
                perimeters: Dict[str, Any],
         | 
| @@ -372,3 +469,104 @@ def confirm_user_has_access_to_perimeter_or_fail( | |
| 372 469 | 
             
                    if output == "json":
         | 
| 373 470 | 
             
                        click.echo(json.dumps(command_response.as_dict(), indent=4))
         | 
| 374 471 | 
             
                    sys.exit(1)
         | 
| 472 | 
            +
             | 
| 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 | 
            +
                aws_config["profile task"] = {
         | 
| 522 | 
            +
                    "role_arn": token_info["role_arn"],
         | 
| 523 | 
            +
                    "web_identity_token_file": token_file_loc,
         | 
| 524 | 
            +
                }
         | 
| 525 | 
            +
             | 
| 526 | 
            +
                if token_info.get("cspr_role_arn"):
         | 
| 527 | 
            +
                    # If CSPR role is present, then we need to use the task role (in the task profile)
         | 
| 528 | 
            +
                    # to assume the CSPR role.
         | 
| 529 | 
            +
                    aws_config["profile outerbounds"] = {
         | 
| 530 | 
            +
                        "role_arn": token_info["cspr_role_arn"],
         | 
| 531 | 
            +
                        "source_profile": "task",
         | 
| 532 | 
            +
                    }
         | 
| 533 | 
            +
                else:
         | 
| 534 | 
            +
                    # If no CSPR role is present, just use the task profile as the outerbounds profile.
         | 
| 535 | 
            +
                    aws_config["profile outerbounds"] = aws_config["profile task"]
         | 
| 536 | 
            +
             | 
| 537 | 
            +
                aws_config_string = StringIO()
         | 
| 538 | 
            +
                aws_config.write(aws_config_string)
         | 
| 539 | 
            +
             | 
| 540 | 
            +
                safe_write_to_disk(token_file_loc, token_info["token"])
         | 
| 541 | 
            +
                safe_write_to_disk(config_file_loc, aws_config_string.getvalue())
         | 
| 542 | 
            +
             | 
| 543 | 
            +
             | 
| 544 | 
            +
            def get_aws_auth_credentials(config_dir, profile):  # pragma: no cover
         | 
| 545 | 
            +
                token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
         | 
| 546 | 
            +
                auth_server_url = metaflowconfig.get_sanitized_url_from_config(
         | 
| 547 | 
            +
                    config_dir, profile, "OBP_AUTH_SERVER"
         | 
| 548 | 
            +
                )
         | 
| 549 | 
            +
             | 
| 550 | 
            +
                response = requests.get(
         | 
| 551 | 
            +
                    "{}/generate/aws".format(auth_server_url), headers={"x-api-key": token}
         | 
| 552 | 
            +
                )
         | 
| 553 | 
            +
                response.raise_for_status()
         | 
| 554 | 
            +
             | 
| 555 | 
            +
                return response.json()
         | 
| 556 | 
            +
             | 
| 557 | 
            +
             | 
| 558 | 
            +
            def get_gcp_auth_credentials(config_dir, profile):
         | 
| 559 | 
            +
                token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
         | 
| 560 | 
            +
                auth_server_url = metaflowconfig.get_sanitized_url_from_config(
         | 
| 561 | 
            +
                    config_dir, profile, "OBP_AUTH_SERVER"
         | 
| 562 | 
            +
                )
         | 
| 563 | 
            +
             | 
| 564 | 
            +
                response = requests.get(
         | 
| 565 | 
            +
                    "{}/generate/gcp".format(auth_server_url), headers={"x-api-key": token}
         | 
| 566 | 
            +
                )
         | 
| 567 | 
            +
                response.raise_for_status()
         | 
| 568 | 
            +
             | 
| 569 | 
            +
                return response.json()
         | 
| 570 | 
            +
             | 
| 571 | 
            +
             | 
| 572 | 
            +
            cli.add_command(perimeter, name="perimeter")
         | 
| @@ -0,0 +1,111 @@ | |
| 1 | 
            +
            import os
         | 
| 2 | 
            +
            from outerbounds._vendor import click
         | 
| 3 | 
            +
            import requests
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            import tarfile
         | 
| 6 | 
            +
            import hashlib
         | 
| 7 | 
            +
            import tempfile
         | 
| 8 | 
            +
             | 
| 9 | 
            +
             | 
| 10 | 
            +
            @click.group()
         | 
| 11 | 
            +
            def cli(**kwargs):
         | 
| 12 | 
            +
                pass
         | 
| 13 | 
            +
             | 
| 14 | 
            +
             | 
| 15 | 
            +
            @click.group(help="Manage tutorials curated by Outerbounds.", hidden=True)
         | 
| 16 | 
            +
            def tutorials(**kwargs):
         | 
| 17 | 
            +
                pass
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 20 | 
            +
            @tutorials.command(help="Pull Outerbounds tutorials.")
         | 
| 21 | 
            +
            @click.option(
         | 
| 22 | 
            +
                "--url",
         | 
| 23 | 
            +
                required=True,
         | 
| 24 | 
            +
                help="URL to pull the tutorials from.",
         | 
| 25 | 
            +
                type=str,
         | 
| 26 | 
            +
            )
         | 
| 27 | 
            +
            @click.option(
         | 
| 28 | 
            +
                "--destination-dir",
         | 
| 29 | 
            +
                help="Show output in the specified format.",
         | 
| 30 | 
            +
                type=str,
         | 
| 31 | 
            +
                required=True,
         | 
| 32 | 
            +
            )
         | 
| 33 | 
            +
            @click.option(
         | 
| 34 | 
            +
                "--force-overwrite",
         | 
| 35 | 
            +
                is_flag=True,
         | 
| 36 | 
            +
                help="Overwrite all existing files across all tutorials.",
         | 
| 37 | 
            +
                type=bool,
         | 
| 38 | 
            +
                required=False,
         | 
| 39 | 
            +
                default=False,
         | 
| 40 | 
            +
            )
         | 
| 41 | 
            +
            def pull(url="", destination_dir="", force_overwrite=False):
         | 
| 42 | 
            +
                try:
         | 
| 43 | 
            +
                    secure_download_and_extract(
         | 
| 44 | 
            +
                        url, destination_dir, force_overwrite=force_overwrite
         | 
| 45 | 
            +
                    )
         | 
| 46 | 
            +
                    click.secho("Tutorials pulled successfully.", fg="green", err=True)
         | 
| 47 | 
            +
                except Exception as e:
         | 
| 48 | 
            +
                    print(e)
         | 
| 49 | 
            +
                    click.secho(f"Failed to pull tutorials: {e}", fg="red", err=True)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
             | 
| 52 | 
            +
            def secure_download_and_extract(
         | 
| 53 | 
            +
                url, dest_dir, expected_hash=None, force_overwrite=False
         | 
| 54 | 
            +
            ):
         | 
| 55 | 
            +
                """
         | 
| 56 | 
            +
                Download a tar.gz file from a URL, verify its integrity, and extract its contents.
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                :param url: URL of the tar.gz file to download
         | 
| 59 | 
            +
                :param dest_dir: Destination directory to extract the contents
         | 
| 60 | 
            +
                :param expected_hash: Expected SHA256 hash of the file (optional)
         | 
| 61 | 
            +
                """
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                with tempfile.TemporaryDirectory() as temp_dir:
         | 
| 64 | 
            +
                    temp_file = os.path.join(
         | 
| 65 | 
            +
                        temp_dir, hashlib.md5(url.encode()).hexdigest() + ".tar.gz"
         | 
| 66 | 
            +
                    )
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    # Download the file
         | 
| 69 | 
            +
                    try:
         | 
| 70 | 
            +
                        response = requests.get(url, stream=True, verify=True)
         | 
| 71 | 
            +
                        response.raise_for_status()
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                        with open(temp_file, "wb") as f:
         | 
| 74 | 
            +
                            for chunk in response.iter_content(chunk_size=8192):
         | 
| 75 | 
            +
                                f.write(chunk)
         | 
| 76 | 
            +
                    except requests.exceptions.RequestException as e:
         | 
| 77 | 
            +
                        raise Exception(f"Failed to download file: {e}")
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    if expected_hash:
         | 
| 80 | 
            +
                        with open(temp_file, "rb") as f:
         | 
| 81 | 
            +
                            file_hash = hashlib.sha256(f.read()).hexdigest()
         | 
| 82 | 
            +
                        if file_hash != expected_hash:
         | 
| 83 | 
            +
                            raise Exception("File integrity check failed")
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    os.makedirs(dest_dir, exist_ok=True)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    try:
         | 
| 88 | 
            +
                        with tarfile.open(temp_file, "r:gz") as tar:
         | 
| 89 | 
            +
                            # Keep track of new journeys to extract.
         | 
| 90 | 
            +
                            to_extract = []
         | 
| 91 | 
            +
                            members = tar.getmembers()
         | 
| 92 | 
            +
                            for member in members:
         | 
| 93 | 
            +
                                member_path = os.path.join(dest_dir, member.name)
         | 
| 94 | 
            +
                                # Check for any files trying to write outside the destination
         | 
| 95 | 
            +
                                if not os.path.abspath(member_path).startswith(
         | 
| 96 | 
            +
                                    os.path.abspath(dest_dir)
         | 
| 97 | 
            +
                                ):
         | 
| 98 | 
            +
                                    raise Exception("Attempted path traversal in tar file")
         | 
| 99 | 
            +
                                if not os.path.exists(member_path):
         | 
| 100 | 
            +
                                    # The user might have modified the existing files, leave them untouched.
         | 
| 101 | 
            +
                                    to_extract.append(member)
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                            if force_overwrite:
         | 
| 104 | 
            +
                                tar.extractall(path=dest_dir)
         | 
| 105 | 
            +
                            else:
         | 
| 106 | 
            +
                                tar.extractall(path=dest_dir, members=to_extract)
         | 
| 107 | 
            +
                    except tarfile.TarError as e:
         | 
| 108 | 
            +
                        raise Exception(f"Failed to extract tar file: {e}")
         | 
| 109 | 
            +
             | 
| 110 | 
            +
             | 
| 111 | 
            +
            cli.add_command(tutorials, name="tutorials")
         | 
| @@ -1,5 +1,5 @@ | |
| 1 | 
            -
            import click
         | 
| 2 | 
            -
            import yaml
         | 
| 1 | 
            +
            from outerbounds._vendor import click
         | 
| 2 | 
            +
            from outerbounds._vendor import yaml
         | 
| 3 3 | 
             
            import requests
         | 
| 4 4 | 
             
            import base64
         | 
| 5 5 | 
             
            import datetime
         | 
| @@ -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(
         | 
| @@ -111,9 +115,6 @@ def configure_cloud_workstation(config_dir=None, profile=None, binary=None, outp | |
| 111 115 | 
             
                    "ConfigureKubeConfig", OuterboundsCommandStatus.OK, "Kubeconfig is configured"
         | 
| 112 116 | 
             
                )
         | 
| 113 117 | 
             
                try:
         | 
| 114 | 
            -
                    if not profile:
         | 
| 115 | 
            -
                        profile = metaflowconfig.get_metaflow_profile()
         | 
| 116 | 
            -
             | 
| 117 118 | 
             
                    metaflow_token = metaflowconfig.get_metaflow_token_from_config(
         | 
| 118 119 | 
             
                        config_dir, profile
         | 
| 119 120 | 
             
                    )
         | 
| @@ -193,7 +194,7 @@ def configure_cloud_workstation(config_dir=None, profile=None, binary=None, outp | |
| 193 194 | 
             
            @click.option(
         | 
| 194 195 | 
             
                "-p",
         | 
| 195 196 | 
             
                "--profile",
         | 
| 196 | 
            -
                default="",
         | 
| 197 | 
            +
                default=os.environ.get("METAFLOW_PROFILE", ""),
         | 
| 197 198 | 
             
                help="The named metaflow profile in which your workstation exists",
         | 
| 198 199 | 
             
            )
         | 
| 199 200 | 
             
            @click.option(
         | 
| @@ -213,8 +214,6 @@ def list_workstations(config_dir=None, profile=None, output="json"): | |
| 213 214 | 
             
                list_response.add_or_update_data("workstations", [])
         | 
| 214 215 |  | 
| 215 216 | 
             
                try:
         | 
| 216 | 
            -
                    if not profile:
         | 
| 217 | 
            -
                        profile = metaflowconfig.get_metaflow_profile()
         | 
| 218 217 | 
             
                    metaflow_token = metaflowconfig.get_metaflow_token_from_config(
         | 
| 219 218 | 
             
                        config_dir, profile
         | 
| 220 219 | 
             
                    )
         | 
| @@ -260,7 +259,7 @@ def list_workstations(config_dir=None, profile=None, output="json"): | |
| 260 259 | 
             
            @click.option(
         | 
| 261 260 | 
             
                "-w",
         | 
| 262 261 | 
             
                "--workstation",
         | 
| 263 | 
            -
                default="",
         | 
| 262 | 
            +
                default=os.environ.get("METAFLOW_PROFILE", ""),
         | 
| 264 263 | 
             
                help="The ID of the workstation to hibernate",
         | 
| 265 264 | 
             
            )
         | 
| 266 265 | 
             
            def hibernate_workstation(config_dir=None, profile=None, workstation=None):
         | 
| @@ -308,7 +307,7 @@ def hibernate_workstation(config_dir=None, profile=None, workstation=None): | |
| 308 307 | 
             
            @click.option(
         | 
| 309 308 | 
             
                "-p",
         | 
| 310 309 | 
             
                "--profile",
         | 
| 311 | 
            -
                default="",
         | 
| 310 | 
            +
                default=os.environ.get("METAFLOW_PROFILE", ""),
         | 
| 312 311 | 
             
                help="The named metaflow profile in which your workstation exists",
         | 
| 313 312 | 
             
            )
         | 
| 314 313 | 
             
            @click.option(
         | 
| @@ -322,9 +321,6 @@ def restart_workstation(config_dir=None, profile=None, workstation=None): | |
| 322 321 | 
             
                    click.secho("Please specify a workstation ID", fg="red")
         | 
| 323 322 | 
             
                    return
         | 
| 324 323 | 
             
                try:
         | 
| 325 | 
            -
                    if not profile:
         | 
| 326 | 
            -
                        profile = metaflowconfig.get_metaflow_profile()
         | 
| 327 | 
            -
             | 
| 328 324 | 
             
                    metaflow_token = metaflowconfig.get_metaflow_token_from_config(
         | 
| 329 325 | 
             
                        config_dir, profile
         | 
| 330 326 | 
             
                    )
         | 
| @@ -516,8 +512,85 @@ def add_to_path(program_path, platform): | |
| 516 512 | 
             
                    with open(path_to_rc_file, "a+") as f:  # Open bashrc file
         | 
| 517 513 | 
             
                        if program_path not in f.read():
         | 
| 518 514 | 
             
                            f.write("\n# Added by Outerbounds\n")
         | 
| 519 | 
            -
                            f.write(program_path)
         | 
| 515 | 
            +
                            f.write(f"export PATH=$PATH:{program_path}")
         | 
| 520 516 |  | 
| 521 517 |  | 
| 522 518 | 
             
            def to_windows_path(path):
         | 
| 523 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)
         | 
    
        outerbounds/utils/kubeconfig.py
    CHANGED