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

DEM now presents a warning if the user tries to uninstall an installed Dev Env that requires an unavailable tool image.. #195

Merged
merged 1 commit into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion dem/cli/command/clone_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def handle_existing_local_dev_env(platform: Platform, local_dev_env: DevEnv) ->
typer.confirm("The Dev Env to overwrite is installed. Do you want to uninstall it?",
abort=True)
try:
platform.uninstall_dev_env(local_dev_env)
for status in platform.uninstall_dev_env(local_dev_env):
stdout.print(status)
except PlatformError as e:
stderr.print(f"[red]{str(e)}[/]")
raise typer.Abort()
Expand Down
3 changes: 2 additions & 1 deletion dem/cli/command/create_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ def create_dev_env(platform: Platform, dev_env_name: str) -> None:
typer.confirm("The Development Environment is installed, so it can't be overwritten. " + \
"Uninstall it first?", abort=True)
try:
platform.uninstall_dev_env(dev_env_original)
for status in platform.uninstall_dev_env(dev_env_original):
stdout.print(status)
except PlatformError as e:
stderr.print(f"[red]{str(e)}[/]")
raise typer.Abort()
Expand Down
5 changes: 3 additions & 2 deletions dem/cli/command/delete_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ def execute(platform: Platform, dev_env_name: str) -> None:
abort=True)

try:
platform.uninstall_dev_env(dev_env_to_delete)
for status in platform.uninstall_dev_env(dev_env_to_delete):
stdout.print(status)
except PlatformError as e:
stderr.print(f"[red]{str(e)}[/]")
return

stdout.print("Deleting the Development Environment...")
stdout.print("Deleting the Development Environment descriptor...")
platform.local_dev_envs.remove(dev_env_to_delete)
platform.flush_dev_env_properties()
stdout.print(f"[green]Successfully deleted the {dev_env_name}![/]")
3 changes: 2 additions & 1 deletion dem/cli/command/init_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def execute(platform: Platform, project_path: str) -> None:
abort=True)

try:
platform.uninstall_dev_env(local_dev_env)
for status in platform.uninstall_dev_env(local_dev_env):
stdout.print(status)
except PlatformError as e:
stderr.print(f"[red]{str(e)}[/]")
return
Expand Down
3 changes: 2 additions & 1 deletion dem/cli/command/modify_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ def execute(platform: Platform, dev_env_name: str) -> None:
stdout.print("[yellow]The Development Environment is installed, so it can't be modified.[/]")
typer.confirm("Do you want to uninstall it first?", abort=True)
try:
platform.uninstall_dev_env(dev_env)
for status in platform.uninstall_dev_env(dev_env):
stdout.print(status)
except PlatformError as e:
stderr.print(f"[red]{str(e)}[/]")
return
Expand Down
3 changes: 2 additions & 1 deletion dem/cli/command/uninstall_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def execute(platform: Platform, dev_env_name: str) -> None:
stderr.print(f"[red]Error: The {dev_env_name} Development Environment is not installed.[/]")
else:
try:
platform.uninstall_dev_env(dev_env_to_uninstall)
for status in platform.uninstall_dev_env(dev_env_to_uninstall):
stdout.print(status)
except PlatformError as e:
stderr.print(f"[red]{str(e)}[/]")
else:
Expand Down
7 changes: 3 additions & 4 deletions dem/core/container_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,18 @@ def run(self, container_arguments: list[str]) -> None:
def remove(self, image: str) -> None:
""" Remove a tool image.

If the removal was successful return with True, otherwise return with False.

Args:
image -- the tool image to remove

Raises:
ContainerEngineError -- if the image is used by a container
"""
try:
self._docker_client.images.remove(image)
except docker.errors.ImageNotFound:
self.user_output.msg(f"[yellow]The {image} doesn't exist. Unable to remove it.[/]\n")
except docker.errors.APIError:
raise ContainerEngineError(f"The {image} is used by a container. Unable to remove it.\n")
else:
self.user_output.msg(f"[green]Successfully removed the {image}![/]\n")

