From 7fe847d433ec4e35db7d3cccff62339ce9b3867f Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Tue, 31 Oct 2023 10:18:56 +0000 Subject: [PATCH] Add context show command --- data_safe_haven/cli.py | 12 ++-- data_safe_haven/commands/__init__.py | 4 +- data_safe_haven/commands/context.py | 36 +++++----- data_safe_haven/commands/init.py | 67 ------------------- data_safe_haven/config/__init__.py | 2 + ...ackend_settings.py => context_settings.py} | 20 ++++-- tests_/commands/test_context.py | 40 +++++++++++ ...d_settings.py => test_context_settings.py} | 2 +- 8 files changed, 84 insertions(+), 99 deletions(-) delete mode 100644 data_safe_haven/commands/init.py rename data_safe_haven/config/{backend_settings.py => context_settings.py} (91%) create mode 100644 tests_/commands/test_context.py rename tests_/config/{test_backend_settings.py => test_context_settings.py} (99%) diff --git a/data_safe_haven/cli.py b/data_safe_haven/cli.py index ae2a5bdf20..2ab080e1ca 100644 --- a/data_safe_haven/cli.py +++ b/data_safe_haven/cli.py @@ -7,8 +7,8 @@ from data_safe_haven import __version__ from data_safe_haven.commands import ( admin_command_group, + context_command_group, deploy_command_group, - initialise_command, teardown_command_group, ) from data_safe_haven.exceptions import DataSafeHavenError @@ -69,6 +69,11 @@ def main() -> None: name="admin", help="Perform administrative tasks for a Data Safe Haven deployment.", ) + application.add_typer( + context_command_group, + name="context", + help="Manage Data Safe Haven contexts." + ) application.add_typer( deploy_command_group, name="deploy", @@ -80,11 +85,6 @@ def main() -> None: help="Tear down a Data Safe Haven component.", ) - # Register direct subcommands - application.command(name="init", help="Initialise a Data Safe Haven deployment.")( - initialise_command - ) - # Start the application try: application() diff --git a/data_safe_haven/commands/__init__.py b/data_safe_haven/commands/__init__.py index 5ba59d66de..299c982302 100644 --- a/data_safe_haven/commands/__init__.py +++ b/data_safe_haven/commands/__init__.py @@ -1,11 +1,11 @@ from .admin import admin_command_group +from .context import context_command_group from .deploy import deploy_command_group -from .init import initialise_command from .teardown import teardown_command_group __all__ = [ "admin_command_group", + "context_command_group", "deploy_command_group", - "initialise_command", "teardown_command_group", ] diff --git a/data_safe_haven/commands/context.py b/data_safe_haven/commands/context.py index 55aa778435..2ef61e900e 100644 --- a/data_safe_haven/commands/context.py +++ b/data_safe_haven/commands/context.py @@ -2,9 +2,10 @@ from typing import Annotated, Optional import typer +from rich import print from data_safe_haven.backend import Backend -from data_safe_haven.config import BackendSettings +from data_safe_haven.config import ContextSettings from data_safe_haven.exceptions import ( DataSafeHavenError, DataSafeHavenInputError, @@ -14,6 +15,23 @@ context_command_group = typer.Typer() +@context_command_group.command() +def show() -> None: + settings = ContextSettings.from_file() + + current_context_key = settings.selected + current_context = settings.context + available = settings.available + + print(f"Current context: [green]{current_context_key}") + print(f"\tName: {current_context.name}") + print(f"\tAdmin Group ID: {current_context.admin_group_id}") + print(f"\tSubscription name: {current_context.subscription_name}") + print(f"\tLocation: {current_context.location}") + print("\nAvailable contexts:") + print("\n".join(available)) + + @context_command_group.command() def add( admin_group: Annotated[ @@ -50,7 +68,7 @@ def add( ), ] = None, ) -> None: - settings = BackendSettings() + settings = ContextSettings() settings.add( admin_group_id=admin_group, location=location, @@ -64,20 +82,6 @@ def remove() -> None: pass -@context_command_group.command() -def show() -> None: - settings = BackendSettings() - settings.summarise() - - -@context_command_group.command() -def switch( - name: Annotated[str, typer.Argument(help="Name of the context to switch to.")] -) -> None: - settings = BackendSettings() - settings.context = name - - @context_command_group.command() def create() -> None: backend = Backend() # How does this get the config!?! diff --git a/data_safe_haven/commands/init.py b/data_safe_haven/commands/init.py deleted file mode 100644 index 916687975f..0000000000 --- a/data_safe_haven/commands/init.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Command-line application for initialising a Data Safe Haven deployment""" -from typing import Annotated, Optional - -import typer - -from data_safe_haven.backend import Backend -from data_safe_haven.config import BackendSettings -from data_safe_haven.exceptions import DataSafeHavenError -from data_safe_haven.functions import validate_aad_guid - - -def initialise_command( - admin_group: Annotated[ - Optional[str], # noqa: UP007 - typer.Option( - "--admin-group", - "-a", - help="The ID of an Azure group containing all administrators.", - callback=validate_aad_guid, - ), - ] = None, - location: Annotated[ - Optional[str], # noqa: UP007 - typer.Option( - "--location", - "-l", - help="The Azure location to deploy resources into.", - ), - ] = None, - name: Annotated[ - Optional[str], # noqa: UP007 - typer.Option( - "--name", - "-n", - help="The name to give this Data Safe Haven deployment.", - ), - ] = None, - subscription: Annotated[ - Optional[str], # noqa: UP007 - typer.Option( - "--subscription", - "-s", - help="The name of an Azure subscription to deploy resources into.", - ), - ] = None, -) -> None: - """Typer command line entrypoint""" - try: - # Load backend settings and update with command line arguments - settings = BackendSettings() - settings.update( - admin_group_id=admin_group, - location=location, - name=name, - subscription_name=subscription, - ) - - # Ensure that the Pulumi backend exists - backend = Backend() - backend.create() - - # Load the generated configuration file and upload it to blob storage - backend.config.upload() - - except DataSafeHavenError as exc: - msg = f"Could not initialise Data Safe Haven.\n{exc}" - raise DataSafeHavenError(msg) from exc diff --git a/data_safe_haven/config/__init__.py b/data_safe_haven/config/__init__.py index f9c53f80d1..4723c0bf08 100644 --- a/data_safe_haven/config/__init__.py +++ b/data_safe_haven/config/__init__.py @@ -1,5 +1,7 @@ from .config import Config +from .context_settings import ContextSettings __all__ = [ "Config", + "ContextSettings", ] diff --git a/data_safe_haven/config/backend_settings.py b/data_safe_haven/config/context_settings.py similarity index 91% rename from data_safe_haven/config/backend_settings.py rename to data_safe_haven/config/context_settings.py index a608c87e5a..207c4018ca 100644 --- a/data_safe_haven/config/backend_settings.py +++ b/data_safe_haven/config/context_settings.py @@ -1,6 +1,7 @@ """Load global and local settings from dotfiles""" -import pathlib +from pathlib import Path from dataclasses import dataclass +from os import getenv from typing import Any import appdirs @@ -15,10 +16,15 @@ from data_safe_haven.utility import LoggingSingleton -config_directory = pathlib.Path( - appdirs.user_config_dir(appname="data_safe_haven") -).resolve() -config_file_path = config_directory / "config.yaml" +def config_file_path() -> Path: + if config_directory_env := getenv("DSH_CONFIG_DIRECTORY"): + config_directory = Path(config_directory_env).resolve() + else: + config_directory = Path( + appdirs.user_config_dir(appname="data_safe_haven") + ).resolve() + + return config_directory / "contexts.yaml" @dataclass @@ -153,7 +159,7 @@ def remove(self, key: str) -> None: del self.settings["contexts"][key] @classmethod - def from_file(cls, config_file_path: str = config_file_path) -> None: + def from_file(cls, config_file_path: str = config_file_path()) -> None: logger = LoggingSingleton() try: with open(config_file_path, encoding="utf-8") as f_yaml: @@ -170,7 +176,7 @@ def from_file(cls, config_file_path: str = config_file_path) -> None: msg = f"Could not load settings from {config_file_path}.\n{exc}" raise DataSafeHavenConfigError(msg) from exc - def write(self, config_file_path: str = config_file_path) -> None: + def write(self, config_file_path: str = config_file_path()) -> None: """Write settings to YAML file""" # Create the parent directory if it does not exist then write YAML config_file_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests_/commands/test_context.py b/tests_/commands/test_context.py new file mode 100644 index 0000000000..2009d696cb --- /dev/null +++ b/tests_/commands/test_context.py @@ -0,0 +1,40 @@ +from data_safe_haven.commands.context import context_command_group + +from pytest import fixture +from typer.testing import CliRunner + +context_settings = """\ + selected: acme_deployment + contexts: + acme_deployment: + name: Acme Deployment + admin_group_id: d5c5c439-1115-4cb6-ab50-b8e547b6c8dd + location: uksouth + subscription_name: Data Safe Haven (Acme) + gems: + name: Gems + admin_group_id: d5c5c439-1115-4cb6-ab50-b8e547b6c8dd + location: uksouth + subscription_name: Data Safe Haven (Gems)""" + + +@fixture +def tmp_contexts(tmp_path): + config_file_path = tmp_path / "contexts.yaml" + with open(config_file_path, "w") as f: + f.write(context_settings) + return config_file_path + + +@fixture +def runner(tmp_contexts): + runner = CliRunner(env={ + "DSH_CONFIG_DIRECTORY": str(tmp_contexts) + }) + return runner + + +class TestShow: + def test_show(self, runner): + result = runner.invoke(context_command_group, ["show"]) + assert result.exit_code == 0 diff --git a/tests_/config/test_backend_settings.py b/tests_/config/test_context_settings.py similarity index 99% rename from tests_/config/test_backend_settings.py rename to tests_/config/test_context_settings.py index 3ece077dca..09a1dfb338 100644 --- a/tests_/config/test_backend_settings.py +++ b/tests_/config/test_context_settings.py @@ -1,4 +1,4 @@ -from data_safe_haven.config.backend_settings import Context, ContextSettings +from data_safe_haven.config.context_settings import Context, ContextSettings from data_safe_haven.exceptions import DataSafeHavenParameterError import pytest