Skip to content

Commit

Permalink
Uninstall command implementation with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
magyta93 authored and janosmurai committed Dec 21, 2023
1 parent a558e7a commit 21df9ed
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 2 deletions.
29 changes: 29 additions & 0 deletions dem/cli/command/uninstall_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""uninstall CLI command implementation."""
# dem/cli/command/uninstall_cmd.py

from dem.core.dev_env import DevEnv
from dem.core.platform import DevEnvLocalSetup
from dem.cli.console import stderr, stdout


def try_to_uninstall_dev_env(dev_env_to_uninstall: DevEnv, is_dev_env_installed: str, platform: DevEnvLocalSetup) -> bool:
if is_dev_env_installed == "True":
return platform.try_to_remove_tool_images(dev_env_to_uninstall)
else:
return False


def execute(platform: DevEnvLocalSetup, dev_env_name: str) -> None:
dev_env_to_uninstall = platform.get_dev_env_by_name(dev_env_name)
is_dev_env_installed = platform.get_dev_env_status_by_name(dev_env_name)


if dev_env_to_uninstall is None:
stderr.print("[red]Error: The [bold]" + dev_env_name + "[/bold] Development Environment doesn't exist.")
else:
if True == try_to_uninstall_dev_env(dev_env_to_uninstall,is_dev_env_installed,platform):
stdout.print("[green]Successfully deleted the " + dev_env_name + "![/]")
platform.update_dev_env_status_in_json(dev_env_to_uninstall)

else:
stderr.print("[red]Error: The [bold]" + dev_env_name + "[/bold] Development Environment uninstall failed")
14 changes: 13 additions & 1 deletion dem/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dem.cli.command import cp_cmd, info_cmd, list_cmd, pull_cmd, create_cmd, modify_cmd, delete_cmd, \
rename_cmd, run_cmd, export_cmd, load_cmd, clone_cmd, add_reg_cmd, \
list_reg_cmd, del_reg_cmd, add_cat_cmd, list_cat_cmd, del_cat_cmd, \
add_host_cmd
add_host_cmd, uninstall_cmd
from dem.cli.console import stdout
from dem.core.platform import DevEnvLocalSetup
from dem.core.exceptions import InternalError
Expand Down Expand Up @@ -194,6 +194,18 @@ def delete(dev_env_name: Annotated[str, typer.Argument(help="Name of the Develop
else:
raise InternalError("Error: The platform hasn't been initialized properly!")

@typer_cli.command()
def uninstall(dev_env_name: Annotated[str, typer.Argument(help="Name of the Development Environment to uninstall.",
autocompletion=autocomplete_dev_env_name)]) -> None:
"""
Uninstall the Development Environment from the local setup. If a tool image is not required
anymore by any of the available local Development Environments, the DEM will delete it.
"""
if platform is not None:
uninstall_cmd.execute(platform, dev_env_name)
else:
raise InternalError("Error: The platform hasn't been initialized properly!")

@typer_cli.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
def run(dev_env_name: Annotated[str, typer.Argument(help="Run the container in this Development Environment context",
autocompletion=autocomplete_dev_env_name)],
Expand Down
14 changes: 13 additions & 1 deletion dem/core/container_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,19 @@ def remove(self, image: str) -> None:
Args:
image -- the tool image to remove
"""
self._docker_client.images.remove(image)
retVal=True
try:
self._docker_client.images.remove(image)
except docker.errors.ImageNotFound:
self.user_output.error("[yellow]" + image + " doesn't exist. Unable to remove it.[/]\n")
retVal=False
except docker.errors.APIError:
self.user_output.error("[red]Error: " + image + " is used by a container. Unable to remove it.[/]\n")
retVal=False
else:
self.user_output.msg("[green]Successfully removed![/]\n")
retVal=True
return retVal