def search(self, registry: str) -> list[str]:
""" Search repository in the axemsolutions registry.
Expand Down
33 changes: 24 additions & 9 deletions dem/core/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""

import os
from typing import Any
from typing import Any, Generator
from dem.core.core import Core
from dem.core.properties import __supported_dev_env_major_version__
from dem.core.exceptions import DataStorageError, PlatformError, ContainerEngineError
Expand Down Expand Up @@ -40,6 +40,7 @@ def __init__(self) -> None:
self._hosts = None
self.default_dev_env_name: str = ""
self.local_dev_envs: list[DevEnv] = []
self.are_tool_images_assigned: bool = False

# Set this to true in the platform instance to work with the local tool images only
self.local_only = False
Expand All @@ -64,6 +65,7 @@ def assign_tool_image_instances_to_all_dev_envs(self) -> None:
""" Assign the ToolImage instances to all Development Environments."""
for dev_env in self.local_dev_envs:
dev_env.assign_tool_image_instances(self.tool_images)
self.are_tool_images_assigned = True

@property
def tool_images(self) -> ToolImages:
Expand Down Expand Up @@ -172,34 +174,47 @@ def install_dev_env(self, dev_env_to_install: DevEnv) -> None:
dev_env_to_install.is_installed = True
self.flush_dev_env_properties()

def uninstall_dev_env(self, dev_env_to_uninstall: DevEnv) -> None:
def uninstall_dev_env(self, dev_env_to_uninstall: DevEnv) -> Generator:
""" Uninstall the Dev Env by removing the images not required anymore.

Args:
dev_env_to_uninstall -- the Development Environment to uninstall

Exceptions:
Returns:
Generator -- the status messages

