Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Pydantic for validation and serialisation #1661

Merged
merged 67 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
f3acf72
WIP: Use Pydantic for context settings
JimMadge Nov 7, 2023
12688ea
WIP: Fix ContextSettings tests
JimMadge Nov 7, 2023
562a6b4
Fix context command tests
JimMadge Nov 7, 2023
026f14a
Ensure selected context is defined
JimMadge Nov 7, 2023
aed1a86
Remove schema typings
JimMadge Nov 7, 2023
1abe112
Add test for invalid YAML
JimMadge Nov 13, 2023
7f314d5
Add test for YAML not being a dict
JimMadge Nov 13, 2023
d22bbd1
Add file not found test
JimMadge Nov 13, 2023
502fe5b
Fix linting errors
JimMadge Nov 13, 2023
9093b97
Add Pydantic to linting environment
JimMadge Nov 14, 2023
d374b1c
WIP: Introduce annotated types for validation
JimMadge Nov 15, 2023
7907e0c
WIP: Add Typer validators factory
JimMadge Nov 15, 2023
36f0e1c
Validate Context and ContextSettings on assignment
JimMadge Nov 15, 2023
1136b49
WIP: rewrite Config (and related) as BaseModel
JimMadge Nov 20, 2023
515668e
Update Context tests
JimMadge Nov 21, 2023
a741542
Remove ConfigSectionContext
JimMadge Nov 21, 2023
acb01b8
Improve design of config sections
JimMadge Nov 21, 2023
e7ca180
Add missing tests
JimMadge Nov 22, 2023
e8e2e5b
Add basic tests for Config
JimMadge Nov 22, 2023
8f98a9e
Add to_yaml method
JimMadge Nov 23, 2023
bde5fd4
Add upload and from remote tests
JimMadge Nov 23, 2023
e908111
Run lint:fmt
JimMadge Nov 23, 2023
e44acd6
Tidy Config
JimMadge Nov 23, 2023
6344a92
Correct sre method, add tests
JimMadge Nov 23, 2023
bf9435e
Dynamically derive config items from context
JimMadge Nov 24, 2023
c0002f6
Dynamically construct ConfigSectionTags
JimMadge Nov 24, 2023
bd358dc
Replace tags to_dict with model_dump
JimMadge Nov 24, 2023
e6eb86f
Add missing type annotation
JimMadge Nov 27, 2023
09e8427
Correct config attributes
JimMadge Nov 27, 2023
adbb464
Add missing properties to context
JimMadge Nov 27, 2023
c040e47
Update minimum Python to 3.11
JimMadge Nov 28, 2023
ba05dd7
Correct typer validator factory
JimMadge Nov 29, 2023
e1a0264
Support optional args in typer validator factory
JimMadge Nov 29, 2023
7a0cab8
Move Pulumi encryption key to Config property
JimMadge Nov 28, 2023
4727c32
Add template method
JimMadge Nov 28, 2023
36d05f3
Add config template command
JimMadge Nov 28, 2023
681379e
Add config upload command
JimMadge Nov 29, 2023
82a8d17
Add config show command
JimMadge Nov 29, 2023
b25f977
Run lint:fmt
JimMadge Nov 29, 2023
3235983
Remove dependency on config for context
JimMadge Nov 29, 2023
db7855a
Use new Config in deploy commands
JimMadge Nov 30, 2023
5fe21f8
Update Pulumi version draft README
JimMadge Nov 30, 2023
e6fb404
Use new config in teardown commands
JimMadge Dec 12, 2023
56ec257
Rename context infrastructure class
JimMadge Dec 12, 2023
9fd210c
Correct references
JimMadge Dec 12, 2023
829d7ac
Use new Config in admin commands
JimMadge Dec 12, 2023
16e73ff
Add help text for config show command
JimMadge Dec 14, 2023
c9ceb39
Fix import
JimMadge Dec 15, 2023
9b91daf
Allow no selected context
JimMadge Dec 15, 2023
9cea20e
Add test for constructor with no selected context
JimMadge Dec 15, 2023
c2f3de5
Add assert context tests
JimMadge Dec 15, 2023
4d291a1
Add test for selecting no context
JimMadge Dec 15, 2023
9321eb8
Add update test for no selected context
JimMadge Dec 15, 2023
8576f50
Add test for removing selected context
JimMadge Dec 15, 2023
3486091
Allow showing no selected context
JimMadge Dec 15, 2023
512f386
Add test for available with no selected context
JimMadge Dec 15, 2023
430926b
Add fqdn validation
JimMadge Jan 15, 2024
7d6c50d
Fix invalid fqdn in tests
JimMadge Jan 15, 2024
3990fdc
Fix test failing due to env variable
JimMadge Jan 15, 2024
26e64e0
Merge pull request #1691 from alan-turing-institute/selected
JimMadge Jan 15, 2024
625eb47
Merge pull request #1674 from alan-turing-institute/config
JimMadge Jan 15, 2024
1493dcb
Run lint:fmt
JimMadge Jan 15, 2024
50d83a9
Print Ruff version in CI
JimMadge Jan 15, 2024
672d7df
Fix linting errors
JimMadge Jan 15, 2024
3b5d552
Add fqdn typings
JimMadge Jan 16, 2024
2b9b268
Add typing hint
JimMadge Jan 16, 2024
ed4ec99
Update data_safe_haven/README.md
JimMadge Jan 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/lint_code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ jobs:
python-version: 3.11
- name: Install hatch
run: pip install hatch
- name: Print Ruff version
run: hatch run lint:ruff --version
- name: Lint Python
run: hatch run lint:all

