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

[RPD-273] Improvements to the provision user experience #173

Merged
merged 5 commits into from
Jul 26, 2023
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
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ repos:
rev: v1.16.1
hooks:
- id: typos
args: [--config=.typos.toml]
2 changes: 1 addition & 1 deletion .typos.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[files]
extend-exclude = ["*.json", "*.js", "*.ipynb", "LICENSE", "*.css"]
extend-exclude = ["*.json", "*.js", "*.ipynb", "LICENSE", "*.css", ".gitignore"]

[default.extend-identifiers]
HashiCorp = "HashiCorp"
Expand Down
2 changes: 1 addition & 1 deletion docs/azure-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ As a provisioning tool, Matcha interacts with Azure on your behalf, hiding away

Your account is required to have **either**:

1. Owner; _OR_
1. Owner; _OR_
2. A combination of: Contributor + User Access Administrator

> Note: These are high level roles with a lot of privileges and we're actively working on introducing more granular permissions.
Expand Down
2 changes: 1 addition & 1 deletion docs/references.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# API Reference Documentation

::: src.matcha_ml.core.core
::: src.matcha_ml.core.core
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,5 @@ module = [
ignore_missing_imports = true

[tool.ruff.pylint]
max-branches = 13
max-branches = 14
max-args = 6
11 changes: 11 additions & 0 deletions src/matcha_ml/cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@
("Azure Resource Group", "The resource group containing the provisioned resources"),
("Matcha State Container", "A storage container for tracking matcha state"),
]

INFRA_FACTS = [
"Did you know that Matcha tea was created by accident?",
"The brewing temperature of the water affects the taste of Matcha",
"Samurai's drank Matcha before battles",
"Matcha is provisioning Kubernetes which orchestrates tools",
"Seldon Core is used for model deployment",
"MLflow is used as an experiment tracker",
"Matcha is maintained by Fuzzy Labs",
"Everything being provisioned is fully open source",
]
9 changes: 7 additions & 2 deletions src/matcha_ml/cli/ui/spinner.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ def __init__(self, status: str):
)
self.progress.add_task(description=status, total=None)

def __enter__(self) -> None:
"""Call when a spinner object is created using a `with` statement."""
def __enter__(self) -> "Spinner":
"""Call when a spinner object is created using a `with` statement.

Returns:
Spinner: the instance for a context manager.
"""
self.progress.start()
return self

def __exit__(
self,
Expand Down
22 changes: 22 additions & 0 deletions src/matcha_ml/cli/ui/status_message_builders.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""UI status message builders."""
import time
from random import shuffle
from typing import List, Optional, Tuple

from rich.console import Console

from matcha_ml.cli.constants import INFRA_FACTS
from matcha_ml.cli.ui.spinner import Spinner

err_console = Console(stderr=True)


Expand Down Expand Up @@ -83,3 +88,20 @@ def build_warning_status(status: str) -> str:
str: formatted message
"""
return f"[yellow]{status}[/yellow]"


def terraform_status_update(spinner: Spinner) -> None:
"""Outputs some facts about the deployment and matcha tea while the terraform functions are running.

Args:
spinner: The rich spinner that the messages are printed above.
"""
infra_facts_shuffled = list(range(len(INFRA_FACTS)))
shuffle(infra_facts_shuffled)

time.sleep(10) # there should be a delay prior to spitting facts.

while True:
fact = INFRA_FACTS[infra_facts_shuffled.pop()]
spinner.progress.console.print(build_status(fact))
time.sleep(10)
4 changes: 1 addition & 3 deletions src/matcha_ml/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,5 @@ def __init__(self, tf_error: str, *args: Any, **kwargs: Any):
*args: args
**kwargs: kwargs
"""
message = (
f"Terraform failed because of the following error: '{tf_error}'."
)
message = f"Terraform failed because of the following error: '{tf_error}'."
super().__init__(message, *args, **kwargs)
32 changes: 21 additions & 11 deletions src/matcha_ml/runners/base_runner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Run terraform templates to provision and deprovision resources."""
import os
from multiprocessing.pool import ThreadPool
from typing import Optional, Tuple

import typer
Expand All @@ -10,9 +11,13 @@
from matcha_ml.cli.ui.status_message_builders import (
build_status,
build_substep_success_status,
terraform_status_update,
)
from matcha_ml.errors import MatchaTerraformError
from matcha_ml.services.terraform_service import TerraformConfig, TerraformService
from matcha_ml.services.terraform_service import (
TerraformConfig,
TerraformService,
)

SPINNER = "dots"

Expand Down Expand Up @@ -95,11 +100,11 @@ def _initialize_terraform(self, msg: str = "", destroy: bool = False) -> None:
)

