Skip to content

Commit 5e5e701

Browse files
[RPD-242] Consolidate matcha destroy and matcha destroy full into one command (#141)
* Consolidate destroy and destroy full * Remove tests that no longer describe the expected functionality * RPD-242 fixes to remove destroy full * RPD-242 added downloading matcha.state * RPD-242 fix for missing _state * RPD-242 fixing tests * RPD-242 updated docstring --------- Co-authored-by: Jonathan Carlton <jonathan.carlton@hotmail.com>
1 parent 65f803b commit 5e5e701

14 files changed

Lines changed: 73 additions & 377 deletions

File tree

src/matcha_ml/cli/cli.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
build_resource_output,
2121
hide_sensitive_in_output,
2222
)
23-
from matcha_ml.cli.ui.status_message_builders import build_warning_status
2423
from matcha_ml.core import core
2524
from matcha_ml.errors import MatchaError, MatchaInputError
2625
from matcha_ml.services.analytics_service import AnalyticsEvent, track
@@ -105,24 +104,11 @@ def get(
105104

106105
@app.command()
107106
@track(event_name=AnalyticsEvent.DESTROY)
108-
def destroy(
109-
full: Optional[str] = typer.Argument(
110-
default=None,
111-
help="When the full command is specified, the resource group and all its provisioned resources will be destroyed together.",
112-
),
113-
) -> None:
107+
def destroy() -> None:
114108
"""Destroy the provisioned cloud resources."""
115109
remote_state_manager = RemoteStateManager()
116-
if full:
117-
destroy_resources(resources=STATE_RESOURCE_MSG + RESOURCE_MSG)
118-
remote_state_manager.deprovision_remote_state()
119-
else:
120-
print_status(
121-
build_warning_status(
122-
"Warning: a storage container holding Matcha state information will persist. To destroy ALL cloud resources, run 'matcha destroy full'."
123-
)
124-
)
125-
destroy_resources(resources=RESOURCE_MSG)
110+
destroy_resources(resources=STATE_RESOURCE_MSG + RESOURCE_MSG)
111+
remote_state_manager.deprovision_remote_state()
126112

127113

128114
@app.command()

src/matcha_ml/cli/destroy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def destroy_resources(resources: List[Tuple[str, str]]) -> None:
2727
remote_state = RemoteStateManager()
2828
if not remote_state.is_state_provisioned():
2929
print_error(
30-
"Error - matcha state has not been initialized, nothing to destroy."
30+
"Error - resources that have not been provisioned cannot be destroy. Run 'matcha provision' to get started!"
3131
)
3232
raise typer.Exit()
3333

src/matcha_ml/cli/provision.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,29 +77,21 @@ def provision_resources(
7777
typer.Exit: if approval for removing a stale state is not given by user.
7878
"""
7979
remote_state_manager = RemoteStateManager()
80+
is_provisioned = remote_state_manager.is_state_provisioned()
8081

81-
if remote_state_manager.is_state_stale():
82+
if is_provisioned:
8283
print_status(
8384
build_warning_status(
84-
"Matcha has detected a stale state file. This means that your local configuration is out of sync with the remote state, the resource group may have been removed."
85+
"WARNING - Matcha has detected that there are resources already provisioned. Use 'matcha destroy' to remove the existing resources before trying to provision again."
8586
)
8687
)
88+
raise typer.Exit()
8789

88-
if not typer.confirm(
89-
"Do you want to remove the existing local config and continue?"
90-
):
91-
raise typer.Exit()
92-
93-
remote_state_manager.remove_matcha_config()
94-
# Re-initialise remote state manager with empty state file
95-
remote_state_manager = RemoteStateManager()
96-
97-
# Check whether remote state storage has been provisioned
98-
if not remote_state_manager.is_state_provisioned():
90+
if not is_provisioned:
9991
location, prefix, _ = fill_provision_variables(
10092
location=location, prefix=prefix, password="temp"
10193
)
102-
# Provision a state storage if it's not provisioned
94+
10395
remote_state_manager.provision_remote_state(location, prefix)
10496

10597
with remote_state_manager.use_lock(), remote_state_manager.use_remote_state():
@@ -117,15 +109,14 @@ def provision_resources(
117109
# Create a azure template object
118110
azure_template = AzureTemplate()
119111

120-
if not azure_template.reuse_configuration(destination):
121-
location, prefix, password = fill_provision_variables(
122-
location, prefix, password
123-
)
112+
location, prefix, password = fill_provision_variables(
113+
location, prefix, password
114+
)
124115

125-
config = azure_template.build_template_configuration(
126-
location=location, prefix=prefix, password=password
127-
)
128-
azure_template.build_template(config, template, destination, verbose)
116+
config = azure_template.build_template_configuration(
117+
location=location, prefix=prefix, password=password
118+
)
119+
azure_template.build_template(config, template, destination, verbose)
129120

130121
# Initializes the infrastructure provisioning process.
131122
if template_runner.is_approved(verb="provision", resources=RESOURCE_MSG):

src/matcha_ml/core/core.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ def get(
3737
)
3838

3939
if not matcha_state_service.state_file_exists:
40-
raise MatchaError(
41-
f"Error - matcha.state file does not exist at {os.path.join(os.getcwd(), '.matcha', 'infrastructure')} . Please run 'matcha provision' before trying to get the resource."
42-
)
40+
# if the state file doesn't exist, then download it from the remote
41+
remote_state.download(os.getcwd())
42+
43+
# reinitialise to create the necessary variables
44+
matcha_state_service = MatchaStateService()
4345

4446
with remote_state.use_lock():
4547
local_hash = matcha_state_service.get_hash_local_state()
@@ -50,6 +52,8 @@ def get(
5052
if local_hash != remote_hash:
5153
remote_state.download(os.getcwd())
5254

55+
matcha_state_service = MatchaStateService()
56+
5357
if resource_name:
5458
get_command_validation(
5559
resource_name,

src/matcha_ml/runners/azure_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,6 @@ def deprovision(self) -> None:
150150
"""Destroy the provisioned resources."""
151151
self._check_matcha_directory_exists()
152152
self._check_terraform_installation()
153-
self._initialize_terraform(msg="Matcha")
153+
self._initialize_terraform(msg="Matcha", destroy=True)
154154
self._destroy_terraform(msg="Matcha")
155155
self._write_outputs_state_cloud_only()

src/matcha_ml/runners/base_runner.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,22 @@ def _validate_kubeconfig(self, base_path: str = ".kube/config") -> None:
7171
"""
7272
self.tfs.verify_kubectl_config_file(base_path)
7373

74-
def _initialize_terraform(self, msg: str = "") -> None:
74+
def _initialize_terraform(self, msg: str = "", destroy: bool = False) -> None:
7575
"""Run terraform init to initialize Terraform .
7676
7777
Raises:
7878
MatchaTerraformError: if 'terraform init' failed.
7979
msg (str) : Message to display. Default is empty string.
80+
destroy (bool): whether this function is being called in a destructive context
8081
"""
8182
if self.tf_state_dir.exists():
82-
# this directory gets created after a successful init command
83-
print_status(
84-
build_status(
85-
f"matcha {Emojis.MATCHA.value} has already been initialized. Skipping this step..."
83+
if not destroy:
84+
# this directory gets created after a successful init command
85+
print_status(
86+
build_status(
87+
f"matcha {Emojis.MATCHA.value} has already been initialized. Skipping this step..."
88+
)
8689
)
87-
)
8890

8991
else:
9092
print_status(

src/matcha_ml/state/remote_state_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
DEFAULT_CONFIG_NAME = "matcha.config.json"
2222
ALREADY_LOCKED_MESSAGE = (
2323
"Remote state is already locked, maybe someone else is using matcha?"
24-
"If you think this is a mistake, you can unlock the state by running 'matcha force-unlock'."
24+
" If you think this is a mistake, you can unlock the state by running 'matcha force-unlock'."
2525
)
2626

2727

src/matcha_ml/templates/azure_template.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@
33
import os
44
from typing import Optional
55

6-
import typer
7-
8-
from matcha_ml.cli._validation import check_current_deployment_exists
9-
from matcha_ml.cli.ui.print_messages import print_status
10-
from matcha_ml.state import MatchaStateService
116
from matcha_ml.templates.base_template import BaseTemplate, TemplateVariables
127

138
SUBMODULE_NAMES = [
@@ -39,35 +34,6 @@ def __init__(self) -> None:
3934
"""
4035
super().__init__(SUBMODULE_NAMES)
4136

42-
def reuse_configuration(self, path: str) -> bool:
43-
"""Check if a configuration already exists, and prompt user to override or reuse it.
44-
45-
Args:
46-
path (str): path to the infrastructure configuration
47-
48-
Returns:
49-
bool: decision to reuse the existing configuration
50-
"""
51-
if os.path.exists(path):
52-
if check_current_deployment_exists():
53-
matcha_state_service = MatchaStateService()
54-
resource_group_name = (
55-
matcha_state_service.fetch_resources_from_state_file(
56-
"cloud", "prefix"
57-
)["cloud"]["prefix"]
58-
)
59-
warning_msg = f"\nWARNING: Matcha has detected that a deployment already exists in Azure with the resource group name '{resource_group_name}'. Use 'matcha destroy' to remove these resources before trying to provision."
60-
confirmation_msg = "\nIf you continue, you will create a orphan resource. You should destroy the resources before proceeding.\n\nDo you want to override the existing configuration?"
61-
else:
62-
warning_msg = "\nMatcha has detected that the you already have resources configured for provisioning."
63-
confirmation_msg = "\nIf you choose to override the existing configuration, the existing configuration will be deleted. Otherwise, the configuration will be reused.\n\nDo you want to override the existing configuration?"
64-
65-
print_status(warning_msg)
66-
67-
return not typer.confirm(confirmation_msg)
68-
else:
69-
return False
70-
7137
def build_template(
7238
self,
7339
config: TemplateVariables,

tests/test_cli/test_destroy.py

Lines changed: 21 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
"""Test suite to test the destroy command and all its subcommands."""
2-
import json
32
import os
43
from typing import Iterable
54
from unittest.mock import MagicMock, patch
65

76
import pytest
87

98
from matcha_ml.cli.cli import app
10-
from matcha_ml.services.terraform_service import TerraformConfig
119

1210

1311
@pytest.fixture(autouse=True)
14-
def mock_provisioned_remote_state() -> Iterable[MagicMock]:
12+
def mock_remote_state_manager() -> Iterable[MagicMock]:
1513
"""Mock remote state manager to have state provisioned.
1614
17-
Returns:
15+
Yields:
1816
MagicMock: mock of an RemoteStateManager instance
1917
"""
2018
with patch("matcha_ml.cli.destroy.RemoteStateManager") as mock_state_manager_class:
2119
mock_state_manager = mock_state_manager_class.return_value
22-
mock_state_manager.is_state_provisioned.return_value = True
2320
yield mock_state_manager
2421

2522

@@ -40,90 +37,43 @@ def test_cli_destroy_command_help(runner):
4037

4138

4239
def test_cli_destroy_command_with_no_provisioned_resources(
43-
runner, matcha_testing_directory, mock_provisioned_remote_state: MagicMock
40+
runner, matcha_testing_directory, mock_remote_state_manager: MagicMock
4441
):
45-
"""Test provision command when there no existing resources deployed.
42+
"""Test destroy command when there no existing resources deployed.
4643
4744
Args:
4845
runner (CliRunner): typer CLI runner
4946
matcha_testing_directory (str): temporary working directory.
50-
mock_provisioned_remote_state (MagicMock): mock of an RemoteStateManager instance
47+
mock_remote_state_manager (MagicMock): mock of an RemoteStateManager instance
5148
"""
5249
os.chdir(matcha_testing_directory)
5350

54-
# Invoke destroy command
55-
with patch(
56-
"matcha_ml.templates.azure_template.check_current_deployment_exists"
57-
) as check_deployment_exists:
58-
check_deployment_exists.return_value = False
59-
result = runner.invoke(app, ["destroy"])
51+
mock_remote_state_manager.is_state_provisioned.return_value = True
52+
53+
result = runner.invoke(app, ["destroy"])
6054

6155
assert (
6256
"Error - you cannot destroy resources that have not been provisioned yet."
6357
in result.stdout
6458
)
6559

66-
mock_provisioned_remote_state.use_lock.assert_called_once()
60+
mock_remote_state_manager.use_lock.assert_called_once()
6761

6862

69-
def test_cli_destroy_command_updates_matcha_state(runner, matcha_testing_directory):
70-
"""Test provision command updates the matcha state file when resources (excluding resource group and state bucket) are destroyed.
63+
def test_cli_destroy_with_nothing_provisioned(
64+
runner, mock_remote_state_manager: MagicMock
65+
):
66+
"""Test the destroy command with no resources exist at all.
7167
7268
Args:
73-
runner (CliRunner): typer CLI runner
74-
matcha_testing_directory (str): temporary working directory.
69+
runner (CliRunner): typer CLI runner.
70+
mock_remote_state_manager (MagicMock): mock of a RemoteStateManager instance.
7571
"""
76-
os.chdir(matcha_testing_directory)
72+
mock_remote_state_manager.is_state_provisioned.return_value = False
7773

78-
matcha_infrastructure_dir = os.path.join(".matcha", "infrastructure", "resources")
79-
matcha_state_file_path = os.path.join(".matcha", "infrastructure", "matcha.state")
80-
81-
os.makedirs(matcha_infrastructure_dir)
82-
83-
state_file_resources = {
84-
"cloud": {
85-
"prefix": "test",
86-
"location": "ukwest",
87-
"flavor": "azure",
88-
"resource-group-name": "test_resources",
89-
},
90-
"id": {"matcha_uuid": "TestStateID"},
91-
"container-registry": {
92-
"flavor": "azure",
93-
"registry-name": "azure_registry_name",
94-
"registry-url": "azure_container_registry",
95-
},
96-
"experiment-tracker": {"flavor": "mlflow", "tracking-url": "mlflow_test_url"},
97-
}
98-
99-
with open(matcha_state_file_path, "w") as f:
100-
json.dump(state_file_resources, f)
101-
102-
expected_matcha_state_contents = {
103-
"cloud": {
104-
"prefix": "test",
105-
"location": "ukwest",
106-
"flavor": "azure",
107-
"resource-group-name": "test_resources",
108-
},
109-
"id": {"matcha_uuid": "TestStateID"},
110-
}
74+
result = runner.invoke(app, ["destroy"])
11175

112-
# Invoke destroy command
113-
with patch(
114-
"matcha_ml.cli.destroy.check_current_deployment_exists"
115-
) as check_deployment_exists, patch.object(
116-
TerraformConfig, "state_file", matcha_state_file_path
117-
):
118-
# mock_az_runner.return_value.state_file = matcha_state_file_path
119-
check_deployment_exists.return_value = True
120-
runner.invoke(
121-
app,
122-
["destroy"],
123-
input="Y\n",
124-
)
125-
126-
with open(matcha_state_file_path) as f:
127-
matcha_state_data = json.load(f)
128-
129-
assert matcha_state_data == expected_matcha_state_contents
76+
assert (
77+
"Error - resources that have not been provisioned cannot be destroy."
78+
in result.stdout
79+
)

tests/test_cli/test_get.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,27 +136,6 @@ def test_cli_get_command_help(runner: CliRunner):
136136
assert "Get information for the provisioned resources." in result.stdout
137137

138138

139-
def test_cli_get_command_with_no_state_file(
140-
runner: CliRunner, mock_provisioned_remote_state: MagicMock
141-
):
142-
"""Test cli for get command when the state file does not exist.
143-
144-
Args:
145-
runner (CliRunner): typer CLI runner
146-
mock_provisioned_remote_state (MagicMock): mock of an RemoteStateManager instance
147-
"""
148-
state_file_path = os.path.join(".matcha", "infrastructure", "matcha.state")
149-
os.remove(state_file_path)
150-
151-
# Invoke get command
152-
result = runner.invoke(app, ["get"])
153-
154-
assert result.exit_code == 0
155-
assert "Error - matcha.state file does not exist at" in str(result.stdout)
156-
157-
mock_provisioned_remote_state.use_lock.assert_not_called()
158-
159-
160139
def test_cli_get_command_hide_sensitive(
161140
runner: CliRunner,
162141
expected_output_lines: List[str],

0 commit comments

Comments
 (0)