Expand Down
9 changes: 9 additions & 0 deletions data_safe_haven/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@ Install the following requirements before starting

```console
> dsh context add ...
> dsh context switch ...
> dsh context create
JimMadge marked this conversation as resolved.
Show resolved Hide resolved
```

- Create the configuration

```console
> dsh config template --file config.yaml
> vim config.yaml
> dsh config upload config.yaml
```

- Next deploy the Safe Haven Management (SHM) infrastructure [approx 30 minutes]:

```console
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(
) -> None:
super().__init__(*args, **kwargs)
shm_stack = SHMStackManager(config)
self.azure_api = AzureApi(config.subscription_name)
self.azure_api = AzureApi(config.context.subscription_name)
self.logger = LoggingSingleton()
self.resource_group_name = shm_stack.output("domain_controllers")[
"resource_group_name"
Expand Down Expand Up @@ -77,7 +77,7 @@ def add(self, new_users: Sequence[ResearchUser]) -> None:
for line in output.split("\n"):
self.logger.parse(line)

def list(self, sre_name: str | None = None) -> Sequence[ResearchUser]: # noqa: A003
def list(self, sre_name: str | None = None) -> Sequence[ResearchUser]:
"""List users in a local Active Directory"""
list_users_script = FileReader(
self.resources_path / "active_directory" / "list_users.ps1"
Expand Down Expand Up @@ -142,7 +142,7 @@ def remove(self, users: Sequence[ResearchUser]) -> None:
for line in output.split("\n"):
self.logger.parse(line)

def set(self, users: Sequence[ResearchUser]) -> None: # noqa: A003
def set(self, users: Sequence[ResearchUser]) -> None:
"""Set local Active Directory users to specified list"""
users_to_remove = [user for user in self.list() if user not in users]
self.remove(users_to_remove)
Expand Down
4 changes: 2 additions & 2 deletions data_safe_haven/administration/users/azure_ad_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def add(self, new_users: Sequence[ResearchUser]) -> None:
# # Also add the user to the research users group
# self.graph_api.add_user_to_group(user.username, self.researchers_group_name)

def list(self) -> Sequence[ResearchUser]: # noqa: A003
def list(self) -> Sequence[ResearchUser]:
user_list = self.graph_api.read_users()
return [
ResearchUser(
Expand Down Expand Up @@ -105,7 +105,7 @@ def remove(self, users: Sequence[ResearchUser]) -> None:
# )
pass

def set(self, users: Sequence[ResearchUser]) -> None: # noqa: A003
def set(self, users: Sequence[ResearchUser]) -> None:
"""Set Guacamole users to specified list"""
users_to_remove = [user for user in self.list() if user not in users]
self.remove(users_to_remove)
Expand Down
4 changes: 2 additions & 2 deletions data_safe_haven/administration/users/guacamole_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, config: Config, sre_name: str, *args: Any, **kwargs: Any):
sre_stack.secret("password-user-database-admin"),
sre_stack.output("remote_desktop")["connection_db_server_name"],
sre_stack.output("remote_desktop")["resource_group_name"],
config.subscription_name,
config.context.subscription_name,
)
self.users_: Sequence[ResearchUser] | None = None
self.postgres_script_path: pathlib.Path = (
Expand All @@ -30,7 +30,7 @@ def __init__(self, config: Config, sre_name: str, *args: Any, **kwargs: Any):
self.sre_name = sre_name
self.group_name = f"Data Safe Haven SRE {sre_name} Users"

def list(self) -> Sequence[ResearchUser]: # noqa: A003
def list(self) -> Sequence[ResearchUser]:
"""List all Guacamole users"""
if self.users_ is None: # Allow for the possibility of an empty list of users
postgres_output = self.postgres_provisioner.execute_scripts(
Expand Down
4 changes: 2 additions & 2 deletions data_safe_haven/administration/users/user_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get_usernames_guacamole(self, sre_name: str) -> list[str]:
self.logger.error(f"Could not load users for SRE '{sre_name}'.")
return []

def list(self) -> None: # noqa: A003
def list(self) -> None:
"""List Active Directory, AzureAD and Guacamole users

Raises:
Expand Down Expand Up @@ -157,7 +157,7 @@ def remove(self, user_names: Sequence[str]) -> None:
msg = f"Could not remove users: {user_names}.\n{exc}"
raise DataSafeHavenUserHandlingError(msg) from exc

def set(self, users_csv_path: str) -> None: # noqa: A003
def set(self, users_csv_path: str) -> None:
"""Set AzureAD and Guacamole users

Raises:
Expand Down
6 changes: 6 additions & 0 deletions data_safe_haven/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from data_safe_haven import __version__
from data_safe_haven.commands import (
admin_command_group,
config_command_group,
context_command_group,
deploy_command_group,
teardown_command_group,
Expand Down Expand Up @@ -69,6 +70,11 @@ def main() -> None:
name="admin",
help="Perform administrative tasks for a Data Safe Haven deployment.",
)
application.add_typer(
config_command_group,
name="config",
help="Manage Data Safe Haven configuration.",
)
application.add_typer(
context_command_group, name="context", help="Manage Data Safe Haven contexts."
)
Expand Down
2 changes: 2 additions & 0 deletions data_safe_haven/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from .admin import admin_command_group
from .config import config_command_group
from .context import context_command_group
from .deploy import deploy_command_group
from .teardown import teardown_command_group

__all__ = [
"admin_command_group",
"context_command_group",
"config_command_group",
"deploy_command_group",
"teardown_command_group",
]
12 changes: 6 additions & 6 deletions data_safe_haven/commands/admin_add_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
import pathlib

from data_safe_haven.administration.users import UserHandler
from data_safe_haven.config import Config
from data_safe_haven.config import Config, ContextSettings
from data_safe_haven.exceptions import DataSafeHavenError
from data_safe_haven.external import GraphApi


def admin_add_users(csv_path: pathlib.Path) -> None:
"""Add users to a deployed Data Safe Haven"""
shm_name = "UNKNOWN"
try:
# Load config file
config = Config()
shm_name = config.name
context = ContextSettings.from_file().assert_context()
config = Config.from_remote(context)

shm_name = context.shm_name

try:
# Load GraphAPI as this may require user-interaction that is not
# possible as part of a Pulumi declarative command
graph_api = GraphApi(
Expand Down
12 changes: 6 additions & 6 deletions data_safe_haven/commands/admin_list_users.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
"""List users from a deployed Data Safe Haven"""
from data_safe_haven.administration.users import UserHandler
from data_safe_haven.config import Config
from data_safe_haven.config import Config, ContextSettings
from data_safe_haven.exceptions import DataSafeHavenError
from data_safe_haven.external import GraphApi


def admin_list_users() -> None:
"""List users from a deployed Data Safe Haven"""
shm_name = "UNKNOWN"
try:
# Load config file
config = Config()
shm_name = config.name
context = ContextSettings.from_file().assert_context()
config = Config.from_remote(context)

shm_name = context.shm_name

try:
# Load GraphAPI as this may require user-interaction that is not
# possible as part of a Pulumi declarative command
graph_api = GraphApi(
Expand Down
16 changes: 7 additions & 9 deletions data_safe_haven/commands/admin_register_users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Register existing users with a deployed SRE"""
from data_safe_haven.administration.users import UserHandler
from data_safe_haven.config import Config
from data_safe_haven.config import Config, ContextSettings
from data_safe_haven.exceptions import DataSafeHavenError
from data_safe_haven.external import GraphApi
from data_safe_haven.functions import alphanumeric
Expand All @@ -12,16 +12,14 @@ def admin_register_users(
sre: str,
) -> None:
"""Register existing users with a deployed SRE"""
shm_name = "UNKNOWN"
sre_name = "UNKNOWN"
try:
# Use a JSON-safe SRE name
sre_name = alphanumeric(sre).lower()
context = ContextSettings.from_file().assert_context()
config = Config.from_remote(context)

# Load config file
config = Config()
shm_name = config.name
shm_name = context.shm_name
# Use a JSON-safe SRE name
sre_name = alphanumeric(sre).lower()

try:
# Check that SRE option has been provided
if not sre_name:
msg = "SRE name must be specified."
Expand Down
12 changes: 6 additions & 6 deletions data_safe_haven/commands/admin_remove_users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Remove existing users from a deployed Data Safe Haven"""
from data_safe_haven.administration.users import UserHandler
from data_safe_haven.config import Config
from data_safe_haven.config import Config, ContextSettings
from data_safe_haven.exceptions import DataSafeHavenError
from data_safe_haven.external import GraphApi

Expand All @@ -9,12 +9,12 @@ def admin_remove_users(
usernames: list[str],
) -> None:
"""Remove existing users from a deployed Data Safe Haven"""
shm_name = "UNKNOWN"
try:
# Load config file
config = Config()
shm_name = config.name
context = ContextSettings.from_file().assert_context()
config = Config.from_remote(context)

shm_name = context.shm_name

try:
# Load GraphAPI as this may require user-interaction that is not
# possible as part of a Pulumi declarative command
graph_api = GraphApi(
Expand Down
16 changes: 7 additions & 9 deletions data_safe_haven/commands/admin_unregister_users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Unregister existing users from a deployed SRE"""
from data_safe_haven.administration.users import UserHandler
from data_safe_haven.config import Config
from data_safe_haven.config import Config, ContextSettings
from data_safe_haven.exceptions import DataSafeHavenError
from data_safe_haven.external import GraphApi
from data_safe_haven.functions import alphanumeric
Expand All @@ -12,16 +12,14 @@ def admin_unregister_users(
sre: str,
) -> None:
"""Unregister existing users from a deployed SRE"""
shm_name = "UNKNOWN"
sre_name = "UNKNOWN"
try:
# Use a JSON-safe SRE name
sre_name = alphanumeric(sre).lower()
context = ContextSettings.from_file().assert_context()
config = Config.from_remote(context)

# Load config file
config = Config()
shm_name = config.name
shm_name = context.shm_name
# Use a JSON-safe SRE name
sre_name = alphanumeric(sre).lower()

try:
# Check that SRE option has been provided
if not sre_name:
msg = "SRE name must be specified."
Expand Down
47 changes: 47 additions & 0 deletions data_safe_haven/commands/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Command group and entrypoints for managing DSH configuration"""
from pathlib import Path
from typing import Annotated, Optional

import typer
from rich import print

from data_safe_haven.config import Config, ContextSettings

config_command_group = typer.Typer()


@config_command_group.command()
def template(
file: Annotated[
Optional[Path], # noqa: UP007
typer.Option(help="File path to write configuration template to."),
] = None
) -> None:
"""Write a template Data Safe Haven configuration."""
context = ContextSettings.from_file().assert_context()
config = Config.template(context)
if file:
with open(file, "w") as outfile:
outfile.write(config.to_yaml())
else:
print(config.to_yaml())


@config_command_group.command()
def upload(
file: Annotated[Path, typer.Argument(help="Path to configuration file")]
) -> None:
"""Upload a configuration to the Data Safe Haven context"""
context = ContextSettings.from_file().assert_context()
with open(file) as config_file:
config_yaml = config_file.read()
config = Config.from_yaml(context, config_yaml)
config.upload()


@config_command_group.command()
def show() -> None:
"""Print the configuration for the selected Data Safe Haven context"""
context = ContextSettings.from_file().assert_context()
config = Config.from_remote(context)
print(config.to_yaml())
Loading
Loading