def search(self, registry: str) -> list[str]:
""" Search repository in the axemsolutions registry.
Expand Down
1 change: 1 addition & 0 deletions dem/core/dev_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(self, descriptor: dict | None = None,
if descriptor:
self.name = descriptor["name"]
self.tools = descriptor["tools"]
self.installed = descriptor["installed"]
else:
self.name = dev_env_to_copy.name
self.tools = dev_env_to_copy.tools
Expand Down
64 changes: 64 additions & 0 deletions dem/core/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from dem.core.tool_images import ToolImages
from dem.core.dev_env import DevEnv, DevEnv, DevEnv

import docker.errors

class DevEnvSetup(Core):
""" Representation of the Development Platform:
- The available tool images.
Expand Down Expand Up @@ -136,6 +138,7 @@ def get_deserialized(self) -> dict:
for dev_env in self.local_dev_envs:
dev_env_descriptor = {}
dev_env_descriptor["name"] = dev_env.name
dev_env_descriptor["installed"] = dev_env.installed
dev_env_descriptor["tools"] = dev_env.tools
dev_env_descriptors.append(dev_env_descriptor)
dev_env_json_deserialized["development_environments"] = dev_env_descriptors
Expand All @@ -153,6 +156,30 @@ def get_dev_env_by_name(self, dev_env_name: str) -> ("DevEnv | DevEnv | None"):
for dev_env in self.local_dev_envs:
if dev_env.name == dev_env_name:
return dev_env

def get_dev_env_status_by_name(self, dev_env_name: str) -> ("DevEnv | DevEnv | None"):
""" Get the Development Environment status by name.
Args:
dev_env_name -- name of the Development Environment to get
Return with the instance representing the Development Environment. If the Development
Environment doesn't exist in the setup, return with None.
"""
for dev_env in self.local_dev_envs:
if dev_env.name == dev_env_name:
return dev_env.installed

def set_dev_env_status_by_name(self, dev_env_name: str, status: str) -> ("DevEnv | DevEnv | None"):
""" Set the Development Environment status by name.
Args:
dev_env_name -- name of the Development Environment to set
status -- status of the Development Environment
"""
for dev_env in self.local_dev_envs:
if dev_env.name == dev_env_name:
dev_env.installed = status

def get_local_dev_env(self, catalog_dev_env: DevEnv) -> DevEnv | None:
""" Get the local copy of the catalog's Dev Env if exists.
Expand All @@ -165,6 +192,43 @@ def get_local_dev_env(self, catalog_dev_env: DevEnv) -> DevEnv | None:
for local_dev_env in self.local_dev_envs:
if catalog_dev_env.name == local_dev_env.name:
return local_dev_env


def try_to_remove_tool_images(self, uninstalled_dev_env: DevEnv) -> None:
retVal=True
all_required_tool_images = set()
for dev_env in self.local_dev_envs:
for tool in dev_env.tools:
if (dev_env.installed == "True") and (dev_env.name != uninstalled_dev_env.name) :
all_required_tool_images.add(tool["image_name"] + ":" + tool["image_version"])

uninstalled_dev_env_tool_images = set()
for tool in uninstalled_dev_env.tools:
uninstalled_dev_env_tool_images.add(tool["image_name"] + ":" + tool["image_version"])


for tool_image in uninstalled_dev_env_tool_images:
if tool_image in all_required_tool_images:
self.user_output.msg("[yellow] Can't delete" + tool_image + "tool images!")
else:
retVal = self.delete_tool_image(tool_image)
if retVal == False:
return retVal
else:
retVal = True


return retVal

def delete_tool_image(self, tool_image: str) -> bool:
self.user_output.msg("\nThe tool image [bold]" + tool_image + "[/bold] is not required by any Development Environment anymore.")
retVal=self.container_engine.remove(tool_image)
return retVal

def update_dev_env_status_in_json(self, uninstalled_dev_env: DevEnv):
listindex = self.local_dev_envs.index(uninstalled_dev_env)
self.local_dev_envs[listindex].installed = "False"
self.flush_to_file()

class DevEnvLocalSetup(DevEnvSetup):
def __init__(self) -> None:
Expand Down
12 changes: 12 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,16 @@ Arguments:

`ADDRESS` IP or hostname of the host. [required]

---

## **`dem uninstall DEV_ENV_NAME`**

Uninstall the selected Development Environment. Set installed flag to False if it was True. Dem checks whether a tool image is
required or not by any of the remaining installed local Development Environments. In case the tool image is
not required anymore, the dem delete it.

Arguments:

`DEV_ENV_NAME` Name of the Development Environment to uninstall. [required]

---
1 change: 1 addition & 0 deletions tests/cli/test_export_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_create_exported_dev_env_json(mock_os_path_isdir,mock_open):
dev_env_name = "dev_env_name"
dev_env_json = {
"name": "Cica",
"installed": "True",
"tools": [
{
"type": "build system",
Expand Down
103 changes: 103 additions & 0 deletions tests/cli/test_uninstall_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Tests for the uninstall command."""


