ResilienceHub - Resource-related functionality (#7480)

This commit is contained in:
Bert Blommers 2024-03-17 10:36:44 +00:00 committed by GitHub
parent caac727530
commit ca5d514c61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 443 additions and 17 deletions

View File

@ -6218,13 +6218,13 @@
## resiliencehub
<details>
<summary>18% implemented</summary>
<summary>31% implemented</summary>
- [ ] add_draft_app_version_resource_mappings
- [ ] batch_update_recommendation_status
- [X] create_app
- [ ] create_app_version_app_component
- [ ] create_app_version_resource
- [X] create_app_version_app_component
- [X] create_app_version_resource
- [ ] create_recommendation_template
- [X] create_resiliency_policy
- [ ] delete_app
@ -6243,17 +6243,17 @@
- [ ] describe_app_version_template
- [ ] describe_draft_app_version_resources_import_status
- [X] describe_resiliency_policy
- [ ] import_resources_to_draft_app_version
- [X] import_resources_to_draft_app_version
- [ ] list_alarm_recommendations
- [ ] list_app_assessment_compliance_drifts
- [X] list_app_assessments
- [ ] list_app_component_compliances
- [ ] list_app_component_recommendations
- [ ] list_app_input_sources
- [ ] list_app_version_app_components
- [X] list_app_version_app_components
- [ ] list_app_version_resource_mappings
- [ ] list_app_version_resources
- [ ] list_app_versions
- [X] list_app_version_resources
- [X] list_app_versions
- [X] list_apps
- [ ] list_recommendation_templates
- [X] list_resiliency_policies
@ -6262,7 +6262,7 @@
- [X] list_tags_for_resource
- [ ] list_test_recommendations
- [ ] list_unsupported_app_version_resources
- [ ] publish_app_version
- [X] publish_app_version
- [ ] put_draft_app_version_template
- [ ] remove_draft_app_version_resource_mappings
- [ ] resolve_app_version_resources

View File

@ -21,8 +21,8 @@ resiliencehub
The ClientToken-parameter is not yet implemented
- [ ] create_app_version_app_component
- [ ] create_app_version_resource
- [X] create_app_version_app_component
- [X] create_app_version_resource
- [ ] create_recommendation_template
- [X] create_resiliency_policy
@ -45,17 +45,17 @@ resiliencehub
- [ ] describe_app_version_template
- [ ] describe_draft_app_version_resources_import_status
- [X] describe_resiliency_policy
- [ ] import_resources_to_draft_app_version
- [X] import_resources_to_draft_app_version
- [ ] list_alarm_recommendations
- [ ] list_app_assessment_compliance_drifts
- [X] list_app_assessments
- [ ] list_app_component_compliances
- [ ] list_app_component_recommendations
- [ ] list_app_input_sources
- [ ] list_app_version_app_components
- [X] list_app_version_app_components
- [ ] list_app_version_resource_mappings
- [ ] list_app_version_resources
- [ ] list_app_versions
- [X] list_app_version_resources
- [X] list_app_versions
- [X] list_apps
The FromAssessmentTime/ToAssessmentTime-parameters are not yet implemented
@ -68,7 +68,7 @@ resiliencehub
- [X] list_tags_for_resource
- [ ] list_test_recommendations
- [ ] list_unsupported_app_version_resources
- [ ] publish_app_version
- [X] publish_app_version
- [ ] put_draft_app_version_template
- [ ] remove_draft_app_version_resource_mappings
- [ ] resolve_app_version_resources

View File

@ -12,6 +12,11 @@ class AppNotFound(ResourceNotFound):
super().__init__(f"App not found for appArn {arn}")
class AppVersionNotFound(ResourceNotFound):
def __init__(self) -> None:
super().__init__("App Version not found")
class ResiliencyPolicyNotFound(ResourceNotFound):
def __init__(self, arn: str):
super().__init__(f"ResiliencyPolicy {arn} not found")

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel
@ -7,7 +7,7 @@ from moto.moto_api._internal import mock_random
from moto.utilities.paginator import paginate
from moto.utilities.tagging_service import TaggingService
from .exceptions import AppNotFound, ResiliencyPolicyNotFound
from .exceptions import AppNotFound, AppVersionNotFound, ResiliencyPolicyNotFound
PAGINATION_MODEL = {
"list_apps": {
@ -25,6 +25,20 @@ PAGINATION_MODEL = {
}
class AppComponent(BaseModel):
def __init__(self, _id: str, name: str, _type: str):
self.id = _id
self.name = name
self.type = _type
def to_json(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"type": self.type,
}
class App(BaseModel):
def __init__(
self,
@ -48,6 +62,16 @@ class App(BaseModel):
self.policy_arn = policy_arn
self.resilience_score = 0.0
self.status = "Active"
self.app_versions: List[AppVersion] = []
app_version = AppVersion(app_arn=self.arn, version_name=None, identifier=0)
self.app_versions.append(app_version)
def get_version(self, version_name: str) -> "AppVersion":
for v in self.app_versions:
if v.app_version == version_name:
return v
raise AppVersionNotFound
def to_json(self) -> Dict[str, Any]:
resp = {
@ -55,6 +79,7 @@ class App(BaseModel):
"assessmentSchedule": self.assessment_schedule,
"complianceStatus": self.compliance_status,
"creationTime": self.creation_time,
"driftStatus": "NotChecked",
"name": self.name,
"resilienceScore": self.resilience_score,
"status": self.status,
@ -71,6 +96,54 @@ class App(BaseModel):
return resp
class Resource:
def __init__(
self,
logical_resource_id: Dict[str, Any],
physical_resource_id: str,
resource_type: str,
components: List[AppComponent],
):
self.logical_resource_id = logical_resource_id
self.physical_resource_id = physical_resource_id
self.resource_type = resource_type
self.components = components
def to_json(self) -> Dict[str, Any]:
return {
"appComponents": [c.to_json() for c in self.components],
"resourceType": self.resource_type,
"logicalResourceId": self.logical_resource_id,
"physicalResourceId": {"identifier": self.physical_resource_id},
"resourceName": self.logical_resource_id["identifier"],
}
class AppVersion(BaseModel):
def __init__(self, app_arn: str, version_name: Optional[str], identifier: int):
self.app_arn = app_arn
self.eks_sources: List[Dict[str, Any]] = []
self.source_arns: List[str] = []
self.terraform_sources: List[Dict[str, str]] = []
self.app_version = "release" if version_name else "draft"
self.identifier = identifier
self.creation_time = unix_time()
self.version_name = version_name
self.app_components: List[AppComponent] = []
self.status = "Pending"
self.resources: List[Resource] = []
def to_json(self) -> Dict[str, Any]:
resp = {
"appVersion": self.app_version,
"creationTime": self.creation_time,
"identifier": self.identifier,
}
if self.version_name:
resp["versionName"] = self.version_name
return resp
class Policy(BaseModel):
def __init__(
self,
@ -208,5 +281,88 @@ class ResilienceHubBackend(BaseBackend):
def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
return self.tagger.get_tag_dict_for_resource(resource_arn)
def import_resources_to_draft_app_version(
self,
app_arn: str,
eks_sources: List[Dict[str, Any]],
source_arns: List[str],
terraform_sources: List[Dict[str, str]],
) -> AppVersion:
app = self.describe_app(app_arn)
app_version = app.get_version("draft")
app_version.eks_sources.extend(eks_sources)
app_version.source_arns.extend(source_arns)
app_version.terraform_sources.extend(terraform_sources)
# Default AppComponent when importing data
# AWS seems to create other components as well, based on the provided sources
app_version.app_components.append(
AppComponent(
_id="appcommon",
name="appcommon",
_type="AWS::ResilienceHub::AppCommonAppComponent",
)
)
return app_version
def create_app_version_app_component(
self, app_arn: str, name: str, _type: str
) -> AppComponent:
app = self.describe_app(app_arn)
app_version = app.get_version("draft")
component = AppComponent(_id=name, name=name, _type=_type)
app_version.app_components.append(component)
return component
def list_app_version_app_components(
self, app_arn: str, app_version: str
) -> List[AppComponent]:
app = self.describe_app(app_arn)
return app.get_version(app_version).app_components
def create_app_version_resource(
self,
app_arn: str,
app_components: List[str],
logical_resource_id: Dict[str, str],
physical_resource_id: str,
resource_type: str,
) -> Resource:
app = self.describe_app(app_arn)
app_version = app.get_version("draft")
components = [c for c in app_version.app_components if c.id in app_components]
resource = Resource(
logical_resource_id=logical_resource_id,
physical_resource_id=physical_resource_id,
resource_type=resource_type,
components=components,
)
app_version.resources.append(resource)
return resource
def list_app_version_resources(
self, app_arn: str, app_version: str
) -> List[Resource]:
app = self.describe_app(app_arn)
return app.get_version(app_version).resources
def list_app_versions(self, app_arn: str) -> List[AppVersion]:
app = self.describe_app(app_arn)
return app.app_versions
def publish_app_version(self, app_arn: str, version_name: str) -> AppVersion:
app = self.describe_app(app_arn)
version = AppVersion(
app_arn=app_arn, version_name=version_name, identifier=len(app.app_versions)
)
for old_version in app.app_versions:
if old_version.app_version == "release":
old_version.app_version = str(old_version.identifier)
app.app_versions.append(version)
return version
resiliencehub_backends = BackendDict(ResilienceHubBackend, "resiliencehub")

View File

@ -157,3 +157,99 @@ class ResilienceHubResponse(BaseResponse):
resource_arn=resource_arn,
)
return json.dumps(dict(tags=tags))
def import_resources_to_draft_app_version(self) -> str:
app_arn = self._get_param("appArn")
eks_sources = self._get_param("eksSources")
source_arns = self._get_param("sourceArns")
terraform_sources = self._get_param("terraformSources")
app_version = self.resiliencehub_backend.import_resources_to_draft_app_version(
app_arn=app_arn,
eks_sources=eks_sources,
source_arns=source_arns,
terraform_sources=terraform_sources,
)
return json.dumps(
{
"appArn": app_version.app_arn,
"appVersion": app_version.version_name,
"eksSources": eks_sources,
"sourceArns": source_arns,
"status": app_version.status,
"terraformSources": terraform_sources,
}
)
def list_app_version_app_components(self) -> str:
app_arn = self._get_param("appArn")
app_version = self._get_param("appVersion")
components = self.resiliencehub_backend.list_app_version_app_components(
app_arn, app_version
)
return json.dumps(
{
"appArn": app_arn,
"appVersion": app_version,
"appComponents": [c.to_json() for c in components],
}
)
def create_app_version_app_component(self) -> str:
app_arn = self._get_param("appArn")
name = self._get_param("name")
_type = self._get_param("type")
component = self.resiliencehub_backend.create_app_version_app_component(
app_arn=app_arn,
name=name,
_type=_type,
)
return json.dumps(
{
"appArn": app_arn,
"appComponent": component.to_json(),
"appVersion": "draft",
}
)
def create_app_version_resource(self) -> str:
app_arn = self._get_param("appArn")
app_components = self._get_param("appComponents")
logical_resource_id = self._get_param("logicalResourceId")
physical_resource_id = self._get_param("physicalResourceId")
resource_type = self._get_param("resourceType")
resource = self.resiliencehub_backend.create_app_version_resource(
app_arn=app_arn,
app_components=app_components,
logical_resource_id=logical_resource_id,
physical_resource_id=physical_resource_id,
resource_type=resource_type,
)
return json.dumps(
{
"appArn": app_arn,
"appVersion": "draft",
"physicalResource": resource.to_json(),
}
)
def list_app_version_resources(self) -> str:
app_arn = self._get_param("appArn")
app_version = self._get_param("appVersion")
resources = self.resiliencehub_backend.list_app_version_resources(
app_arn, app_version
)
return json.dumps({"physicalResources": [r.to_json() for r in resources]})
def list_app_versions(self) -> str:
app_arn = self._get_param("appArn")
versions = self.resiliencehub_backend.list_app_versions(app_arn)
return json.dumps({"appVersions": [v.to_json() for v in versions]})
def publish_app_version(self) -> str:
app_arn = self._get_param("appArn")
version_name = self._get_param("versionName")
version = self.resiliencehub_backend.publish_app_version(app_arn, version_name)
return json.dumps({"appArn": app_arn, **version.to_json()})

View File

@ -7,12 +7,19 @@ url_bases = [
url_paths = {
"{0}/create-app$": ResilienceHubResponse.dispatch,
"{0}/create-app-version-app-component$": ResilienceHubResponse.dispatch,
"{0}/create-app-version-resource$": ResilienceHubResponse.dispatch,
"{0}/create-resiliency-policy$": ResilienceHubResponse.dispatch,
"{0}/describe-app$": ResilienceHubResponse.dispatch,
"{0}/describe-resiliency-policy$": ResilienceHubResponse.dispatch,
"{0}/import-resources-to-draft-app-version$": ResilienceHubResponse.dispatch,
"{0}/list-apps$": ResilienceHubResponse.dispatch,
"{0}/list-app-assessments$": ResilienceHubResponse.dispatch,
"{0}/list-app-versions$": ResilienceHubResponse.dispatch,
"{0}/list-app-version-app-components$": ResilienceHubResponse.dispatch,
"{0}/list-app-version-resources$": ResilienceHubResponse.dispatch,
"{0}/list-resiliency-policies$": ResilienceHubResponse.dispatch,
"{0}/publish-app-version$": ResilienceHubResponse.dispatch,
"{0}/tags/.+$": ResilienceHubResponse.dispatch,
"{0}/tags/(?P<arn_prefix>[^/]+)/(?P<workspace_id>[^/]+)$": ResilienceHubResponse.method_dispatch(
ResilienceHubResponse.tags # type: ignore

View File

@ -0,0 +1,162 @@
import boto3
from moto import mock_aws
@mock_aws
def test_import_resources_to_draft_app_version():
client = boto3.client("resiliencehub", region_name="us-east-2")
app_arn = client.create_app(name="myapp")["app"]["appArn"]
components = client.list_app_version_app_components(
appArn=app_arn, appVersion="draft"
)
assert components["appArn"] == app_arn
assert components["appComponents"] == []
assert components["appVersion"] == "draft"
resp = client.import_resources_to_draft_app_version(
appArn=app_arn,
eksSources=[{"eksClusterArn": "some-arn", "namespaces": ["eks/ns/1"]}],
importStrategy="AddOnly",
sourceArns=["sarn1", "sarn2"],
terraformSources=[{"s3StateFileUrl": "tf://url"}],
)
assert resp["appArn"] == app_arn
assert resp["eksSources"] == [
{"eksClusterArn": "some-arn", "namespaces": ["eks/ns/1"]}
]
assert resp["sourceArns"] == ["sarn1", "sarn2"]
assert resp["status"] == "Pending"
assert resp["terraformSources"] == [{"s3StateFileUrl": "tf://url"}]
components = client.list_app_version_app_components(
appArn=app_arn, appVersion="draft"
)["appComponents"]
assert {
"id": "appcommon",
"name": "appcommon",
"type": "AWS::ResilienceHub::AppCommonAppComponent",
} in components
@mock_aws
def test_create_app_version_app_component():
client = boto3.client("resiliencehub", region_name="us-east-2")
app_arn = client.create_app(name="myapp")["app"]["appArn"]
component = client.create_app_version_app_component(
appArn=app_arn,
name="my_databases",
type="AWS::ResilienceHub::DatabaseAppComponent",
)
assert component["appArn"] == app_arn
assert component["appVersion"] == "draft"
assert component["appComponent"]["id"] == "my_databases"
assert component["appComponent"]["name"] == "my_databases"
assert (
component["appComponent"]["type"] == "AWS::ResilienceHub::DatabaseAppComponent"
)
components = client.list_app_version_app_components(
appArn=app_arn, appVersion="draft"
)["appComponents"]
assert components == [
{
"id": "my_databases",
"name": "my_databases",
"type": "AWS::ResilienceHub::DatabaseAppComponent",
}
]
@mock_aws
def test_create_app_version_resource():
client = boto3.client("resiliencehub", region_name="us-east-2")
app_arn = client.create_app(name="myapp")["app"]["appArn"]
component = client.create_app_version_app_component(
appArn=app_arn,
name="my_databases",
type="AWS::ResilienceHub::DatabaseAppComponent",
)["appComponent"]
resp = client.create_app_version_resource(
appArn=app_arn,
appComponents=["my_databases"],
logicalResourceId={
"identifier": "myres",
},
physicalResourceId="myphys",
resourceType="AWS::Lambda::Function",
)
assert resp["appArn"] == app_arn
assert resp["appVersion"] == "draft"
assert resp["physicalResource"]["appComponents"] == [component]
assert resp["physicalResource"]["logicalResourceId"] == {"identifier": "myres"}
assert resp["physicalResource"]["physicalResourceId"]["identifier"] == "myphys"
assert resp["physicalResource"]["resourceName"] == "myres"
assert resp["physicalResource"]["resourceType"] == "AWS::Lambda::Function"
resources = client.list_app_version_resources(appArn=app_arn, appVersion="draft")
assert resources["physicalResources"] == [resp["physicalResource"]]
@mock_aws
def test_create_app_version_resource_with_unknown_component():
client = boto3.client("resiliencehub", region_name="us-east-2")
app_arn = client.create_app(name="myapp")["app"]["appArn"]
# Not sure how AWS behaves, when providing an unknown appComponent
# But let's try to be flexible in what we accept
resp = client.create_app_version_resource(
appArn=app_arn,
appComponents=["unknown"],
logicalResourceId={
"identifier": "myres",
},
physicalResourceId="myphys",
resourceType="AWS::Lambda::Function",
)
assert resp["physicalResource"]["appComponents"] == []
@mock_aws
def test_publish():
client = boto3.client("resiliencehub", region_name="us-east-2")
app_arn = client.create_app(name="myapp")["app"]["appArn"]
versions = client.list_app_versions(appArn=app_arn)["appVersions"]
assert len(versions) == 1
assert versions[0]["appVersion"] == "draft"
assert versions[0]["creationTime"]
assert versions[0]["identifier"] == 0
client.import_resources_to_draft_app_version(
appArn=app_arn,
eksSources=[{"eksClusterArn": "some-arn", "namespaces": ["eks/ns/1"]}],
importStrategy="AddOnly",
sourceArns=["sarn1", "sarn2"],
terraformSources=[{"s3StateFileUrl": "tf://url"}],
)
publish = client.publish_app_version(appArn=app_arn, versionName="v1")
assert publish["appArn"] == app_arn
assert publish["appVersion"] == "release"
assert publish["identifier"] == 1
assert publish["versionName"] == "v1"
versions = client.list_app_versions(appArn=app_arn)["appVersions"]
assert len(versions) == 2
assert versions[1]["appVersion"] == "release"
assert versions[1]["identifier"] == 1
assert versions[1]["versionName"] == "v1"
client.publish_app_version(appArn=app_arn, versionName="v2")
versions = client.list_app_versions(appArn=app_arn)["appVersions"]
assert len(versions) == 3
for v in versions:
del v["creationTime"]
assert {"appVersion": "release", "identifier": 2, "versionName": "v2"} in versions
assert {"appVersion": "1", "identifier": 1, "versionName": "v1"} in versions