with Spinner("Initializing"):
ret_code, _, err = self.tfs.init()
tf_result = self.tfs.init()

if ret_code != 0:
if tf_result.return_code != 0:
print_error("The command 'terraform init' failed.")
raise MatchaTerraformError(tf_error=err)
raise MatchaTerraformError(tf_error=tf_result.std_err)

print_status(
build_substep_success_status(
Expand Down Expand Up @@ -132,11 +137,16 @@ def _apply_terraform(self) -> None:
Raises:
MatchaTerraformError: if 'terraform apply' failed.
"""
with Spinner("Applying"):
ret_code, _, err = self.tfs.apply()
with Spinner("Applying") as spinner:
pool = ThreadPool(processes=1)
_ = pool.apply_async(terraform_status_update, (spinner,))

tf_result = self.tfs.apply()

pool.terminate()

if ret_code != 0:
raise MatchaTerraformError(tf_error=err)
if tf_result.return_code != 0:
raise MatchaTerraformError(tf_error=tf_result.std_err)
print_status(
build_substep_success_status(
f"{Emojis.CHECKMARK.value} Matcha resources have been provisioned!\n"
Expand All @@ -156,10 +166,10 @@ def _destroy_terraform(self, msg: str = "") -> None:
)
print()
with Spinner("Destroying"):
ret_code, _, err = self.tfs.destroy()
tf_result = self.tfs.destroy()

if ret_code != 0:
raise MatchaTerraformError(tf_error=err)
if tf_result.return_code != 0:
raise MatchaTerraformError(tf_error=tf_result.std_err)

def provision(self) -> Optional[Tuple[str, str, str]]:
"""Provision resources required for the deployment."""
Expand Down
4 changes: 1 addition & 3 deletions src/matcha_ml/runners/remote_state_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ def _get_terraform_output(self) -> Tuple[str, str, str]:

prefix = "remote_state_storage"
account_name = tf_outputs[f"{prefix}_account_name"]["value"]
resource_group_name = tf_outputs[f"{prefix}_resource_group_name"][
"value"
]
resource_group_name = tf_outputs[f"{prefix}_resource_group_name"]["value"]
container_name = tf_outputs[f"{prefix}_container_name"]["value"]

return account_name, container_name, resource_group_name
Expand Down
6 changes: 4 additions & 2 deletions src/matcha_ml/services/analytics_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ class AnalyticsEvent(str, Enum):


def execute_analytics_event(
func: Callable, *args, **kwargs
func: Callable[..., Any], *args: Any, **kwargs: Any
) -> Tuple[Optional[MatchaState], Any]:
"""Exists to Temporarily fix misleading error messages coming from track decorator.

Args:
func (Callable): The function decorated by track.
*args (Any): arguments passed to the function.
**kwargs (Any): additional key word arguments passed to the function.

Returns:
The result of the call to func, the error code.
Expand Down Expand Up @@ -114,7 +116,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:
global_params.user_id,
event_name.value,
{
"time_taken": te - ts,
"time_taken": float(te) - float(ts), # type: ignore
"error_type": f"{error_code.__class__}.{error_code.__class__.__name__}",
"command_succeeded": error_code is None,
"matcha_state_uuid": matcha_state_uuid,
Expand Down
27 changes: 17 additions & 10 deletions src/matcha_ml/services/terraform_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
import glob
import os
from pathlib import Path
from typing import Optional, Tuple
from typing import Optional

import python_terraform


@dataclasses.dataclass
class TerraformResult:
"""A class to hold the result of the terraform commands."""

return_code: int
std_out: str
std_err: str


@dataclasses.dataclass
class TerraformConfig:
"""Configuration required for terraform."""
Expand Down Expand Up @@ -66,9 +75,7 @@ def check_installation(self) -> bool:

return True

def verify_kubectl_config_file(
self, config_path: str = ".kube/config"
) -> None:
def verify_kubectl_config_file(self, config_path: str = ".kube/config") -> None:
"""Checks if kubeconfig is present at location ~/.kube/config.

Args:
Expand Down Expand Up @@ -122,7 +129,7 @@ def get_tf_state_dir(self) -> Path:
"""
return Path(os.path.join(self.config.working_dir, ".terraform"))

def init(self) -> Tuple[int, str, str]:
def init(self) -> TerraformResult:
"""Run `terraform init` with the initialized Terraform client from the python_terraform module.

Returns:
Expand All @@ -133,9 +140,9 @@ def init(self) -> Tuple[int, str, str]:
raise_on_error=False,
)

return ret_code, out, err
return TerraformResult(ret_code, out, err)

def apply(self) -> Tuple[int, str, str]:
def apply(self) -> TerraformResult:
"""Run `terraform apply` with the initialized Terraform client from the python_terraform module.

Returns:
Expand All @@ -150,9 +157,9 @@ def apply(self) -> Tuple[int, str, str]:
auto_approve=True,
)

return ret_code, out, err
return TerraformResult(ret_code, out, err)

def destroy(self) -> Tuple[int, str, str]:
def destroy(self) -> TerraformResult:
"""Destroy the provisioned resources with the initialized Terraform client from the python_terraform module..

Returns:
Expand All @@ -166,4 +173,4 @@ def destroy(self) -> Tuple[int, str, str]:
auto_approve=True,
)

return ret_code, out, err
return TerraformResult(ret_code, out, err)
15 changes: 10 additions & 5 deletions tests/test_runners/test_base_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from matcha_ml.errors import MatchaTerraformError
from matcha_ml.runners.base_runner import BaseRunner
from matcha_ml.services.terraform_service import TerraformResult

CLASS_STUB = "matcha_ml.runners.base_runner.BaseRunner"

Expand Down Expand Up @@ -101,7 +102,7 @@ def test_initialize_terraform(capsys: SysCapture):
assert expected in captured.out

with mock.patch.object(template_runner.tf_state_dir, "exists", return_value=False):
template_runner.tfs.init = MagicMock(return_value=(0, "", ""))
template_runner.tfs.init = MagicMock(return_value=TerraformResult(0, "", ""))
expected = " initialized!"
template_runner._initialize_terraform()

Expand All @@ -117,15 +118,17 @@ def test_apply_terraform(capsys: SysCapture):
capsys (SysCapture): fixture to capture stdout and stderr
"""
template_runner = BaseRunner()
template_runner.tfs.apply = MagicMock(return_value=(0, "", ""))
template_runner.tfs.apply = MagicMock(return_value=TerraformResult(0, "", ""))
expected = "Matcha resources have been provisioned!"

template_runner._apply_terraform()
captured = capsys.readouterr()

assert expected in captured.out

template_runner.tfs.apply = MagicMock(return_value=(1, "", "Apply failed"))
template_runner.tfs.apply = MagicMock(
return_value=TerraformResult(1, "", "Apply failed")
)

with pytest.raises(MatchaTerraformError) as exc_info:
template_runner._apply_terraform()
Expand All @@ -143,7 +146,7 @@ def test_destroy_terraform(capsys: SysCapture):
template_runner (AzureTemplateRunner): a AzureTemplateRunner object instance
"""
template_runner = BaseRunner()
template_runner.tfs.destroy = MagicMock(return_value=(0, "", ""))
template_runner.tfs.destroy = MagicMock(return_value=TerraformResult(0, "", ""))

expected = "Destroying your resources"

Expand All @@ -155,7 +158,9 @@ def test_destroy_terraform(capsys: SysCapture):

assert expected in captured.out

template_runner.tfs.destroy = MagicMock(return_value=(1, "", "Init failed"))
template_runner.tfs.destroy = MagicMock(
return_value=TerraformResult(1, "", "Init failed")
)

with pytest.raises(MatchaTerraformError) as exc_info:
template_runner._destroy_terraform()
Expand Down
11 changes: 7 additions & 4 deletions tests/test_services/test_terraform_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import pytest
from python_terraform import TerraformCommandError

from matcha_ml.services.terraform_service import TerraformConfig, TerraformService
from matcha_ml.services.terraform_service import (
TerraformConfig,
TerraformService,
)


@pytest.fixture
Expand Down Expand Up @@ -218,7 +221,7 @@ def test_init(terraform_test_config: TerraformConfig):

tfs.terraform_client.init = MagicMock(return_value=(0, "", ""))

_, _, _ = tfs.init()
_ = tfs.init()

tfs.terraform_client.init.assert_called()

Expand All @@ -233,7 +236,7 @@ def test_apply(terraform_test_config: TerraformConfig):

tfs.terraform_client.apply = MagicMock(return_value=(0, "", ""))

_, _, _ = tfs.apply()
_ = tfs.apply()

tfs.terraform_client.apply.assert_called()

Expand All @@ -248,6 +251,6 @@ def test_destroy(terraform_test_config: TerraformConfig):

tfs.terraform_client.destroy = MagicMock(return_value=(0, "", ""))

_, _, _ = tfs.destroy()
_ = tfs.destroy()

tfs.terraform_client.destroy.assert_called()