From 08521957299709277f7376ea74e1fccff8a6e907 Mon Sep 17 00:00:00 2001 From: Nikita Melkozerov Date: Fri, 27 Oct 2023 13:48:36 +0000 Subject: [PATCH] add is_configured and enabled fields to cloud accounts --- fixbackend/cloud_accounts/models/__init__.py | 2 + fixbackend/cloud_accounts/models/orm.py | 13 +++++- fixbackend/cloud_accounts/repository.py | 4 ++ fixbackend/cloud_accounts/service_impl.py | 10 ++++ ...0Z_add_configured_flag_to_cloud_account.py | 25 ++++++++++ static/openapi-events.yaml | 17 +++++++ .../cloud_accounts/repository_test.py | 2 + .../fixbackend/cloud_accounts/router_test.py | 10 ++++ .../fixbackend/cloud_accounts/service_test.py | 46 +++++++++++++++++++ .../dispatcher/dispatcher_service_test.py | 9 +++- 10 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/2023-10-27T12:46:40Z_add_configured_flag_to_cloud_account.py diff --git a/fixbackend/cloud_accounts/models/__init__.py b/fixbackend/cloud_accounts/models/__init__.py index 88103516..c0d6520f 100644 --- a/fixbackend/cloud_accounts/models/__init__.py +++ b/fixbackend/cloud_accounts/models/__init__.py @@ -58,6 +58,8 @@ class CloudAccount: workspace_id: WorkspaceId name: Optional[str] access: CloudAccess + is_configured: bool + enabled: bool @frozen diff --git a/fixbackend/cloud_accounts/models/orm.py b/fixbackend/cloud_accounts/models/orm.py index f1076e10..36bf490b 100644 --- a/fixbackend/cloud_accounts/models/orm.py +++ b/fixbackend/cloud_accounts/models/orm.py @@ -17,7 +17,7 @@ from typing import Optional from fastapi_users_db_sqlalchemy.generics import GUID -from sqlalchemy import ForeignKey, String, UniqueConstraint +from sqlalchemy import ForeignKey, String, UniqueConstraint, Boolean from sqlalchemy.orm import Mapped, mapped_column from fixbackend.base_model import Base @@ -35,6 +35,8 @@ class CloudAccount(Base): aws_external_id: Mapped[ExternalId] = mapped_column(GUID, nullable=False) aws_role_name: Mapped[str] = mapped_column(String(length=64), nullable=False) name: Mapped[Optional[str]] = mapped_column(String(length=64), nullable=True) + is_configured: Mapped[bool] = mapped_column(Boolean, nullable=False) + enabled: Mapped[bool] = mapped_column(Boolean, nullable=False) __table_args__ = (UniqueConstraint("tenant_id", "account_id"),) def to_model(self) -> models.CloudAccount: @@ -47,4 +49,11 @@ def access() -> models.CloudAccess: case _: raise ValueError(f"Unknown cloud {self.cloud}") - return models.CloudAccount(id=self.id, workspace_id=self.tenant_id, access=access(), name=self.name) + return models.CloudAccount( + id=self.id, + workspace_id=self.tenant_id, + access=access(), + name=self.name, + is_configured=self.is_configured, + enabled=self.enabled, + ) diff --git a/fixbackend/cloud_accounts/repository.py b/fixbackend/cloud_accounts/repository.py index 34ee1768..4acfd7c1 100644 --- a/fixbackend/cloud_accounts/repository.py +++ b/fixbackend/cloud_accounts/repository.py @@ -61,6 +61,8 @@ async def create(self, cloud_account: CloudAccount) -> CloudAccount: aws_role_name=cloud_account.access.role_name, aws_external_id=cloud_account.access.external_id, name=cloud_account.name, + is_configured=cloud_account.is_configured, + enabled=cloud_account.enabled, ) else: raise ValueError(f"Unknown cloud {cloud_account.access}") @@ -82,6 +84,8 @@ async def update(self, id: FixCloudAccountId, cloud_account: CloudAccount) -> Cl raise ValueError(f"Cloud account {id} not found") stored_account.name = cloud_account.name + stored_account.is_configured = cloud_account.is_configured + stored_account.enabled = cloud_account.enabled match cloud_account.access: case AwsCloudAccess(account_id, external_id, role_name): diff --git a/fixbackend/cloud_accounts/service_impl.py b/fixbackend/cloud_accounts/service_impl.py index be73bf59..3b0a3071 100644 --- a/fixbackend/cloud_accounts/service_impl.py +++ b/fixbackend/cloud_accounts/service_impl.py @@ -116,6 +116,14 @@ async def process_domain_event(self, message: Json, context: MessageContext) -> ), ) + case AwsAccountConfigured.kind: + configured_event = AwsAccountConfigured.from_json(message) + account = await self.cloud_account_repository.get(configured_event.cloud_account_id) + if account is None: + log.warning(f"Account {configured_event.cloud_account_id} not found, cannot mark as configured") + return None + await self.cloud_account_repository.update(account.id, evolve(account, is_configured=True)) + case _: pass # ignore other domain events @@ -185,6 +193,8 @@ async def account_already_exists(workspace_id: WorkspaceId, account_id: str) -> workspace_id=workspace_id, access=AwsCloudAccess(aws_account_id=account_id, external_id=external_id, role_name=role_name), name=None, + is_configured=False, + enabled=True, ) if existing := await account_already_exists(workspace_id, account_id): account = evolve(account, id=existing.id) diff --git a/migrations/versions/2023-10-27T12:46:40Z_add_configured_flag_to_cloud_account.py b/migrations/versions/2023-10-27T12:46:40Z_add_configured_flag_to_cloud_account.py new file mode 100644 index 00000000..e55ba550 --- /dev/null +++ b/migrations/versions/2023-10-27T12:46:40Z_add_configured_flag_to_cloud_account.py @@ -0,0 +1,25 @@ +"""add configured flag to cloud account + +Revision ID: e5a452318fa7 +Revises: 69f29fc94a5c +Create Date: 2023-10-27 12:46:40.781041+00:00 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "e5a452318fa7" +down_revision: Union[str, None] = "69f29fc94a5c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("cloud_account", sa.Column("is_configured", sa.Boolean(), nullable=False, server_default=sa.false())) + op.add_column("cloud_account", sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true())) + # ### end Alembic commands ### diff --git a/static/openapi-events.yaml b/static/openapi-events.yaml index 1f5a231d..83bd4d12 100644 --- a/static/openapi-events.yaml +++ b/static/openapi-events.yaml @@ -66,3 +66,20 @@ components: aws_account_id: type: string description: Id of the aws account. + + AwsAccountConfigured: + description: "This event emitted when a new aws account has been configured." + type: object + properties: + cloud_account_id: + type: string + description: Id of the cloud account. + tenant_id: + type: string + description: Id of the workspace. + cloud: + type: string + description: Name of the cloud. + aws_account_id: + type: string + description: Id of the aws account. \ No newline at end of file diff --git a/tests/fixbackend/cloud_accounts/repository_test.py b/tests/fixbackend/cloud_accounts/repository_test.py index 157ed3b7..0eac33bc 100644 --- a/tests/fixbackend/cloud_accounts/repository_test.py +++ b/tests/fixbackend/cloud_accounts/repository_test.py @@ -41,6 +41,8 @@ async def test_create_cloud_account( external_id=ExternalId(uuid.uuid4()), ), name="foo", + is_configured=False, + enabled=True, ) # create diff --git a/tests/fixbackend/cloud_accounts/router_test.py b/tests/fixbackend/cloud_accounts/router_test.py index 7e51c4a5..a8ea51e9 100644 --- a/tests/fixbackend/cloud_accounts/router_test.py +++ b/tests/fixbackend/cloud_accounts/router_test.py @@ -47,6 +47,8 @@ async def create_aws_account( workspace_id=workspace_id, access=AwsCloudAccess(account_id, external_id, role_name), name=None, + is_configured=False, + enabled=True, ) self.accounts[account.id] = account return account @@ -128,6 +130,8 @@ async def test_delete_cloud_account(client: AsyncClient) -> None: workspace_id=workspace_id, access=AwsCloudAccess(account_id, external_id, role_name), name="foo", + is_configured=False, + enabled=True, ) response = await client.delete(f"/api/workspaces/{workspace_id}/cloud_account/{cloud_account_id}") assert response.status_code == 200 @@ -171,6 +175,8 @@ async def test_get_cloud_account(client: AsyncClient) -> None: workspace_id=workspace_id, access=AwsCloudAccess(account_id, external_id, role_name), name="foo", + is_configured=False, + enabled=True, ) response = await client.get(f"/api/workspaces/{workspace_id}/cloud_account/{cloud_account_id}") @@ -191,6 +197,8 @@ async def test_list_cloud_accounts(client: AsyncClient) -> None: workspace_id=workspace_id, access=AwsCloudAccess(account_id, external_id, role_name), name="foo", + is_configured=False, + enabled=True, ) response = await client.get(f"/api/workspaces/{workspace_id}/cloud_accounts") @@ -212,6 +220,8 @@ async def test_update_cloud_account(client: AsyncClient) -> None: workspace_id=workspace_id, access=AwsCloudAccess(account_id, external_id, role_name), name="foo", + is_configured=False, + enabled=True, ) payload = { diff --git a/tests/fixbackend/cloud_accounts/service_test.py b/tests/fixbackend/cloud_accounts/service_test.py index 3ec9e54b..7d2ed63d 100644 --- a/tests/fixbackend/cloud_accounts/service_test.py +++ b/tests/fixbackend/cloud_accounts/service_test.py @@ -162,6 +162,9 @@ async def test_create_aws_account( assert account.access.aws_account_id == account_id assert account.access.role_name == role_name assert account.access.external_id == external_id + assert account.name is None + assert account.is_configured is False + assert account.enabled is True message = { "cloud_account_id": str(account.id), @@ -442,3 +445,46 @@ async def test_handle_account_discovered(arq_redis: Redis, default_config: Confi assert event.cloud_account_id == account1.id assert event.aws_account_id == account_id1 assert event.tenant_id == account1.workspace_id + + +@pytest.mark.asyncio +async def test_handle_account_configured(arq_redis: Redis, default_config: Config) -> None: + repository = CloudAccountRepositoryMock() + organization_repository = OrganizationServiceMock() + pubsub_publisher = RedisPubSubPublisherMock() + domain_sender = DomainEventSenderMock() + last_scan_repo = LastScanRepositoryMock() + account_setup_helper = AwsAccountSetupHelperMock() + service = CloudAccountServiceImpl( + organization_repository, + repository, + pubsub_publisher, + domain_sender, + last_scan_repo, + arq_redis, + default_config, + account_setup_helper, + account_setup_sleep_seconds=0.05, + ) + + account = await service.create_aws_account(test_workspace_id, account_id, role_name, external_id) + assert account.is_configured is False + + event = AwsAccountConfigured( + cloud_account_id=account.id, + tenant_id=account.workspace_id, + aws_account_id=account_id, + ) + # happy case, boto3 can assume role + await service.process_domain_event( + event.to_json(), MessageContext("test", event.kind, "test", datetime.utcnow(), datetime.utcnow()) + ) + + after_configured = await service.get_cloud_account(account.id, test_workspace_id) + assert after_configured is not None + assert after_configured.is_configured is True + assert after_configured.access == account.access + assert after_configured.workspace_id == account.workspace_id + assert after_configured.name == account.name + assert after_configured.id == account.id + assert after_configured.enabled == account.enabled diff --git a/tests/fixbackend/dispatcher/dispatcher_service_test.py b/tests/fixbackend/dispatcher/dispatcher_service_test.py index 2bfc8f92..49d3e07d 100644 --- a/tests/fixbackend/dispatcher/dispatcher_service_test.py +++ b/tests/fixbackend/dispatcher/dispatcher_service_test.py @@ -57,6 +57,8 @@ async def test_receive_workspace_created( organization.id, "foo", AwsCloudAccess(aws_account_id, organization.external_id, "test"), + is_configured=False, + enabled=True, ) ) # signal to the dispatcher that the new workspace was created @@ -84,7 +86,12 @@ async def test_receive_aws_account_discovered( aws_account_id = CloudAccountId("123") account = CloudAccount( - cloud_account_id, organization.id, "foo", AwsCloudAccess(aws_account_id, organization.external_id, "test") + cloud_account_id, + organization.id, + "foo", + AwsCloudAccess(aws_account_id, organization.external_id, "test"), + is_configured=False, + enabled=True, ) await cloud_account_repository.create(account)