# Unit under test:
import dem.cli.main as main
import dem.cli.command.uninstall_cmd as uninstall_cmd

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

import docker.errors

## Global test variables
runner = CliRunner()


@patch("dem.cli.command.uninstall_cmd.stderr.print")
def test_uninstall_dev_env_invalid_name(mock_stderr_print):
# Test setup
test_invalid_name = "fake_dev_env_name"

mock_platform = MagicMock()
mock_platform.get_dev_env_by_name.return_value = None
main.platform = mock_platform

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

# Check expectations
assert 0 == runner_result.exit_code

mock_platform.get_dev_env_by_name.assert_called_once_with(test_invalid_name)
mock_stderr_print.assert_called_once_with("[red]Error: The [bold]" + test_invalid_name + "[/bold] Development Environment doesn't exist.")


@patch("dem.cli.command.uninstall_cmd.stdout.print")
def test_uninstall_dev_env_valid_name(mock_stdout_print):
# Test setup
fake_dev_env_to_uninstall = MagicMock()
fake_dev_env_to_uninstall.name = "dev_env"
fake_dev_env_to_uninstall.installed = "True"
mock_platform = MagicMock()
mock_platform.get_dev_env_by_name.return_value = [fake_dev_env_to_uninstall]
mock_platform.get_dev_env_status_by_name.return_value = "True"
main.platform = mock_platform
mock_platform.try_to_remove_tool_images.return_value = True


# Run unit under test
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_stdout_print.assert_called_once_with("[green]Successfully deleted 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):
# Test setup
fake_dev_env_to_uninstall = MagicMock()
fake_dev_env_to_uninstall.name = "dev_env"
fake_dev_env_to_uninstall.installed = "False"
mock_platform = MagicMock()
mock_platform.get_dev_env_by_name.return_value = [fake_dev_env_to_uninstall]
mock_platform.get_dev_env_status_by_name.return_value = "False"
main.platform = mock_platform


# Run unit under test
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_stderr_print.assert_called_once_with("[red]Error: The [bold]" + fake_dev_env_to_uninstall.name + "[/bold] Development Environment uninstall failed")


@patch("dem.cli.command.uninstall_cmd.stderr.print")
def test_uninstall_dev_env_valid_name_failed(mock_stderr_print):
# Test setup
fake_dev_env_to_uninstall = MagicMock()
fake_dev_env_to_uninstall.name = "dev_env"
fake_dev_env_to_uninstall.installed = "True"
mock_platform = MagicMock()
mock_platform.get_dev_env_by_name.return_value = [fake_dev_env_to_uninstall]
mock_platform.get_dev_env_status_by_name.return_value = "True"
mock_platform.try_to_remove_tool_images.return_value = False
main.platform = mock_platform


# Run unit under test
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_stderr_print.assert_called_once_with("[red]Error: The [bold]" + fake_dev_env_to_uninstall.name + "[/bold] Development Environment uninstall failed")

3 changes: 3 additions & 0 deletions tests/core/test_dev_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def test_DevEnv():
# Test setup
test_descriptor = {
"name": "test_name",
"installed": "True",
"tools": [MagicMock()]
}

Expand Down Expand Up @@ -38,6 +39,7 @@ def test_DevEnv_check_image_availability():
# Test setup
test_descriptor = {
"name": "test_name",
"installed": "True",
"tools": [
{
"image_name": "test_image_name1",
Expand Down Expand Up @@ -91,6 +93,7 @@ def test_DevEnv_check_image_availability_local_only():
# Test setup
test_descriptor = {
"name": "test_name",
"installed": "True",
"tools": [
{
"image_name": "test_image_name1",
Expand Down
3 changes: 3 additions & 0 deletions tests/fake_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"version": "0.1",
"development_environments": [{
"name": "demo",
"installed": "True",
"tools": [{
"type": "build system",
"image_name": "axemsolutions/make_gnu_arm",
Expand Down Expand Up @@ -31,6 +32,7 @@
},
{
"name": "nagy_cica_project",
"installed": "True",
"tools": [{
"type": "build system",
"image_name": "axemsolutions/bazel",
Expand Down Expand Up @@ -72,6 +74,7 @@
"version": "0.1",
"development_environments": [{
"name": "demo",
"installed": "False",
"tools": [{
"type": "build_system",
"image_name": "axemsolutions/make_gnu_arm",
Expand Down

0 comments on commit 21df9ed

Please sign in to comment.