Raises:
PlatformError -- if the uninstall fails
"""
if not self.are_tool_images_assigned:
self.assign_tool_image_instances_to_all_dev_envs()

all_required_tool_images = set()
for dev_env in self.local_dev_envs:
if (dev_env is not dev_env_to_uninstall) and dev_env.is_installed:
for tool_image_descriptor in dev_env.tool_image_descriptors:
all_required_tool_images.add(tool_image_descriptor["image_name"] + ":" + tool_image_descriptor["image_version"])
for tool_image in dev_env.tool_images:
all_required_tool_images.add(tool_image.name)

tool_images_to_remove = set()
for tool_image_descriptor in dev_env_to_uninstall.tool_image_descriptors:
tool_image_name = tool_image_descriptor["image_name"] + ":" + tool_image_descriptor["image_version"]
if tool_image_name not in all_required_tool_images:
tool_images_to_remove.add(tool_image_name)
for tool_image in dev_env_to_uninstall.tool_images:
if tool_image.availability == ToolImage.NOT_AVAILABLE or tool_image.availability == ToolImage.REGISTRY_ONLY:
yield f"[yellow]Warning: The {tool_image.name} image could not be removed, because it is not available locally.[/]"
continue

if tool_image.name not in all_required_tool_images:
tool_images_to_remove.add(tool_image.name)

for tool_image_name in tool_images_to_remove:
try:
self.container_engine.remove(tool_image_name)
except ContainerEngineError as e:
raise PlatformError(f"Dev Env uninstall failed. --> {str(e)}")
else:
yield f"The {tool_image_name} image has been removed."

dev_env_to_uninstall.is_installed = False
if self.default_dev_env_name == dev_env_to_uninstall.name:
self.default_dev_env_name = ""
self.flush_dev_env_properties()

def flush_dev_env_properties(self) -> None:
Expand Down
8 changes: 7 additions & 1 deletion tests/cli/test_clone_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ def test_handle_existing_local_dev_env(mock_stdout_print: MagicMock, mock_typer_
mock_platform = MagicMock()
mock_local_dev_env = MagicMock()

test_uninstall_dev_env_status = ["test_uninstall_dev_env_status",
"test_uninstall_dev_env_status2"]
mock_platform.uninstall_dev_env.return_value = test_uninstall_dev_env_status

# Run unit under test
clone_cmd.handle_existing_local_dev_env(mock_platform, mock_local_dev_env)

# Check expectations
mock_stdout_print.assert_called_once_with("[yellow]The Dev Env already exists.[/]")
mock_stdout_print.assert_has_calls([call("[yellow]The Dev Env already exists.[/]"),
call(test_uninstall_dev_env_status[0]),
call(test_uninstall_dev_env_status[1])])
mock_typer_confirm.assert_has_calls([call("Continue with overwrite?", abort=True),
call("The Dev Env to overwrite is installed. Do you want to uninstall it?",
abort=True)])
Expand Down
10 changes: 9 additions & 1 deletion tests/cli/test_create_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ def test_create_dev_env_new(mock_open_dev_env_settings_panel: MagicMock,

@patch("dem.cli.command.create_cmd.create_new_dev_env_descriptor")
@patch("dem.cli.command.create_cmd.open_dev_env_settings_panel")
@patch("dem.cli.command.create_cmd.stdout.print")
@patch("dem.cli.command.create_cmd.typer.confirm")
def test_create_dev_env_overwrite_installed(mock_confirm: MagicMock,
def test_create_dev_env_overwrite_installed(mock_confirm: MagicMock, mock_stdout_print: MagicMock,
mock_open_dev_env_settings_panel: MagicMock,
mock_create_new_dev_env_descriptor: MagicMock) -> None:
# Test setup
Expand All @@ -163,6 +164,9 @@ def test_create_dev_env_overwrite_installed(mock_confirm: MagicMock,
mock_selected_tool_images = MagicMock()
mock_open_dev_env_settings_panel.return_value = mock_selected_tool_images

test_uninstall_dev_env_status = ["status1", "status2"]
mock_platform.uninstall_dev_env.return_value = test_uninstall_dev_env_status

test_dev_env_name = "test_dev_env"

# Run unit under test
Expand All @@ -179,6 +183,10 @@ def test_create_dev_env_overwrite_installed(mock_confirm: MagicMock,
"Uninstall it first?", abort=True)
])
mock_platform.uninstall_dev_env.assert_called_once_with(mock_dev_env_original)
mock_stdout_print.assert_has_calls([
call("status1"),
call("status2")
])
mock_open_dev_env_settings_panel.assert_called_once_with(mock_platform.tool_images.all_tool_images)
mock_create_new_dev_env_descriptor.assert_called_once_with(test_dev_env_name,
mock_selected_tool_images)
Expand Down
7 changes: 6 additions & 1 deletion tests/cli/test_delete_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def test_delete(mock_stdout_print: MagicMock, mock_config: MagicMock) -> None:
mock_platform.get_dev_env_by_name.return_value = test_dev_env
mock_platform.local_dev_envs = [test_dev_env]

test_uninstall_dev_env_status = ["test_uninstall_dev_env_status",
"test_uninstall_dev_env_status2"]
mock_platform.uninstall_dev_env.return_value = test_uninstall_dev_env_status

# Run unit under test
runner_result = runner.invoke(main.typer_cli, ["delete", test_dev_env_name])

Expand All @@ -37,7 +41,8 @@ def test_delete(mock_stdout_print: MagicMock, mock_config: MagicMock) -> None:
abort=True)
mock_platform.uninstall_dev_env.assert_called_once_with(test_dev_env)
mock_stdout_print.assert_has_calls([
call("Deleting the Development Environment..."),
call(test_uninstall_dev_env_status[0]), call(test_uninstall_dev_env_status[1]),
call("Deleting the Development Environment descriptor..."),
call(f"[green]Successfully deleted the {test_dev_env_name}![/]")
])
mock_platform.flush_dev_env_properties.assert_called_once()
Expand Down
10 changes: 8 additions & 2 deletions tests/cli/test_init_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def test_execute_reinit_installed(mock_DevEnv, mock_confirm, mock_stdout_print,
mock_local_dev_env.is_installed = True
mock_platform.local_dev_envs = [mock_local_dev_env]

test_uninstall_dev_env_status = ["test_uninstall_dev_env_status",
"test_uninstall_dev_env_status2"]
mock_platform.uninstall_dev_env.return_value = test_uninstall_dev_env_status

# Run unit under test
init_cmd.execute(mock_platform, mock_project_path)

Expand All @@ -100,8 +104,10 @@ def test_execute_reinit_installed(mock_DevEnv, mock_confirm, mock_stdout_print,
call("The Development Environment is installed, so it can't be deleted. Do you want to uninstall it first?", abort=True)])
mock_platform.uninstall_dev_env.assert_called_once_with(mock_local_dev_env)
mock_platform.flush_dev_env_properties.assert_called_once()
mock_stdout_print.assert_has_calls([call(f"[green]Successfully initialized the {mock_dev_env_name} Dev Env for the project at {mock_project_path}![/]"),
call(f"\nNow you can install the Dev Env with the `dem install {mock_dev_env_name}` command.")])
mock_stdout_print.assert_has_calls([
call(test_uninstall_dev_env_status[0]), call(test_uninstall_dev_env_status[1]),
call(f"[green]Successfully initialized the {mock_dev_env_name} Dev Env for the project at {mock_project_path}![/]"),
call(f"\nNow you can install the Dev Env with the `dem install {mock_dev_env_name}` command.")])

@patch("dem.cli.command.init_cmd.os.path.isdir")
@patch("dem.cli.command.init_cmd.stderr.print")
Expand Down
41 changes: 38 additions & 3 deletions tests/cli/test_modify_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# Test framework
import pytest
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock, call

from rich.console import Console
import io, typer
Expand Down Expand Up @@ -241,6 +241,7 @@ def test_execute_invalid_name():
# Check expectations
assert 0 == runner_result.exit_code

mock_platform.assign_tool_image_instances_to_all_dev_envs.assert_called_once()
mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name)

console = Console(file=io.StringIO())
Expand All @@ -262,15 +263,48 @@ def test_execute(mock_modify_with_tui: MagicMock, mock_stdout_print: MagicMock)
modify_cmd.execute(mock_platform, test_dev_env_name)

# Check expectations
mock_platform.assign_tool_image_instances_to_all_dev_envs.assert_called_once()
mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name)
mock_modify_with_tui.assert_called_once_with(mock_platform, mock_dev_env)
mock_stdout_print.assert_called_once_with("[green]The Development Environment has been modified successfully![/]")

@patch("dem.cli.command.modify_cmd.modify_with_tui")
@patch("dem.cli.command.modify_cmd.typer.confirm")
@patch("dem.cli.command.modify_cmd.stdout.print")
def test_execute_installed(mock_stdout_print: MagicMock, mock_confirm: MagicMock,
mock_modify_with_tui: MagicMock) -> None:
# Test setup
mock_platform = MagicMock()
test_dev_env_name = "test_dev_env_name"

mock_dev_env = MagicMock()
mock_dev_env.is_installed = True
mock_platform.get_dev_env_by_name.return_value = mock_dev_env

test_uninstall_dev_env_status = ["test_uninstall_dev_env_status",
"test_uninstall_dev_env_status2"]
mock_platform.uninstall_dev_env.return_value = test_uninstall_dev_env_status

# Run unit under test
modify_cmd.execute(mock_platform, test_dev_env_name)

# Check expectations
mock_platform.assign_tool_image_instances_to_all_dev_envs.assert_called_once()
mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name)
mock_stdout_print.assert_has_calls([
call("[yellow]The Development Environment is installed, so it can't be modified.[/]"),
call(test_uninstall_dev_env_status[0]), call(test_uninstall_dev_env_status[1]),
call("[green]The Development Environment has been modified successfully![/]"),
])
mock_confirm.assert_called_once_with("Do you want to uninstall it first?", abort=True)
mock_platform.uninstall_dev_env.assert_called_once_with(mock_dev_env)
mock_modify_with_tui.assert_called_once_with(mock_platform, mock_dev_env)

@patch("dem.cli.command.modify_cmd.stderr.print")
@patch("dem.cli.command.modify_cmd.typer.confirm")
@patch("dem.cli.command.modify_cmd.stdout.print")
def test_execute_PlatformError(mock_stdout_print: MagicMock, mock_confirm: MagicMock,
mock_stderr_print: MagicMock) -> None:
def test_execute_installed_PlatformError(mock_stdout_print: MagicMock, mock_confirm: MagicMock,
mock_stderr_print: MagicMock) -> None:
# Test setup
mock_platform = MagicMock()
test_dev_env_name = "test_dev_env_name"
Expand All @@ -285,6 +319,7 @@ def test_execute_PlatformError(mock_stdout_print: MagicMock, mock_confirm: Magic
modify_cmd.execute(mock_platform, test_dev_env_name)

# Check expectations
mock_platform.assign_tool_image_instances_to_all_dev_envs.assert_called_once()
mock_platform.get_dev_env_by_name.assert_called_once_with(test_dev_env_name)
mock_stdout_print.assert_called_once_with("[yellow]The Development Environment is installed, so it can't be modified.[/]")
mock_confirm.assert_called_once_with("Do you want to uninstall it first?", abort=True)
Expand Down
17 changes: 13 additions & 4 deletions tests/cli/test_uninstall_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# Test framework
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock, call

## Global test variables
runner = CliRunner()
Expand All @@ -30,7 +30,7 @@ def test_uninstall_dev_env_invalid_name(mock_stderr_print):
mock_stderr_print.assert_called_once_with(f"[red]Error: The {test_invalid_name} Development Environment does not exist.[/]")

@patch("dem.cli.command.uninstall_cmd.stdout.print")
def test_uninstall_dev_env_valid_name(mock_stdout_print):
def test_uninstall_dev_env_valid_name(mock_stdout_print: MagicMock) -> None:
# Test setup
fake_dev_env_to_uninstall = MagicMock()
fake_dev_env_to_uninstall.name = "dev_env"
Expand All @@ -39,15 +39,24 @@ def test_uninstall_dev_env_valid_name(mock_stdout_print):
mock_platform.get_dev_env_by_name.return_value = fake_dev_env_to_uninstall
main.platform = mock_platform

test_uninstall_dev_env_status = ["test_uninstall_dev_env_status",
"test_uninstall_dev_env_status2"]
mock_platform.uninstall_dev_env.return_value = test_uninstall_dev_env_status

# Run unit under test
runner_result = runner.invoke(main.typer_cli, ["uninstall", fake_dev_env_to_uninstall.name ], color=True)
runner_result = runner.invoke(main.typer_cli, ["uninstall", fake_dev_env_to_uninstall.name],
color=True)

# Check expectations
assert 0 == runner_result.exit_code

mock_platform.get_dev_env_by_name.assert_called_once_with(fake_dev_env_to_uninstall.name )
mock_platform.uninstall_dev_env.assert_called_once_with(fake_dev_env_to_uninstall)
mock_stdout_print.assert_called_once_with(f"[green]Successfully uninstalled the {fake_dev_env_to_uninstall.name}![/]")
mock_stdout_print.assert_has_calls([
call(test_uninstall_dev_env_status[0]),
call(test_uninstall_dev_env_status[1]),
call(f"[green]Successfully uninstalled the {fake_dev_env_to_uninstall.name}![/]")
])

@patch("dem.cli.command.uninstall_cmd.stderr.print")
def test_uninstall_dev_env_valid_name_not_installed(mock_stderr_print):
Expand Down
1 change: 0 additions & 1 deletion tests/core/test_container_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ def test_remove(mock_from_env: MagicMock, mock_user_output: MagicMock) -> None:

# Check expectations
mock_docker_client.images.remove.assert_called_once_with(test_image_to_remove)
mock_user_output.msg.assert_called_once_with(f"[green]Successfully removed the {test_image_to_remove}![/]\n")

@patch.object(container_engine.ContainerEngine, "user_output")
@patch("docker.from_env")
Expand Down
Loading
Loading