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-259] Refactor build_state_from_terraform_output within matcha_state.py to use objects defined within matcha_state.py #190

Merged
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
6 changes: 3 additions & 3 deletions src/matcha_ml/services/analytics_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ def _get_state_uuid() -> Optional[MatchaResourceProperty]:
except MatchaError:
return None

try:
state_id_component = matcha_state_service.get_component("id")
except MatchaError:
state_id_component = matcha_state_service.get_component("id")

if state_id_component is None:
return None

matcha_state_uuid = state_id_component.find_property(property_name="matcha_uuid")
Expand Down
119 changes: 77 additions & 42 deletions src/matcha_ml/state/matcha_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import json
import os
import uuid
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple

Expand Down Expand Up @@ -80,6 +79,25 @@ class MatchaState:

components: List[MatchaStateComponent]

def get_component(self, resource_name: str) -> Optional[MatchaStateComponent]:
"""Get a component of the state given a resource name.

Args:
resource_name (str): the components resource name

Returns:
Optional[MatchaStateComponent]: the state component matching the resource name parameter.
"""
component = next(
filter(
lambda component: component.resource.name == resource_name,
self.components,
),
None,
)

return component

def to_dict(self) -> Dict[str, Dict[str, str]]:
"""Convert the MatchaState object to a dictionary.

Expand Down Expand Up @@ -129,7 +147,7 @@ class MatchaStateService:
def __init__(
self,
matcha_state: Optional[MatchaState] = None,
terraform_output: Optional[Dict[str, str]] = None,
terraform_output: Optional[Dict[str, Dict[str, str]]] = None,
) -> None:
"""Constructor for the MatchaStateService.

Expand Down Expand Up @@ -169,7 +187,7 @@ def state_exists(cls) -> bool:
return bool(os.path.isfile(cls.matcha_state_path))

def build_state_from_terraform_output(
self, terraform_output: Dict[str, str]
self, terraform_output: Dict[str, Dict[str, str]]
) -> MatchaState:
"""Builds a MatchaState class from a terraform output dictionary.

Expand All @@ -182,7 +200,7 @@ def build_state_from_terraform_output(

def _parse_terraform_output_resource_name(
output_name: str,
) -> Tuple[str, str, str]:
) -> Tuple[MatchaResource, str, str]:
"""Build resource output for each Terraform output.

Format for Terraform output names is:
Expand All @@ -195,41 +213,68 @@ def _parse_terraform_output_resource_name(
Returns:
Tuple[str, str, str]: the resource output for matcha.state.
"""
resource_type: Optional[str] = None
resource_type = None

for key in RESOURCE_NAMES:
if key in output_name:
resource_type = key
resource_type = MatchaResource(key)
break

if resource_type is None:
raise MatchaInputError(
f"A valid resource type for the output '{output_name}' does not exist."
)

flavor_and_resource_name = output_name[len(resource_type) + 1 :]
flavor_and_resource_name = output_name[len(resource_type.name) + 1 :]

flavor, resource_name = flavor_and_resource_name.split("_", maxsplit=1)
resource_name = resource_name.replace("_", "-")
resource_type = resource_type.replace("_", "-")
resource_type.name = resource_type.name.replace("_", "-")

return resource_type, flavor, resource_name

state_outputs: Dict[str, Dict[str, str]] = defaultdict(dict)
matcha_state = MatchaState(components=[])

for output_name, properties in terraform_output.items():
for output_name, output_value in terraform_output.items():
(
resource_type,
flavor,
resource_name,
) = _parse_terraform_output_resource_name(output_name)
state_outputs[resource_type].setdefault("flavor", flavor)
state_outputs[resource_type][resource_name] = properties["value"] # type: ignore

component = matcha_state.get_component(resource_type.name)

if component is not None:
# add just the properties
component.properties.append(
MatchaResourceProperty(
name=resource_name, value=output_value["value"]
)
)
else:
# add the component
matcha_state.components.append(
MatchaStateComponent(
resource=resource_type,
properties=[
MatchaResourceProperty(name="flavor", value=flavor),
MatchaResourceProperty(
name=resource_name, value=output_value["value"]
),
],
)
)

# Create a unique matcha state identifier
state_outputs["id"] = {"matcha_uuid": str(uuid.uuid4())}
matcha_uuid_component = MatchaStateComponent(
resource=MatchaResource(name="id"),
properties=[
MatchaResourceProperty(name="matcha_uuid", value=str(uuid.uuid4()))
],
)
matcha_state.components.append(matcha_uuid_component)

return MatchaState.from_dict(state_outputs)
return matcha_state

def _write_state(self, matcha_state: MatchaState) -> None:
"""Writes a given MatchaState object to the matcha.state file.
Expand Down Expand Up @@ -260,17 +305,21 @@ def is_local_state_stale(self) -> bool:
"""Checks for congruence between the local config file and the local state file."""
local_config_file = os.path.join(os.getcwd(), "matcha.config.json")

# the resource group name from the state object
matcha_state_resource_group = (
self.get_component("cloud").find_property("resource-group-name").value
)
cloud_component = self.get_component("cloud")

if self.state_exists() and os.path.exists(local_config_file):
if (
self.state_exists()
and os.path.exists(local_config_file)
and cloud_component
):
with open(local_config_file) as config:
local_config = json.load(config)

resource_group_property = cloud_component.find_property(
"resource-group-name"
).value
return bool(
matcha_state_resource_group
resource_group_property
!= local_config["remote_state_bucket"]["resource_group_name"]
)
else:
Expand All @@ -293,12 +342,14 @@ def fetch_resources_from_state_file(
if resource_name is None:
return self._state

component = self.get_component(resource_name=resource_name)

if component is None:
return MatchaState(components=[])

if property_name is None:
return MatchaState(
components=[self.get_component(resource_name=resource_name)]
)
return MatchaState(components=[component])

component = self.get_component(resource_name=resource_name)
property = component.find_property(property_name=property_name)

return MatchaState(
Expand All @@ -307,32 +358,16 @@ def fetch_resources_from_state_file(
]
)

def get_component(self, resource_name: str) -> MatchaStateComponent:
def get_component(self, resource_name: str) -> Optional[MatchaStateComponent]:
"""Get a component of the state given a resource name.

Args:
resource_name (str): the components resource name

Raises:
MatchaError: if the component cannot be found in the state.

Returns:
MatchaStateComponent: the state component matching the resource name parameter.
"""
component = next(
filter(
lambda component: component.resource.name == resource_name,
self._state.components,
),
None,
)

if component is None:
raise MatchaError(
f"The component with the name '{resource_name}' could not be found in the state."
)

return component
return self._state.get_component(resource_name=resource_name)

def get_resource_names(self) -> List[str]:
"""Method for returning all existing resource names.
Expand Down
8 changes: 2 additions & 6 deletions tests/test_state/test_matcha_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,13 +365,9 @@ def test_get_component_not_found(
matcha_state_service (MatchaStateService): the Matcha state service testing instance.
"""
invalid_resource_name = "not a resource"
with pytest.raises(MatchaError) as err:
_ = matcha_state_service.get_component(resource_name=invalid_resource_name)
component = matcha_state_service.get_component(resource_name=invalid_resource_name)

assert (
str(err.value)
== "The component with the name 'not a resource' could not be found in the state."
)
assert component is None


def test_state_component_find_property_expected(
Expand Down