diff --git a/.gitignore b/.gitignore index bfece706e..bd08efa82 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,7 @@ dmypy.json .pyre/ # iambic +proposed_changes.txt proposed_changes.json proposed_changes.yaml .DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json index 020b9fe0a..d52012137 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,50 @@ "console": "integratedTerminal", "justMyCode": false }, + { + "name": "Iambic: Functional Tests", + "type": "python", + "request": "launch", + "module": "pytest", + "console": "integratedTerminal", + "args": [ + "--disable-warnings", + "--cov=iambic", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", + "--cov-branch", + "--cov-fail-under=100", + "--junitxml=functional_tests/results.xml", + "--color=yes", + "--verbose", + "${workspaceFolder}/functional_tests", + ], + "envFile": "${workspaceFolder}/.env", + "justMyCode": false + }, + { + "name": "Iambic: Specific Functional Test", + "type": "python", + "request": "launch", + "module": "pytest", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + "args": [ + "--disable-warnings", + "--cov=iambic", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", + "--cov-branch", + "--cov-fail-under=100", + "--junitxml=functional_tests/results.xml", + "--color=yes", + "--verbose", + "${workspaceFolder}/functional_tests/test_google_workspace.py::test_google", + ], + "justMyCode": false + }, { "name": "Iambic: Plan", "type": "python", diff --git a/.vscode/settings.json b/.vscode/settings.json index bee6417c2..5b250c205 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,5 @@ { - "python.testing.pytestArgs": [ - ".", - "-s" - ], + "python.testing.pytestArgs": [], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.formatting.provider": "black", @@ -11,12 +8,12 @@ "-s", "./iambic", "-p", - "*_test.py" + "*_test.py", ], "coverage-gutters.coverageFileNames": [ "cov_unit_tests.xml" ], "coverage-gutters.coverageBaseDir": ".", "coverage-gutters.showGutterCoverage": false, - "coverage-gutters.showLineCoverage": true + "coverage-gutters.showLineCoverage": true, } \ No newline at end of file diff --git a/functional_tests/aws/group/test_create_template.py b/functional_tests/aws/group/test_create_template.py index f951f618b..b8bb0ac15 100644 --- a/functional_tests/aws/group/test_create_template.py +++ b/functional_tests/aws/group/test_create_template.py @@ -5,6 +5,7 @@ from functional_tests.aws.group.utils import generate_group_template_from_base from functional_tests.aws.user.utils import get_modifiable_user from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.iam.group.utils import get_group_across_accounts from iambic.plugins.v0_1_0.aws.iam.user.utils import get_user_groups from iambic.plugins.v0_1_0.aws.utils import boto_crud_call @@ -30,9 +31,10 @@ async def test_create_group_all_accounts(self): self.template.included_accounts = ["*"] self.template.excluded_accounts = [] - await self.template.apply( + changes = await self.template.apply( IAMBIC_TEST_DETAILS.config.aws, ) + screen_render_resource_changes([changes]) account_group_mapping = await get_group_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.group_name, False @@ -53,9 +55,10 @@ async def test_create_group_on_single_account(self): self.template.included_accounts = [included_account] self.template.excluded_accounts = [] - await self.template.apply( + changes = await self.template.apply( IAMBIC_TEST_DETAILS.config.aws, ) + screen_render_resource_changes([changes]) account_group_mapping = await get_group_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.group_name, False @@ -81,9 +84,10 @@ async def test_create_group_and_attach_to_user(self): self.template.included_accounts = [included_account] self.template.excluded_accounts = [] - await self.template.apply( + changes = await self.template.apply( IAMBIC_TEST_DETAILS.config.aws, ) + screen_render_resource_changes([changes]) user = await get_modifiable_user(iam_client) user_name = user["UserName"] diff --git a/functional_tests/aws/group/test_update_template.py b/functional_tests/aws/group/test_update_template.py index 4bd286ddd..fde0f0849 100644 --- a/functional_tests/aws/group/test_update_template.py +++ b/functional_tests/aws/group/test_update_template.py @@ -7,6 +7,7 @@ from functional_tests.aws.group.utils import generate_group_template_from_base from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.iam.group.utils import get_group_across_accounts from iambic.plugins.v0_1_0.aws.iam.policy.models import ManagedPolicyRef, PolicyDocument @@ -55,7 +56,8 @@ async def test_update_managed_policies(self): self.template.properties.managed_policies = [ ManagedPolicyRef(policy_arn=policy_arn) ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_group_mapping = await get_group_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, @@ -71,7 +73,8 @@ async def test_update_managed_policies(self): ) self.template.properties.managed_policies = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_group_mapping = await get_group_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, @@ -129,6 +132,7 @@ async def test_create_update_group_all_accounts(self): ) ) r = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes(([r])) self.assertEqual(len(r.proposed_changes), 2) # Set expiration @@ -138,6 +142,7 @@ async def test_create_update_group_all_accounts(self): "yesterday", settings={"TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": True} ) r = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes(([r])) self.assertEqual(len(r.proposed_changes), 1) @@ -199,4 +204,5 @@ async def test_bad_input(self): ) ) r = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes(([r])) self.assertEqual(len(r.exceptions_seen), 2) diff --git a/functional_tests/aws/managed_policy/test_create_template.py b/functional_tests/aws/managed_policy/test_create_template.py index ea463902e..4d490add2 100644 --- a/functional_tests/aws/managed_policy/test_create_template.py +++ b/functional_tests/aws/managed_policy/test_create_template.py @@ -8,6 +8,7 @@ from functional_tests.aws.role.utils import get_modifiable_role from functional_tests.conftest import IAMBIC_TEST_DETAILS from iambic.core.utils import aio_wrapper +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.iam.policy.utils import ( get_managed_policy_across_accounts, get_managed_policy_attachments, @@ -33,7 +34,8 @@ async def test_create_managed_policy_all_accounts(self): self.template.included_accounts = ["*"] self.template.excluded_accounts = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_mp_mapping = await get_managed_policy_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.path, self.policy_name @@ -54,7 +56,8 @@ async def test_create_managed_policy_on_single_account(self): self.template.included_accounts = [included_account] self.template.excluded_accounts = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_mp_mapping = await get_managed_policy_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.path, self.policy_name @@ -80,7 +83,8 @@ async def test_create_managed_policy_and_attach_to_role(self): self.template.included_accounts = [included_account] self.template.excluded_accounts = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) role = await get_modifiable_role(iam_client) role_name = role["RoleName"] diff --git a/functional_tests/aws/managed_policy/test_template_generation.py b/functional_tests/aws/managed_policy/test_template_generation.py index c72f1b0bf..bbe397ca2 100644 --- a/functional_tests/aws/managed_policy/test_template_generation.py +++ b/functional_tests/aws/managed_policy/test_template_generation.py @@ -8,6 +8,7 @@ managed_policy_full_import, ) from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.event_bridge.models import ManagedPolicyMessageDetails from iambic.plugins.v0_1_0.aws.iam.policy.models import AwsIamManagedPolicyTemplate @@ -38,7 +39,8 @@ async def test_update_managed_policy_attribute(self): self.template.write() self.template.properties.description = updated_description - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) file_sys_template = AwsIamManagedPolicyTemplate.load(self.template.file_path) self.assertEqual(file_sys_template.properties.description, initial_description) @@ -96,7 +98,8 @@ async def test_delete_managed_policy_from_one_account(self): self.assertNotIn(deleted_account, file_sys_template.excluded_accounts) # Create the policy on all accounts except 1 - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) # Refresh the template await managed_policy_full_import( diff --git a/functional_tests/aws/managed_policy/test_update_template.py b/functional_tests/aws/managed_policy/test_update_template.py index f71e3cd65..ddf055659 100644 --- a/functional_tests/aws/managed_policy/test_update_template.py +++ b/functional_tests/aws/managed_policy/test_update_template.py @@ -8,6 +8,7 @@ ) from functional_tests.conftest import IAMBIC_TEST_DETAILS from iambic.core import noq_json as json +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.models import Tag @@ -40,6 +41,7 @@ async def test_update_tag_with_bad_input(self): template_change_details = await self.template.apply( IAMBIC_TEST_DETAILS.config.aws, ) + screen_render_resource_changes([template_change_details]) self.assertGreater( len(template_change_details.exceptions_seen), diff --git a/functional_tests/aws/permission_set/test_create_template.py b/functional_tests/aws/permission_set/test_create_template.py index 2d6f840e0..446d0582c 100644 --- a/functional_tests/aws/permission_set/test_create_template.py +++ b/functional_tests/aws/permission_set/test_create_template.py @@ -8,6 +8,7 @@ generate_permission_set_template_from_base, ) from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes class CreatePermissionSetTestCase(IsolatedAsyncioTestCase): @@ -23,7 +24,8 @@ async def asyncTearDown(self): await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) async def test_create_permission_set(self): - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) await IAMBIC_TEST_DETAILS.identity_center_account.set_identity_center_details() self.assertIn( @@ -35,7 +37,8 @@ async def test_create_permission_set_with_account_assignment(self): self.template = attach_access_rule( self.template, IAMBIC_TEST_DETAILS.identity_center_account ) - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) await IAMBIC_TEST_DETAILS.identity_center_account.set_identity_center_details() self.assertIn( diff --git a/functional_tests/aws/permission_set/test_update_template.py b/functional_tests/aws/permission_set/test_update_template.py index cde1be73a..86e52ae0a 100644 --- a/functional_tests/aws/permission_set/test_update_template.py +++ b/functional_tests/aws/permission_set/test_update_template.py @@ -9,6 +9,7 @@ ) from functional_tests.conftest import IAMBIC_TEST_DETAILS from iambic.core import noq_json as json +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.identity_center.permission_set.models import ( PermissionSetAccess, ) @@ -52,7 +53,8 @@ def tearDownClass(cls): async def test_update_description(self): self.template.properties.description = "Updated description" - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) await IAMBIC_TEST_DETAILS.identity_center_account.set_identity_center_details( batch_size=5 ) @@ -98,7 +100,8 @@ async def test_account_assignment(self): # test un-assignment self.template.access_rules = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) cloud_access_rules = await get_permission_set_users_and_groups_as_access_rules( identity_center_client, IAMBIC_TEST_DETAILS.identity_center_account.identity_center_details.instance_arn, @@ -114,6 +117,7 @@ async def test_update_invalid_description(self): template_change_details = await self.template.apply( IAMBIC_TEST_DETAILS.config.aws ) + screen_render_resource_changes([template_change_details]) self.assertEqual( len(template_change_details.exceptions_seen), 1, diff --git a/functional_tests/aws/role/test_create_template.py b/functional_tests/aws/role/test_create_template.py index 84da11310..b90c40d43 100644 --- a/functional_tests/aws/role/test_create_template.py +++ b/functional_tests/aws/role/test_create_template.py @@ -4,6 +4,7 @@ from functional_tests.aws.role.utils import generate_role_template_from_base from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.iam.role.utils import get_role_across_accounts @@ -25,7 +26,8 @@ async def test_create_role_all_accounts(self): self.template.included_accounts = ["*"] self.template.excluded_accounts = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.role_name, False @@ -46,7 +48,8 @@ async def test_create_role_on_single_account(self): self.template.included_accounts = [included_account] self.template.excluded_accounts = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.role_name, False diff --git a/functional_tests/aws/role/test_update_template.py b/functional_tests/aws/role/test_update_template.py index 591aafdaf..6149e0237 100644 --- a/functional_tests/aws/role/test_update_template.py +++ b/functional_tests/aws/role/test_update_template.py @@ -7,6 +7,7 @@ from functional_tests.aws.role.utils import generate_role_template_from_base from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.aws.iam.policy.models import ManagedPolicyRef, PolicyDocument from iambic.plugins.v0_1_0.aws.iam.role.models import PermissionBoundary from iambic.plugins.v0_1_0.aws.iam.role.utils import get_role_across_accounts @@ -38,7 +39,8 @@ def tearDownClass(cls): # empty tag string value is a valid input async def test_update_tag_with_empty_string(self): self.template.properties.tags = [Tag(key="test", value="")] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.role_name, False @@ -68,6 +70,7 @@ async def test_update_tag_with_bad_input(self): template_change_details = await self.template.apply( IAMBIC_TEST_DETAILS.config.aws ) + screen_render_resource_changes([template_change_details]) except Exception as e: # because it should still crash # FIXME check assert here @@ -100,7 +103,8 @@ async def test_update_tag_with_bad_input(self): async def test_update_description(self): self.template.properties.description = "Updated description" - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.role_name, False @@ -120,7 +124,8 @@ async def test_update_permission_boundary(self): self.template.properties.permissions_boundary = PermissionBoundary( policy_arn=view_policy_arn ) - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.role_name, False @@ -138,7 +143,8 @@ async def test_update_permission_boundary(self): async def test_update_managed_policies(self): if self.template.properties.managed_policies: self.template.properties.managed_policies = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, @@ -173,7 +179,8 @@ async def test_update_managed_policies(self): ) self.template.properties.managed_policies = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, @@ -192,7 +199,8 @@ async def test_create_update_role_all_accounts(self): self.template.included_accounts = ["*"] self.template.excluded_accounts = [] - await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([changes]) account_role_mapping = await get_role_across_accounts( IAMBIC_TEST_DETAILS.config.aws.accounts, self.role_name, False @@ -231,6 +239,7 @@ async def test_create_update_role_all_accounts(self): ) ) r = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([r]) self.assertEqual(len(r.proposed_changes), 2) # Set expiration @@ -240,4 +249,5 @@ async def test_create_update_role_all_accounts(self): "yesterday", settings={"TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": True} ) r = await self.template.apply(IAMBIC_TEST_DETAILS.config.aws) + screen_render_resource_changes([r]) self.assertEqual(len(r.proposed_changes), 1) diff --git a/functional_tests/azure_ad/group/test_create_template.py b/functional_tests/azure_ad/group/test_create_template.py index c40e7018e..108976a39 100644 --- a/functional_tests/azure_ad/group/test_create_template.py +++ b/functional_tests/azure_ad/group/test_create_template.py @@ -3,6 +3,7 @@ from functional_tests.azure_ad.base_test_case import BaseMS365TestCase from functional_tests.azure_ad.group.utils import generate_group_template from functional_tests.conftest import IAMBIC_TEST_DETAILS +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.azure_ad.group.models import Member, MemberDataType from iambic.plugins.v0_1_0.azure_ad.group.utils import get_group @@ -26,7 +27,8 @@ async def test_create_ms_365_group_with_user_member(self): data_type=MemberDataType.USER, ) ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + template_change = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([template_change]) try: group = await get_group(self.org, group_name=self.group_name) @@ -42,6 +44,7 @@ async def test_create_security_group(self): self.template.properties.security_enabled = True changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) self.assertEqual(len(changes.exceptions_seen), 0, changes.exceptions_seen) try: @@ -67,7 +70,8 @@ async def test_create_security_group_with_group_member(self): self.template.properties.group_types = [] self.template.properties.mail_enabled = False self.template.properties.security_enabled = True - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) try: group = await get_group(self.org, group_name=self.group_name) @@ -82,6 +86,7 @@ async def test_attempt_create_mail_enabled_security_group(self): self.template.properties.security_enabled = True changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) self.assertGreaterEqual(len(changes.exceptions_seen), 1) # Should not exist diff --git a/functional_tests/azure_ad/group/test_update_template.py b/functional_tests/azure_ad/group/test_update_template.py index 3353059a0..90858f6f3 100644 --- a/functional_tests/azure_ad/group/test_update_template.py +++ b/functional_tests/azure_ad/group/test_update_template.py @@ -10,6 +10,7 @@ from iambic.core.iambic_enum import Command from iambic.core.models import ExecutionMessage from iambic.core.parser import load_templates +from iambic.output.text import screen_render_resource_changes from iambic.plugins.v0_1_0.azure_ad.group.models import Member, MemberDataType from iambic.plugins.v0_1_0.azure_ad.group.utils import get_group from iambic.request_handler.expire_resources import flag_expired_resources @@ -31,7 +32,8 @@ def tearDownClass(cls): async def test_update_description(self): self.template.properties.description = "Updated description" - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) try: group = await get_group(self.org, group_name=self.group_name) @@ -54,7 +56,8 @@ async def test_add_user_member(self): data_type=MemberDataType.USER, ) ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) try: group = await get_group(self.org, group_name=self.group_name) @@ -74,7 +77,8 @@ async def test_remove_user_member(self): data_type=MemberDataType.USER, ) ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) self.template.properties.members = [ member @@ -109,7 +113,8 @@ async def test_expire_member(self): data_type=MemberDataType.USER, ), ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) expired_member = self.template.properties.members[0].name cur_time = datetime.datetime.now(datetime.timezone.utc) @@ -156,7 +161,8 @@ def tearDownClass(cls): async def test_update_description(self): self.template.properties.description = "Updated description" - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) try: group = await get_group(self.org, group_name=self.group_name) @@ -180,7 +186,8 @@ async def test_add_group_member(self): data_type=MemberDataType.GROUP, ) ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) try: group = await get_group(self.org, group_name=self.group_name) @@ -207,7 +214,8 @@ async def test_add_user_and_group_members(self): data_type=MemberDataType.GROUP, ), ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) try: group = await get_group(self.org, group_name=self.group_name) @@ -235,7 +243,8 @@ async def test_remove_user_and_group_members(self): data_type=MemberDataType.GROUP, ), ] - await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + changes = await self.template.apply(IAMBIC_TEST_DETAILS.config.azure_ad) + screen_render_resource_changes([changes]) self.template.properties.members = [ member diff --git a/functional_tests/okta/group/test_okta_group.py b/functional_tests/okta/group/test_okta_group.py index e03ef9bc0..1a3fc654b 100644 --- a/functional_tests/okta/group/test_okta_group.py +++ b/functional_tests/okta/group/test_okta_group.py @@ -53,9 +53,9 @@ def test_okta_group(): assert len(group_template.properties.members) == 2 # set the template to import_only - proposed_changes_yaml_path = "{0}/proposed_changes.yaml".format(os.getcwd()) - if os.path.isfile(proposed_changes_yaml_path): - os.remove(proposed_changes_yaml_path) + proposed_changes_path = "{0}/proposed_changes.json".format(os.getcwd()) + if os.path.isfile(proposed_changes_path): + os.remove(proposed_changes_path) else: assert ( False # Previous changes are not being written out to proposed_changes.yaml @@ -67,8 +67,8 @@ def test_okta_group(): ].username = "this_user_should_not_exist@example.com" group_template.write() run_apply(IAMBIC_TEST_DETAILS.config, [test_group_fp]) - if os.path.isfile(proposed_changes_yaml_path): - assert os.path.getsize(proposed_changes_yaml_path) == 0 + if os.path.isfile(proposed_changes_path): + assert os.path.getsize(proposed_changes_path) == 2 # empty json file else: # this is acceptable as well because there are no changes to be made. pass diff --git a/functional_tests/okta/user/test_okta_user.py b/functional_tests/okta/user/test_okta_user.py index 8e0333c1a..d8e54abd3 100644 --- a/functional_tests/okta/user/test_okta_user.py +++ b/functional_tests/okta/user/test_okta_user.py @@ -56,9 +56,9 @@ def test_okta_user(): assert user_template.properties.profile["firstName"] == "TestNameChange" # set the template to import_only - proposed_changes_yaml_path = "{0}/proposed_changes.yaml".format(os.getcwd()) - if os.path.isfile(proposed_changes_yaml_path): - os.remove(proposed_changes_yaml_path) + proposed_changes_path = "{0}/proposed_changes.json".format(os.getcwd()) + if os.path.isfile(proposed_changes_path): + os.remove(proposed_changes_path) else: assert ( False # Previous changes are not being written out to proposed_changes.yaml @@ -68,8 +68,8 @@ def test_okta_user(): user_template.properties.profile["firstName"] = "shouldNotWork" user_template.write() run_apply(IAMBIC_TEST_DETAILS.config, [test_user_fp]) - if os.path.isfile(proposed_changes_yaml_path): - assert os.path.getsize(proposed_changes_yaml_path) == 0 + if os.path.isfile(proposed_changes_path): + assert os.path.getsize(proposed_changes_path) == 2 # {} is 2 bytes else: # this is acceptable as well because there are no changes to be made. pass diff --git a/iambic/core/models.py b/iambic/core/models.py index 577ad968f..0844f11f9 100644 --- a/iambic/core/models.py +++ b/iambic/core/models.py @@ -317,8 +317,8 @@ class ProposedChange(PydanticBaseModel): attribute: Optional[str] resource_id: Optional[str] resource_type: Optional[str] - current_value: Optional[Union[list, dict, str, int]] - new_value: Optional[Union[list, dict, str, int]] + current_value: Optional[Union[list, dict, str, int, None]] + new_value: Optional[Union[list, dict, str, int, None]] change_summary: Optional[dict] exceptions_seen: list[str] = Field( default=[] diff --git a/iambic/main.py b/iambic/main.py index 95dee12dd..c1813dad8 100644 --- a/iambic/main.py +++ b/iambic/main.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import os import pathlib import sys @@ -22,12 +23,8 @@ from iambic.core.logger import log from iambic.core.models import ExecutionMessage, TemplateChangeDetails from iambic.core.parser import load_templates -from iambic.core.utils import ( - exceptions_in_proposed_changes, - gather_templates, - init_writable_directory, - yaml, -) +from iambic.core.utils import exceptions_in_proposed_changes, gather_templates, init_writable_directory +from iambic.output.text import file_render_resource_changes, screen_render_resource_changes from iambic.request_handler.expire_resources import flag_expired_resources from iambic.request_handler.git_apply import apply_git_changes from iambic.request_handler.git_plan import plan_git_changes @@ -39,18 +36,20 @@ def output_proposed_changes( template_changes: list[TemplateChangeDetails], - output_path: str = "proposed_changes.yaml", + output_path: str = "proposed_changes.txt", exit_on_error: bool = True, ): + if output_path is None: + output_path = "proposed_changes.txt" if template_changes: log.info(f"A detailed summary of changes has been saved to {output_path}") + file_render_resource_changes(output_path, template_changes) - with open(output_path, "w") as f: - f.write( - yaml.dump( - [template_change.dict() for template_change in template_changes], - ) - ) + json_filepath = pathlib.Path(output_path).with_suffix(".json") + with open(str(json_filepath), "w") as fp: + json.dump( + [template_change.dict() for template_change in template_changes], fp + ) if exceptions_in_proposed_changes([change.dict() for change in template_changes]): log.error( @@ -255,6 +254,8 @@ def run_apply( template_changes = asyncio.run(config.run_apply(exe_message, templates)) output_proposed_changes(template_changes, output_path=output_path) + screen_render_resource_changes(template_changes) + if ctx.eval_only and template_changes and click.confirm("Proceed?"): ctx.eval_only = False template_changes = asyncio.run(config.run_apply(exe_message, templates)) @@ -283,6 +284,7 @@ def run_git_apply( ) ) output_proposed_changes(template_changes, output_path, exit_on_error=False) + screen_render_resource_changes(template_changes) return template_changes @@ -336,6 +338,7 @@ def run_git_plan( check_and_update_resource_limit(config) template_changes = asyncio.run(plan_git_changes(config_path, repo_dir)) output_proposed_changes(template_changes, output_path, exit_on_error=False) + screen_render_resource_changes(template_changes) return template_changes @@ -350,9 +353,9 @@ def run_plan(templates: list[str], repo_dir: str = str(pathlib.Path.cwd())): ) asyncio.run(flag_expired_resources(templates)) ctx.eval_only = True - output_proposed_changes( - asyncio.run(config.run_apply(exe_message, load_templates(templates))) - ) + template_changes = asyncio.run(config.run_apply(exe_message, load_templates(templates))) + output_proposed_changes(template_changes) + screen_render_resource_changes(template_changes) @cli.command() diff --git a/iambic/output/__init__.py b/iambic/output/__init__.py index e69de29bb..b56f0f611 100644 --- a/iambic/output/__init__.py +++ b/iambic/output/__init__.py @@ -0,0 +1,20 @@ +import pathlib +from jinja2 import Environment, FileSystemLoader +from iambic.output.filters import ( + rich_format, + rich_text, + rich_text_table, + rich_tree_exception, + rich_tree_summary, +) + + +def get_template_env(): + my_path = pathlib.Path(__file__).parent.absolute() + env = Environment(loader=FileSystemLoader(my_path / "templates")) + env.filters["rich_format"] = rich_format + env.filters["rich_text"] = rich_text + env.filters["rich_text_table"] = rich_text_table + env.filters["rich_tree_summary"] = rich_tree_summary + env.filters["rich_tree_exception"] = rich_tree_exception + return env diff --git a/iambic/output/filters.py b/iambic/output/filters.py new file mode 100644 index 000000000..bbbd89cab --- /dev/null +++ b/iambic/output/filters.py @@ -0,0 +1,63 @@ +from io import StringIO +from typing import List +from rich.console import Console +from rich.style import Style +from rich.text import Text +from rich.table import Table +from rich.tree import Tree +from iambic.output.models import ActionSummary, ExceptionSummary + + +console = Console() +def rich_format(value: str, style_str: str) -> str: + style = Style.parse(style_str) + return console.render_str(str(value), style=style) + + +def rich_text(text: str, style_str: str) -> str: + style = Style.parse(style_str) + return Text(str(text), style=style) + + +def rich_text_table(table_headers: List[str], table_data: List[List[str]]) -> str: + table = Table(show_header=True, header_style="bold magenta") + for header in table_headers: + table.add_column(header, justify="left", style="dim", no_wrap=True) + for row in table_data: + table.add_row(*row) + console.render_lines(table) + + +def rich_tree_summary(action_summary: ActionSummary): + action_summary_tree = Tree(action_summary.action, expanded=True) + for template in action_summary.templates: + template_tree = action_summary_tree.add(template.template_path, expanded=True) + for account in template.accounts: + account_tree = template_tree.add(account.account, expanded=True) + for change in account.changes: + change_tree = account_tree.add(f"{change.change.resource_type} // {change.change.resource_id}", expanded=True) + if change.change.diff: + change_tree.add(change.change.diff_plus_minus) + console = Console(file=StringIO(), force_terminal=True) + console.print(action_summary_tree) + output = console.file.getvalue() + return output + + +def rich_tree_exception(exceptions: ExceptionSummary): + exception_tree = Tree(exceptions.action, expanded=True) + for template in exceptions.templates: + template_tree = exception_tree.add(template.template_path, expanded=True) + for account in template.accounts: + account_tree = template_tree.add(account.account, expanded=True) + for change in account.changes: + change_tree = account_tree.add(str(change.change.change_type), expanded=True) + change_tree.add(change.change.resource_id) + change_tree.add(change.change.resource_type) + change_tree.add(str(change.change.change_type.value)) + if change.change.diff: + change_tree.add("* " + "\n* ".join(change.change.diff_plus_minus)) + console = Console(file=StringIO(), force_terminal=True) + console.print(exception_tree) + output = console.file.getvalue() + return output diff --git a/iambic/output/markdown.py b/iambic/output/markdown.py index 4efd1c243..3a4208a64 100644 --- a/iambic/output/markdown.py +++ b/iambic/output/markdown.py @@ -1,309 +1,16 @@ from __future__ import annotations -import pathlib -from typing import Any, Dict, List, Set +from typing import List -from jinja2 import Environment, FileSystemLoader -from pydantic import BaseModel as PydanticBaseModel -from pydantic import Field -from recursive_diff import recursive_diff - -from iambic.core.logger import log from iambic.core.models import ( - AccountChangeDetails, - ProposedChange, - ProposedChangeType, TemplateChangeDetails, ) - - -class ProposedChangeDiff(ProposedChange): - diff: str = Field(default=None) - - def __init__(self, proposed_change: ProposedChange) -> None: - super().__init__(**proposed_change.dict()) - self.diff = "\n".join(list(recursive_diff(self.current_value, self.new_value))) - - -class ApplicableChange(PydanticBaseModel): - account: str = Field(default=None) - change: ProposedChange = Field(default=None) - template_change: TemplateChangeDetails = Field(default=None) - template_name: str = Field(default=None) - resource_id: str = Field(default=None) - resource_type: str = Field(default=None) - - def __hash__(self): - return hash((self.resource_id, self.resource_type)) - - def __init__( - self, - change: ProposedChange, - template_change: TemplateChangeDetails, - **data: Any, - ) -> None: - super().__init__( - change=ProposedChangeDiff(change), template_change=template_change, **data - ) - self.template_name = pathlib.Path(template_change.template_path).name - - -class AccountSummary(PydanticBaseModel): - account: str = Field(default="NONE") - count: int = Field(default=0) - num_changes: int = Field(default=0) - changes: List[ProposedChange] = Field(default=[]) - - @classmethod - def compile( - cls, account: str, count: int, changes: List[ProposedChange], **data: Any - ) -> None: - instance = cls() - instance.account = account - instance.count = count - instance.num_changes = len(changes) - instance.changes = changes - return instance - - -class TemplateSummary(PydanticBaseModel): - template_path: str = Field(default="") - template_name: str = Field(default="") - count: int = Field(default=0) - num_accounts: int = Field(default=0) - accounts: List[AccountSummary] = Field(default=[]) - - def __hash__(self): - return hash(self.template_path) - - @classmethod - def compile( - cls, - template_path: str, - template_name: str, - count: int, - changes: List[ProposedChange], - **data: Any, - ) -> None: - instance = cls() - instance.template_path = template_path - instance.template_name = template_name - instance.count = count - instance.num_accounts = len(set([x.account for x in changes])) - instance.accounts = [ - AccountSummary.compile( - account=change.account, - count=len(changes), - changes=[x for x in changes if x.account == change.account], - ) - for change in changes - ] - return instance - - -def get_applicable_changes( - template_changes: List[TemplateChangeDetails], - proposed_change_type: str, - attribute: str = "proposed_changes", -) -> Any: - """Compile applicable changes as a list of ApplicableChange objects. - - :param template_changes: list of TemplateChangeDetails objects - :param proposed_change_type: one of ProposedChangeType values - :param attribute: str. is either "proposed_changes" or "exceptions_seen" - :return: set of ApplicableChange - """ - - def _get_annotated_change( - change: ProposedChange, - template_change: TemplateChangeDetails, - account: str = "NONE", - ) -> ApplicableChange: - return ApplicableChange( - account=account, - change=change, - template_change=template_change, - resource_id=change.resource_id, - resource_type=change.resource_type, - ) - - applicable_changes: Set[ApplicableChange] = set() - for template_change in template_changes: - for proposed_change in getattr(template_change, attribute, []): - if isinstance(proposed_change, AccountChangeDetails): - # If proposed change is a list of AccountChangeDetails, we need to iterate through those - for account_change in proposed_change.proposed_changes: - if account_change.change_type.value == proposed_change_type: - applicable_changes.add( - _get_annotated_change( - account_change, template_change, proposed_change.account - ) - ) - else: - if proposed_change.change_type.value == proposed_change_type: - # If proposed change is a single change, we can just append it - applicable_changes.add( - _get_annotated_change(proposed_change, template_change) - ) - - return set(applicable_changes) # collapse across accounts and no accounts - - -class ActionSummary(PydanticBaseModel): - action: str = Field(default="") - count: int = Field(default=0) - num_templates = Field(default=0) - templates: List[TemplateSummary] = Field(default=[]) - - @classmethod - def compile_proposed_changes( - cls, template_changes: List[TemplateChangeDetails], proposed_change_type: str - ) -> Any: - """Compile a list of TemplateChangeDetails into a list of TemplateSummary objects. - - :param resources_changes: list of TemplateChangeDetails objects - :returns: None - """ - applicable_changes = get_applicable_changes( - template_changes, proposed_change_type, attribute="proposed_changes" - ) - log.debug(f"Found {len(applicable_changes)} applicable changes") - - instance = cls( - action=proposed_change_type, count=len(applicable_changes), templates=[] - ) - templates = set( - [ - TemplateSummary.compile( - template_path=x.template_change.template_path, - template_name=x.template_name, - count=1, - changes=[ - y - for y in applicable_changes - if y.template_change.template_path - == x.template_change.template_path - ], - ) - for x in applicable_changes - ] - ) - instance.templates = templates - instance.num_templates = len(templates) - - return instance - - -class ExceptionSummary(PydanticBaseModel): - action: str = Field(default="") - count: int = Field(default=0) - num_templates = Field(default=0) - templates: List[TemplateSummary] = Field(default=[]) - - @classmethod - def compile_exceptions_seen( - cls, template_changes: List[TemplateChangeDetails], proposed_change_type: str - ) -> Any: - exceptions = get_applicable_changes( - template_changes, proposed_change_type, attribute="exceptions_seen" - ) - log.debug(f"Found {len(exceptions)} exceptions") - - instance = cls( - action=proposed_change_type, - count=len(exceptions), - num_templates=0, - templates=[], - ) - templates = set( - [ - TemplateSummary.compile( - template_path=x.template_change.template_path, - template_name=x.template_name, - count=1, - changes=[ - y - for y in exceptions - if y.template_change.template_path - == x.template_change.template_path - ], - ) - for x in exceptions - ] - ) - instance.templates = templates - instance.num_templates = len(templates) - - return instance - - -class ActionSummaries(PydanticBaseModel): - num_actions: int = Field(default=0) - num_templates: int = Field(default=0) - num_accounts: int = Field(default=0) - num_exceptions: int = Field(default=0) - action_summaries: List[ActionSummary] = Field(default=[]) - exceptions: List[ExceptionSummary] = Field(default=[]) - - @classmethod - def compile(cls, changes: List[TemplateChangeDetails]): - instance = cls() - instance.action_summaries = [ - ActionSummary.compile_proposed_changes(changes, x) - for x in list([e.value for e in ProposedChangeType]) - ] - instance.num_actions = sum( - [1 for x in instance.action_summaries if x.count > 0] - ) - instance.num_templates = sum( - [len(x.templates) for x in instance.action_summaries] - ) - accounts = set( - [ - g.account - for y in instance.action_summaries - for z in y.templates - for g in z.accounts - ] - ) - instance.num_accounts = len(accounts) - instance.exceptions = [ - ExceptionSummary.compile_exceptions_seen(changes, x) - for x in list([e.value for e in ProposedChangeType]) - ] - instance.num_exceptions = sum([1 for x in instance.exceptions if x.count > 0]) - return instance - - -def get_template_data(resources_changes: List[TemplateChangeDetails]) -> Dict[str, Any]: - """Convert TemplateChangeDetails into a format that is oriented in this format. - - * Action (Add, Delete, Modify) - * Template Name (Count of ) - * Account Name (Count of ) - * Changes for Account (Count of ) - * Table of proposed changes - - For each, the corresponding jinja2 templates are: - - * Action: templates/actions.jinja2 - * Template Name: templates/template.jinja2 - * Account Name: templates/accounts.jinja2 - * Changes for Account: templates/resource_change.jinja2 - - There is also a jinja template to display all exceptions: - - * Exceptions: templates/exception_details.jinja2 - - :param resources_changes: list of TemplateChangeDetails objects - :returns: Dict[str, Any] - """ - return ActionSummaries.compile(resources_changes) +from iambic.output import get_template_env +from iambic.output.models import get_template_data def gh_render_resource_changes(resource_changes: List[TemplateChangeDetails]): template_data = get_template_data(resource_changes) - my_path = pathlib.Path(__file__).parent.absolute() - env = Environment(loader=FileSystemLoader(my_path / "templates")) + env = get_template_env() template = env.get_template("github_summary.jinja2") return template.render(iambic=template_data) diff --git a/iambic/output/models.py b/iambic/output/models.py new file mode 100644 index 000000000..2ce282b89 --- /dev/null +++ b/iambic/output/models.py @@ -0,0 +1,362 @@ +from __future__ import annotations +import json + +import pathlib +from typing import Any, Dict, List, Optional, Set + +from dictdiffer import diff +from pydantic import BaseModel as PydanticBaseModel +from pydantic import Field + +from iambic.core.logger import log +from iambic.core.models import ( + AccountChangeDetails, + ProposedChange, + ProposedChangeType, + TemplateChangeDetails, +) +from iambic.core.utils import camel_to_snake + + +class ProposedChangeDiff(ProposedChange): + diff: Optional[str] + diff_resolved: Optional[str] + + field_map = { + "inline_policies": "InlinePolicies", + "managed_policies": "ManagedPolicies", + "policy_document": "PolicyDocument", + "tags": "Tags", + "assume_role_policy_document": "AssumeRolePolicyDocument", + "permission_boundary": "PermissionBoundary", + "customer_managed_policies": "CustomerManagedPolicies", + "group_name": "GroupName", + "group_email": "GroupEmail", + "assignments": "Assignment", + "domain": "Domain", + "group": "Group", + "groups": "Group", + "users": "User", + "user": "User", + + } + + def __init__(self, proposed_change: ProposedChange) -> None: + super().__init__(**proposed_change.dict()) + try: + object_attribute = camel_to_snake(self.attribute) + except TypeError: + object_attribute = None + if self.current_value is None: + self.current_value = {} + if self.new_value is None: + self.new_value = {} + if isinstance(self.current_value, dict): + self.diff = list(diff(self.current_value.get(object_attribute, {}), self.new_value)) + else: + self.diff = list(diff(self.current_value, self.new_value)) + + @property + def diff_plus_minus(self) -> List[str]: + diff_plus_minus = "" + for x in self.diff: + label = self.attribute + if x[0] == "change": + if isinstance(x[2], list) or isinstance(x[2], tuple): + change_from = x[2][0] + change_to = x[2][1] + else: + change_from = x[2] + change_to = x[2] + if change_to: + diff_plus_minus += f"{label}:\n-(From)\n{json.dumps(change_from, indent=2)}\n+(To)\n{json.dumps(change_to, indent=2)}" + else: + diff_plus_minus += f"{label}:\n-(Remove)\n{json.dumps(change_from, indent=2)}" + diff_plus_minus.rstrip('\n') + elif x[0] == "add": + diff_plus_minus += f"{label}:\n+(Add)\n{json.dumps([y[1] for y in x[2]], indent=2)}" + diff_plus_minus.rstrip('\n') + elif x[0] == "remove": + diff_plus_minus += f"{label}:\n-(Remove)\n{json.dumps([y[1] for y in x[2]], indent=2)}" + diff_plus_minus.rstrip('\n') + return diff_plus_minus + + +class ApplicableChange(PydanticBaseModel): + account: Optional[str] + change: Optional[ProposedChange] + template_change: Optional[TemplateChangeDetails] + template_name: Optional[str] + resource_id: Optional[str] + resource_type: Optional[str] + + def __hash__(self): + return hash((self.resource_id, self.resource_type)) + + def __init__( + self, + change: ProposedChange, + template_change: TemplateChangeDetails, + **data: Any, + ) -> None: + super().__init__( + change=ProposedChangeDiff(change), template_change=template_change, **data + ) + self.template_name = pathlib.Path(template_change.template_path).name + + +class AccountSummary(PydanticBaseModel): + account: Optional[str] = Field(default="NONE") + count: Optional[int] + num_changes: Optional[int] + changes: Optional[List[ProposedChange]] + + def __hash__(self): + return hash((self.account, self.num_changes)) + + @classmethod + def compile( + cls, account: str, count: int, changes: List[ProposedChange], **data: Any + ) -> None: + instance = cls() + instance.account = account + instance.count = count + instance.num_changes = len(changes) + instance.changes = changes + return instance + + +class TemplateSummary(PydanticBaseModel): + template_path: Optional[str] + template_name: Optional[str] + count: Optional[int] + num_accounts: Optional[int] + accounts: Optional[List[AccountSummary]] + + def __hash__(self): + return hash(self.template_path) + + @classmethod + def compile( + cls, + template_path: str, + template_name: str, + count: int, + changes: List[ProposedChange], + **data: Any, + ) -> None: + instance = cls() + instance.template_path = template_path + instance.template_name = template_name + instance.count = count + instance.num_accounts = len(set([x.account for x in changes])) + accounts = set(x.account for x in changes) + instance.accounts = [ + AccountSummary.compile( + account=account, + count=len(changes), + changes=[x for x in changes if x.account == account], + ) + for account in accounts + ] + return instance + + +def get_applicable_changes( + template_changes: List[TemplateChangeDetails], + proposed_change_type: str, + attribute: str = "proposed_changes", +) -> Any: + """Compile applicable changes as a list of ApplicableChange objects. + + :param template_changes: list of TemplateChangeDetails objects + :param proposed_change_type: one of ProposedChangeType values + :param attribute: str. is either "proposed_changes" or "exceptions_seen" + :return: set of ApplicableChange + """ + + def _get_annotated_change( + change: ProposedChange, + template_change: TemplateChangeDetails, + account: str = "NONE", + ) -> ApplicableChange: + return ApplicableChange( + account=account, + change=change, + template_change=template_change, + resource_id=change.resource_id, + resource_type=change.resource_type, + ) + + applicable_changes: Set[ApplicableChange] = set() + for template_change in template_changes: + for proposed_change in getattr(template_change, attribute, []): + if isinstance(proposed_change, AccountChangeDetails): + # If proposed change is a list of AccountChangeDetails, we need to iterate through those + for account_change in proposed_change.proposed_changes: + if account_change.change_type.value == proposed_change_type: + applicable_changes.add( + _get_annotated_change( + account_change, template_change, proposed_change.account + ) + ) + else: + if proposed_change.change_type.value == proposed_change_type: + # If proposed change is a single change, we can just append it + applicable_changes.add( + _get_annotated_change(proposed_change, template_change) + ) + + return set(applicable_changes) # collapse across accounts and no accounts + + +class ActionSummary(PydanticBaseModel): + action: Optional[str] + count: Optional[int] + num_templates: Optional[int] + templates: Optional[List[TemplateSummary]] + + @classmethod + def compile_proposed_changes( + cls, template_changes: List[TemplateChangeDetails], proposed_change_type: str + ) -> Any: + """Compile a list of TemplateChangeDetails into a list of TemplateSummary objects. + + :param resources_changes: list of TemplateChangeDetails objects + :returns: None + """ + applicable_changes = get_applicable_changes( + template_changes, proposed_change_type, attribute="proposed_changes" + ) + log.debug(f"Found {len(applicable_changes)} applicable changes") + + instance = cls( + action=proposed_change_type, count=len(applicable_changes), templates=[] + ) + templates = set( + [ + TemplateSummary.compile( + template_path=x.template_change.template_path, + template_name=x.template_name, + count=1, + changes=[ + y + for y in applicable_changes + if y.template_change.template_path + == x.template_change.template_path + ], + ) + for x in applicable_changes + ] + ) + instance.templates = templates + instance.num_templates = len(templates) + + return instance + + +class ExceptionSummary(PydanticBaseModel): + action: Optional[str] + count: Optional[int] + num_templates: Optional[int] + templates: Optional[List[TemplateSummary]] + + @classmethod + def compile_exceptions_seen( + cls, template_changes: List[TemplateChangeDetails], proposed_change_type: str + ) -> Any: + exceptions = get_applicable_changes( + template_changes, proposed_change_type, attribute="exceptions_seen" + ) + log.debug(f"Found {len(exceptions)} exceptions") + + instance = cls( + action=proposed_change_type, + count=len(exceptions), + num_templates=0, + templates=[], + ) + templates = set( + [ + TemplateSummary.compile( + template_path=x.template_change.template_path, + template_name=x.template_name, + count=1, + changes=[ + y + for y in exceptions + if y.template_change.template_path + == x.template_change.template_path + ], + ) + for x in exceptions + ] + ) + instance.templates = templates + instance.num_templates = len(templates) + + return instance + + +class ActionSummaries(PydanticBaseModel): + num_actions: Optional[int] + num_templates: Optional[int] + num_accounts: Optional[int] + num_exceptions: Optional[int] + action_summaries: Optional[List[ActionSummary]] + exceptions: Optional[List[ExceptionSummary]] + + @classmethod + def compile(cls, changes: List[TemplateChangeDetails]): + instance = cls() + instance.action_summaries = [ + ActionSummary.compile_proposed_changes(changes, x) + for x in list([e.value for e in ProposedChangeType]) + ] + instance.num_actions = sum( + [1 for x in instance.action_summaries if x.count > 0] + ) + instance.num_templates = sum( + [len(x.templates) for x in instance.action_summaries] + ) + accounts = set( + [ + g.account + for y in instance.action_summaries + for z in y.templates + for g in z.accounts + ] + ) + instance.num_accounts = len(accounts) + instance.exceptions = [ + ExceptionSummary.compile_exceptions_seen(changes, x) + for x in list([e.value for e in ProposedChangeType]) + ] + instance.num_exceptions = sum([1 for x in instance.exceptions if x.count > 0]) + return instance + + +def get_template_data(resources_changes: List[TemplateChangeDetails]) -> Dict[str, Any]: + """Convert TemplateChangeDetails into a format that is oriented in this format. + + * Action (Add, Delete, Modify) + * Template Name (Count of ) + * Account Name (Count of ) + * Changes for Account (Count of ) + * Table of proposed changes + + For each, the corresponding jinja2 templates are: + + * Action: templates/actions.jinja2 + * Template Name: templates/template.jinja2 + * Account Name: templates/accounts.jinja2 + * Changes for Account: templates/resource_change.jinja2 + + There is also a jinja template to display all exceptions: + + * Exceptions: templates/exception_details.jinja2 + + :param resources_changes: list of TemplateChangeDetails objects + :returns: Dict[str, Any] + """ + return ActionSummaries.compile(resources_changes) diff --git a/iambic/output/templates/github_summary.jinja2 b/iambic/output/templates/github_summary.jinja2 index dcaf8c299..4cbb9a1d6 100644 --- a/iambic/output/templates/github_summary.jinja2 +++ b/iambic/output/templates/github_summary.jinja2 @@ -40,9 +40,9 @@ {{ change.resource_type }} {{ change.change.change_type.value }} - {% if change.diff -%} + {% if change.change.diff_resolved -%} - {{ change.diff }} + {{ "* " + "
* ".join(change.change.diff_resolved) }} {% endif -%} @@ -97,9 +97,9 @@ {{ change.resource_type }} {{ change.change.change_type.value }} - {% if change.diff -%} + {% if change.change.diff_plus_minus -%} - {{ change.diff }} + {{ change.change.diff_plus_minus }} {% endif -%} diff --git a/iambic/output/templates/text_file_summary.jinja2 b/iambic/output/templates/text_file_summary.jinja2 new file mode 100644 index 000000000..2a8018bcd --- /dev/null +++ b/iambic/output/templates/text_file_summary.jinja2 @@ -0,0 +1,26 @@ +{{ "IAMbic Summary" | rich_format("bold blue") }} + +{{ "Change Detection" | rich_format("italic blue") }} + +* {{ iambic.num_actions | rich_format("blue") }} {{ "distinct actions." | rich_format("blue") }} +* {{ iambic.num_templates | rich_format("blue") }} {{ "templates with changes." | rich_format("blue") }} +* {{ iambic.num_accounts | rich_format("blue") }} {{ "accounts affected." | rich_format("blue") }} + +{{ "Exceptions" | rich_format("italic blue") }} +* {{ iambic.num_exceptions | rich_format("blue") }} {{ "exceptions were recorded." | rich_format("blue") }} +{% if iambic.num_templates > 0 -%} +{{ "IAMbic Change Details" | rich_format("bold blue") }} +{% for action_summary in iambic.action_summaries -%} +{% if action_summary.num_templates > 0 -%} +{{ action_summary | rich_tree_summary() -}} +{% endif -%} +{% endfor -%} +{% endif -%} +{% if iambic.num_exceptions > 0 -%} +{{ "IAMbic Exceptions" | rich_format("bold blue") }} +{% for exception in iambic.exceptions -%} +{% if exception.num_templates > 0 -%} +{{ exception | rich_tree_exception() -}} +{% endif -%} +{% endfor -%} +{% endif -%} diff --git a/iambic/output/templates/text_screen_summary.jinja2 b/iambic/output/templates/text_screen_summary.jinja2 new file mode 100644 index 000000000..2f35767f5 --- /dev/null +++ b/iambic/output/templates/text_screen_summary.jinja2 @@ -0,0 +1,26 @@ +{{ "IAMbic Summary" | rich_format("bold blue") }} + +{{ "Change Detection" | rich_format("italic blue") }} + +* {{ iambic.num_actions | rich_format("blue") }} {{ "distinct actions." | rich_format("blue") }} +* {{ iambic.num_templates | rich_format("blue") }} {{ "templates with changes." | rich_format("blue") }} +* {{ iambic.num_accounts | rich_format("blue") }} {{ "accounts affected." | rich_format("blue") }} + +{{ "Exceptions" | rich_format("italic blue") }} +* {{ iambic.num_exceptions | rich_format("blue") }} {{ "exceptions were recorded." | rich_format("blue") }} +{% if iambic.num_templates > 0 -%} +{{ "IAMbic Change Details" | rich_format("bold blue") }} +{% for action_summary in iambic.action_summaries -%} +{% if action_summary.num_templates > 0 -%} +{{ action_summary | rich_tree_summary }} +{% endif -%} +{% endfor -%} +{% endif -%} +{% if iambic.num_exceptions > 0 -%} +{{ "IAMbic Exceptions" | rich_format("bold blue") }} +{% for exception in iambic.exceptions -%} +{% if exception.num_templates > 0 -%} +{{ exception | rich_tree_exception() }} +{% endif -%} +{% endfor -%} +{% endif -%} diff --git a/iambic/output/text.py b/iambic/output/text.py new file mode 100644 index 000000000..2113ffe80 --- /dev/null +++ b/iambic/output/text.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import List + +import rich + +from iambic.core.models import ( + TemplateChangeDetails, +) +from iambic.output import get_template_env +from iambic.output.models import get_template_data + + +def file_render_resource_changes( + filepath: str, + resource_changes: List[TemplateChangeDetails], +) -> str: + # Get template data + template_data = get_template_data(resource_changes) + # Get template environment + env = get_template_env() + # Render template + template = env.get_template("text_file_summary.jinja2") + rendered_data = template.render(iambic=template_data) + with open(filepath, "w") as f: + f.write(rendered_data) + + +def screen_render_resource_changes(resource_changes: List[TemplateChangeDetails]): + template_data = get_template_data(resource_changes) + env = get_template_env() + template = env.get_template("text_screen_summary.jinja2") + rendered_template = template.render(iambic=template_data) + rich.print(rendered_template) + return rendered_template diff --git a/iambic/plugins/v0_1_0/aws/iam/group/utils.py b/iambic/plugins/v0_1_0/aws/iam/group/utils.py index 33069b360..6f41b53cd 100644 --- a/iambic/plugins/v0_1_0/aws/iam/group/utils.py +++ b/iambic/plugins/v0_1_0/aws/iam/group/utils.py @@ -139,6 +139,7 @@ async def apply_group_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -166,6 +167,7 @@ async def apply_group_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -212,6 +214,7 @@ async def apply_group_inline_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DELETE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", ) @@ -254,6 +257,7 @@ async def apply_group_inline_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.UPDATE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", change_summary=policy_drift, @@ -267,6 +271,7 @@ async def apply_group_inline_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.CREATE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", new_value=policy_document, diff --git a/iambic/plugins/v0_1_0/aws/iam/policy/utils.py b/iambic/plugins/v0_1_0/aws/iam/policy/utils.py index 04203e835..e365fa4f2 100644 --- a/iambic/plugins/v0_1_0/aws/iam/policy/utils.py +++ b/iambic/plugins/v0_1_0/aws/iam/policy/utils.py @@ -167,6 +167,8 @@ async def apply_update_managed_policy( change_type=ProposedChangeType.UPDATE, attribute="policy_document", change_summary=policy_drift, + resource_type="aws:policy_document", + resource_id=policy_arn, current_value=existing_policy_document, new_value=template_policy_document, ) @@ -236,6 +238,8 @@ async def apply_managed_policy_tags( ProposedChange( change_type=ProposedChangeType.DETACH, attribute="tags", + resource_type="aws:policy_document", + resource_id=policy_arn, change_summary={"TagKeys": tags_to_remove}, ) ] @@ -256,6 +260,8 @@ async def apply_managed_policy_tags( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", + resource_id=policy_arn, attribute="tags", new_value=tag, ) diff --git a/iambic/plugins/v0_1_0/aws/iam/role/utils.py b/iambic/plugins/v0_1_0/aws/iam/role/utils.py index ea7e72c87..2989ec8eb 100644 --- a/iambic/plugins/v0_1_0/aws/iam/role/utils.py +++ b/iambic/plugins/v0_1_0/aws/iam/role/utils.py @@ -182,6 +182,8 @@ async def untag_role(): ProposedChange( change_type=ProposedChangeType.DETACH, attribute="tags", + resource_type="aws:iam:role", + resource_id=role_name, change_summary={"TagKeys": tags_to_remove}, exceptions_seen=exceptions, ) @@ -193,6 +195,8 @@ async def untag_role(): ProposedChange( change_type=ProposedChangeType.DETACH, attribute="tags", + resource_type="aws:iam:role", + resource_id=role_name, change_summary={"TagKeys": tags_to_remove}, ) ) @@ -219,6 +223,8 @@ async def tag_role(): ProposedChange( change_type=ProposedChangeType.ATTACH, attribute="tags", + resource_type="aws:iam:role", + resource_id=role_name, new_value=tag, exceptions_seen=exceptions, ) @@ -232,6 +238,8 @@ async def tag_role(): ProposedChange( change_type=ProposedChangeType.ATTACH, attribute="tags", + resource_type="aws:iam:role", + resource_id=role_name, new_value=tag, ) ) @@ -281,6 +289,8 @@ async def update_assume_role_policy( ProposedChange( change_type=ProposedChangeType.UPDATE, attribute="assume_role_policy_document", + resource_type="aws:iam:role", + resource_id=role_name, change_summary=policy_drift, current_value=existing_policy_document, new_value=template_policy_document, @@ -291,6 +301,8 @@ async def update_assume_role_policy( ProposedChange( change_type=ProposedChangeType.CREATE, attribute="assume_role_policy_document", + resource_type="aws:iam:role", + resource_id=role_name, new_value=template_policy_document, ) ) @@ -332,6 +344,7 @@ async def apply_role_managed_policies( response.append( ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -360,6 +373,7 @@ async def apply_role_managed_policies( response.append( ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -409,6 +423,7 @@ async def apply_role_permission_boundary( ProposedChange( change_type=ProposedChangeType.ATTACH, resource_id=template_boundary_policy_arn, + resource_type="aws:policy_document", attribute="permission_boundary", ) ] @@ -441,6 +456,7 @@ async def apply_role_permission_boundary( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=existing_boundary_policy_arn, attribute="permission_boundary", ) @@ -497,6 +513,7 @@ async def apply_role_inline_policies( response.append( ProposedChange( change_type=ProposedChangeType.DELETE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", ) @@ -537,6 +554,7 @@ async def apply_role_inline_policies( response.append( ProposedChange( change_type=ProposedChangeType.UPDATE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", change_summary=policy_drift, @@ -550,6 +568,7 @@ async def apply_role_inline_policies( response.append( ProposedChange( change_type=ProposedChangeType.CREATE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", new_value=policy_document, diff --git a/iambic/plugins/v0_1_0/aws/iam/user/utils.py b/iambic/plugins/v0_1_0/aws/iam/user/utils.py index 1bdde5635..b3fd46c16 100644 --- a/iambic/plugins/v0_1_0/aws/iam/user/utils.py +++ b/iambic/plugins/v0_1_0/aws/iam/user/utils.py @@ -155,6 +155,8 @@ async def apply_user_tags( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:iam:user", + resource_id=user_name, attribute="tags", change_summary={"TagKeys": tags_to_remove}, ) @@ -180,6 +182,8 @@ async def apply_user_tags( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:iam:user", + resource_id=user_name, attribute="tags", new_value=tag, ) @@ -226,6 +230,7 @@ async def apply_user_permission_boundary( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=template_boundary_policy_arn, attribute="permission_boundary", ) @@ -259,6 +264,7 @@ async def apply_user_permission_boundary( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=existing_boundary_policy_arn, attribute="permission_boundary", ) @@ -315,6 +321,7 @@ async def apply_user_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -335,6 +342,7 @@ async def apply_user_managed_policies( [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -357,6 +365,7 @@ async def apply_user_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -378,6 +387,7 @@ async def apply_user_managed_policies( [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -421,6 +431,7 @@ async def apply_user_inline_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DELETE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", ) @@ -464,6 +475,7 @@ async def apply_user_inline_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.UPDATE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", change_summary=policy_drift, @@ -477,6 +489,7 @@ async def apply_user_inline_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.CREATE, + resource_type="aws:policy_document", resource_id=policy_name, attribute="inline_policies", new_value=policy_document, @@ -524,6 +537,7 @@ async def apply_user_groups( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.CREATE, + resource_type="aws:iam:group", resource_id=group, attribute="groups", ) @@ -548,6 +562,7 @@ async def apply_user_groups( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DELETE, + resource_type="aws:iam:group", resource_id=group, attribute="groups", ) diff --git a/iambic/plugins/v0_1_0/aws/identity_center/permission_set/utils.py b/iambic/plugins/v0_1_0/aws/identity_center/permission_set/utils.py index aae5178f9..daae6cd45 100644 --- a/iambic/plugins/v0_1_0/aws/identity_center/permission_set/utils.py +++ b/iambic/plugins/v0_1_0/aws/identity_center/permission_set/utils.py @@ -218,6 +218,7 @@ async def apply_permission_set_aws_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -246,6 +247,7 @@ async def apply_permission_set_aws_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=policy_arn, attribute="managed_policies", ) @@ -319,6 +321,7 @@ async def apply_permission_set_customer_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:policy_document", resource_id=f"{policy['Path']}{policy['Name']}", attribute="customer_managed_policies", ) @@ -351,6 +354,7 @@ async def apply_permission_set_customer_managed_policies( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:policy_document", resource_id=f"{policy['Path']}{policy['Name']}", attribute="customer_managed_policies", ) @@ -508,12 +512,13 @@ async def apply_account_assignments( for assignment_id, assignment in existing_assignment_map.items(): if not template_assignment_map.get(assignment_id): log_str = "Stale assignments discovered." + resource_type="arn:aws:iam::aws:user" if assignment["resource_type"] == "USER" else "arn:aws:iam::aws:group" proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DELETE, account=assignment["account_name"], resource_id=assignment["resource_name"], - resource_type=assignment["resource_type"], + resource_type=resource_type, attribute="account_assignment", ) ] @@ -535,12 +540,13 @@ async def apply_account_assignments( for assignment_id, assignment in template_assignment_map.items(): if not existing_assignment_map.get(assignment_id): + resource_type="arn:aws:iam::aws:user" if assignment["resource_type"] == "USER" else "arn:aws:iam::aws:group" proposed_changes = [ ProposedChange( change_type=ProposedChangeType.CREATE, account=assignment["account_name"], resource_id=assignment["resource_name"], - resource_type=assignment["resource_type"], + resource_type=resource_type, attribute="account_assignment", ) ] @@ -602,6 +608,8 @@ async def apply_permission_set_inline_policy( ProposedChange( change_type=ProposedChangeType.UPDATE, attribute="inline_policy_document", + resource_id=permission_set_arn, + resource_type="aws:identity_center:permission_set", change_summary=policy_drift, current_value=existing_inline_policy, new_value=template_inline_policy, @@ -611,6 +619,8 @@ async def apply_permission_set_inline_policy( response.append( ProposedChange( change_type=ProposedChangeType.CREATE, + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, attribute="inline_policy_document", new_value=template_inline_policy, ) @@ -632,6 +642,8 @@ async def apply_permission_set_inline_policy( ProposedChange( change_type=ProposedChangeType.DELETE, attribute="inline_policy", + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, current_value=existing_inline_policy, ) ) @@ -676,6 +688,8 @@ async def apply_permission_set_permission_boundary( ProposedChange( change_type=ProposedChangeType.UPDATE, attribute="permissions_boundary", + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, change_summary=policy_drift, current_value=existing_permission_boundary, new_value=template_permission_boundary, @@ -685,6 +699,8 @@ async def apply_permission_set_permission_boundary( response.append( ProposedChange( change_type=ProposedChangeType.CREATE, + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, attribute="permissions_boundary", new_value=template_permission_boundary, ) @@ -705,6 +721,8 @@ async def apply_permission_set_permission_boundary( response.append( ProposedChange( change_type=ProposedChangeType.DELETE, + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, attribute="permissions_boundary", current_value=existing_permission_boundary, ) @@ -747,6 +765,8 @@ async def apply_permission_set_tags( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.DETACH, + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, attribute="tags", change_summary={"TagKeys": tags_to_remove}, ) @@ -768,6 +788,8 @@ async def apply_permission_set_tags( proposed_changes = [ ProposedChange( change_type=ProposedChangeType.ATTACH, + resource_type="aws:identity_center:permission_set", + resource_id=permission_set_arn, attribute="tags", new_value=tag, ) diff --git a/iambic/plugins/v0_1_0/azure_ad/group/utils.py b/iambic/plugins/v0_1_0/azure_ad/group/utils.py index ff8b2cb06..5927e83e6 100644 --- a/iambic/plugins/v0_1_0/azure_ad/group/utils.py +++ b/iambic/plugins/v0_1_0/azure_ad/group/utils.py @@ -299,6 +299,8 @@ async def update_group_members( change_summary={ "MembersToRemove": [member.dict() for member in members_to_remove] }, + current_value=[member.name for member in cloud_group.members], + new_value=[member.name for member in members_to_remove], ) ) @@ -312,6 +314,8 @@ async def update_group_members( change_summary={ "MembersToAdd": [member.dict() for member in members_to_add] }, + current_value=[member.name for member in cloud_group.members], + new_value=[member.name for member in members_to_add], ) ) @@ -378,6 +382,8 @@ async def delete_group( resource_type=group.resource_type, attribute="group", change_summary={"group": group.name}, + current_value=group.name, + new_value=None, ) ] diff --git a/iambic/plugins/v0_1_0/azure_ad/user/utils.py b/iambic/plugins/v0_1_0/azure_ad/user/utils.py index 56faaf198..6cb93ff93 100644 --- a/iambic/plugins/v0_1_0/azure_ad/user/utils.py +++ b/iambic/plugins/v0_1_0/azure_ad/user/utils.py @@ -200,6 +200,8 @@ async def delete_user( resource_type=user.resource_type, attribute="user", change_summary={"user": user.username}, + current_value=user.username, + new_value=None, ) ] diff --git a/iambic/plugins/v0_1_0/google_workspace/group/models.py b/iambic/plugins/v0_1_0/google_workspace/group/models.py index 1a42eb14f..2097f452e 100644 --- a/iambic/plugins/v0_1_0/google_workspace/group/models.py +++ b/iambic/plugins/v0_1_0/google_workspace/group/models.py @@ -152,12 +152,14 @@ async def _apply_to_account( "Resource is marked for deletion, but does not exist in the cloud. Skipping.", ) return change_details - change_details.proposed_changes.append( - ProposedChange( - change_type=ProposedChangeType.CREATE, - resource_id=self.properties.email, - resource_type=self.properties.resource_type, - ) + change_details.extend_changes( + [ + ProposedChange( + change_type=ProposedChangeType.CREATE, + resource_id=self.properties.email, + resource_type=self.properties.resource_type, + ) + ] ) log_str = "New resource found in code." if not ctx.execute: @@ -232,18 +234,17 @@ async def _apply_to_account( google_project, log_params, ), + maybe_delete_group( + self, + google_project, + log_params, + ) ] ) changes_made = await asyncio.gather(*tasks) - deletion_change = await maybe_delete_group( - self, - google_project, - log_params, - ) - changes_made.extend(deletion_change) if any(changes_made): - change_details.proposed_changes.extend( + change_details.extend_changes( list(chain.from_iterable(changes_made)) ) diff --git a/iambic/plugins/v0_1_0/google_workspace/group/utils.py b/iambic/plugins/v0_1_0/google_workspace/group/utils.py index 4431fc079..41b931ced 100644 --- a/iambic/plugins/v0_1_0/google_workspace/group/utils.py +++ b/iambic/plugins/v0_1_0/google_workspace/group/utils.py @@ -110,6 +110,8 @@ async def update_group_domain( "current_domain": current_domain, "proposed_domain": proposed_domain, }, + current_value=current_domain, + new_value=proposed_domain, ) ) log.info( @@ -158,6 +160,8 @@ async def update_group_description( "current_description": current_description, "proposed_description": proposed_description, }, + current_value=current_description, + new_value=proposed_description, ) ) if ctx.execute: @@ -195,6 +199,7 @@ async def update_group_name( resource_id=group_email, resource_type="google:group:template", attribute="group_name", + current_value=current_name, new_value=proposed_name, ) ) @@ -241,6 +246,7 @@ async def update_group_email( resource_id=current_email, resource_type="google:group:template", attribute="group_email", + current_value=current_email, new_value=proposed_email, ) ) @@ -284,6 +290,7 @@ async def maybe_delete_group( resource_type=group.resource_type, attribute="group", change_summary={"group": group.properties.name}, + current_value=group.properties.name, ) ) if ctx.execute: @@ -369,6 +376,8 @@ async def update_group_members( change_summary={ "UsersToRemove": [user.email for user in users_to_remove] }, + current_value=[user.email for user in current_members], + new_value=[user.email for user in proposed_members], ) ) if ctx.execute: @@ -398,6 +407,8 @@ async def update_group_members( resource_type="google:group:template", attribute="users", change_summary={"UsersToAdd": [user.email for user in users_to_add]}, + current_value=[user.email for user in current_members], + new_value=[user.email for user in proposed_members], ) ) if ctx.execute: diff --git a/iambic/plugins/v0_1_0/okta/app/models.py b/iambic/plugins/v0_1_0/okta/app/models.py index 1638c0ae6..741e50919 100644 --- a/iambic/plugins/v0_1_0/okta/app/models.py +++ b/iambic/plugins/v0_1_0/okta/app/models.py @@ -232,7 +232,7 @@ async def _apply_to_account( changes_made = await asyncio.gather(*tasks) if any(changes_made): - change_details.proposed_changes.extend( + change_details.extend_changes( list(chain.from_iterable(changes_made)) ) diff --git a/iambic/plugins/v0_1_0/okta/app/utils.py b/iambic/plugins/v0_1_0/okta/app/utils.py index 250534d3c..401929873 100644 --- a/iambic/plugins/v0_1_0/okta/app/utils.py +++ b/iambic/plugins/v0_1_0/okta/app/utils.py @@ -265,6 +265,8 @@ async def update_app_assignments( "group_assignments_to_unassign": group_assignments_to_unassign, } }, + current_value=current_user_assignments + current_group_assignments, + new_value = desired_user_assignments + desired_group_assignments, ) ) @@ -413,6 +415,7 @@ async def update_app_name( resource_type=app.resource_type, resource_id=app.resource_id, attribute="app_name", + current_value=app.name, new_value=new_name, ) ) @@ -463,6 +466,8 @@ async def maybe_delete_app( resource_type=app.resource_type, attribute="app", change_summary={"app": app.name}, + current_value=app.name, + new_value=None, ) ) if ctx.execute: diff --git a/iambic/plugins/v0_1_0/okta/group/models.py b/iambic/plugins/v0_1_0/okta/group/models.py index 0f6b81f9f..204449df9 100644 --- a/iambic/plugins/v0_1_0/okta/group/models.py +++ b/iambic/plugins/v0_1_0/okta/group/models.py @@ -226,12 +226,14 @@ async def _apply_to_account( await self.remove_expired_resources() if not group_exists and not self.deleted: - change_details.proposed_changes.append( - ProposedChange( - change_type=ProposedChangeType.CREATE, - resource_id=self.properties.group_id, - resource_type=self.properties.resource_type, - ) + change_details.extend_changes( + [ + ProposedChange( + change_type=ProposedChangeType.CREATE, + resource_id=self.properties.group_id, + resource_type=self.properties.resource_type, + ) + ] ) log_str = "New resource found in code." if not ctx.execute: @@ -289,7 +291,7 @@ async def _apply_to_account( changes_made = await asyncio.gather(*tasks) if any(changes_made): - change_details.proposed_changes.extend( + change_details.extend_changes( list(chain.from_iterable(changes_made)) ) diff --git a/iambic/plugins/v0_1_0/okta/group/utils.py b/iambic/plugins/v0_1_0/okta/group/utils.py index 89fd21f17..bb20a5541 100644 --- a/iambic/plugins/v0_1_0/okta/group/utils.py +++ b/iambic/plugins/v0_1_0/okta/group/utils.py @@ -328,6 +328,7 @@ async def update_group_name( resource_id=group.group_id, resource_type=group.resource_type, attribute="group_name", + current_value=group.name, new_value=new_name, ) ) @@ -405,6 +406,8 @@ async def update_group_description( "current_description": group.description, "proposed_description": new_description, }, + current_value=group.description, + new_value=new_description, ) ) if ctx.execute: @@ -467,6 +470,8 @@ async def update_group_members( resource_type=group.resource_type, attribute="users", change_summary={"UsersToRemove": list(users_to_remove)}, + current_value=current_user_usernames, + new_value=users_to_remove, ) ) @@ -478,6 +483,8 @@ async def update_group_members( resource_type=group.resource_type, attribute="users", change_summary={"UsersToAdd": list(users_to_add)}, + current_value=current_user_usernames, + new_value=users_to_add, ) ) @@ -564,6 +571,8 @@ async def maybe_delete_group( resource_type=group.resource_type, attribute="group", change_summary={"group": group.name}, + current_value=group.name, + new_value=None, ) ) if ctx.execute: diff --git a/iambic/plugins/v0_1_0/okta/user/models.py b/iambic/plugins/v0_1_0/okta/user/models.py index b978e64e7..f8a8a87bc 100644 --- a/iambic/plugins/v0_1_0/okta/user/models.py +++ b/iambic/plugins/v0_1_0/okta/user/models.py @@ -200,12 +200,14 @@ async def _apply_to_account( proposed_user["status"] = "deprovisioned" if not user_exists and not self.deleted: - change_details.proposed_changes.append( - ProposedChange( - change_type=ProposedChangeType.CREATE, - resource_id=self.properties.username, - resource_type=self.properties.resource_type, - ) + change_details.extend_changes( + [ + ProposedChange( + change_type=ProposedChangeType.CREATE, + resource_id=self.properties.username, + resource_type=self.properties.resource_type, + ) + ] ) log_str = "New resource found in code." if not ctx.execute: @@ -261,7 +263,7 @@ async def _apply_to_account( changes_made = await asyncio.gather(*tasks) if any(changes_made): - change_details.proposed_changes.extend( + change_details.extend_changes( list(chain.from_iterable(changes_made)) ) diff --git a/iambic/plugins/v0_1_0/okta/user/utils.py b/iambic/plugins/v0_1_0/okta/user/utils.py index 73dc90aaa..f97e4a1cf 100644 --- a/iambic/plugins/v0_1_0/okta/user/utils.py +++ b/iambic/plugins/v0_1_0/okta/user/utils.py @@ -131,12 +131,18 @@ async def change_user_status( if user.status == new_status: return response + if user.status: + current_status = user.status.value + else: + current_status = None + response.append( ProposedChange( change_type=ProposedChangeType.UPDATE, resource_id=user.user_id, resource_type=user.resource_type, attribute="status", + current_value=current_status, new_value=new_status, ) ) @@ -186,6 +192,8 @@ async def update_user_profile( "current_profile": current_profile, "new_profile": new_profile, }, + current_value=current_profile, + new_value=new_profile, ) ) if ctx.execute: @@ -261,6 +269,8 @@ async def update_user_status( "current_status": current_status, "proposed_status": new_status, }, + current_value=current_status, + new_value=new_status, ) ) if ctx.execute: @@ -369,6 +379,8 @@ async def maybe_deprovision_user( resource_type=user.resource_type, attribute="user", change_summary={"user": user.username}, + current_value=user.username, + new_value=None, ) ) if ctx.execute: diff --git a/poetry.lock b/poetry.lock index 88ab1c974..541c9b70d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -227,14 +227,14 @@ botocore = "*" [[package]] name = "aws-sam-translator" -version = "1.62.0" +version = "1.63.0" description = "AWS SAM Translator is a library that transform SAM templates into AWS CloudFormation templates" category = "dev" optional = false python-versions = ">=3.7, <=4.0, !=4.0" files = [ - {file = "aws-sam-translator-1.62.0.tar.gz", hash = "sha256:2db24633fbc76b8e6eb76adaf0c1001a0d749288af91d85e7d9007e3b05479fa"}, - {file = "aws_sam_translator-1.62.0-py3-none-any.whl", hash = "sha256:5d198c8b4097b9210e1a44a64f55c4ee53b84f35d16ef1671b340242c41379cf"}, + {file = "aws-sam-translator-1.63.0.tar.gz", hash = "sha256:bf3cca23b0e08e483956649302e53ae15e2021ef74a8fa1f9ecb2c53d8ce324c"}, + {file = "aws_sam_translator-1.63.0-py3-none-any.whl", hash = "sha256:723683828d38e8769e4db0b0566ce56e923570b2cfdb88b965ce796a4554200f"}, ] [package.dependencies] @@ -244,7 +244,7 @@ pydantic = ">=1.8,<2.0" typing-extensions = ">=4.4,<5" [package.extras] -dev = ["black (==23.1.0)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.0.0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.0.0,<1.1.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (==0.0.253)", "tenacity (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] +dev = ["black (==23.1.0)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.0.0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.0.0,<1.1.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (==0.0.254)", "tenacity (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] [[package]] name = "aws-xray-sdk" @@ -314,18 +314,18 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.26.100" +version = "1.26.101" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.26.100-py3-none-any.whl", hash = "sha256:b5be5bcffe17d70a72622f8ecbb428df7b11ef8d1facdfa984e94c6fc9fa301b"}, - {file = "boto3-1.26.100.tar.gz", hash = "sha256:567f03ac638c3a6f4af00d88d081df7d6b8de4d127a26543c4ec1e7509e1a626"}, + {file = "boto3-1.26.101-py3-none-any.whl", hash = "sha256:5f5279a63b359ba8889e9a81b319e745b14216608ffb5a39fcbf269d1af1ea83"}, + {file = "boto3-1.26.101.tar.gz", hash = "sha256:670ae4d1875a2162e11c6e941888817c3e9cf1bb9a3335b3588d805b7d24da31"}, ] [package.dependencies] -botocore = ">=1.29.100,<1.30.0" +botocore = ">=1.29.101,<1.30.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -334,14 +334,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.29.100" +version = "1.29.101" description = "Low-level, data-driven core of boto 3." category = "main" optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.29.100-py3-none-any.whl", hash = "sha256:d5c4c5bbbbf0ec62a4235ccac1b9bbb579558f7bb3231d7fb6054e1f64d3a623"}, - {file = "botocore-1.29.100.tar.gz", hash = "sha256:ff6585df3dcef2057be5e54b45d254608d3769d726ea4ccd4e17f77825e5b13d"}, + {file = "botocore-1.29.101-py3-none-any.whl", hash = "sha256:60c7a7bf8e2a288735e507007a6769be03dc24815f7dc5c7b59b12743f4a31cf"}, + {file = "botocore-1.29.101.tar.gz", hash = "sha256:7bb60d9d4c49500df55dfb6005c16002703333ff5f69dada565167c8d493dfd5"}, ] [package.dependencies] @@ -773,6 +773,24 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] +[[package]] +name = "dictdiffer" +version = "0.9.0" +description = "Dictdiffer is a library that helps you to diff and patch dictionaries." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, + {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, +] + +[package.extras] +all = ["Sphinx (>=3)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "tox (>=3.7.0)"] +docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"] +numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"] +tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "tox (>=3.7.0)"] + [[package]] name = "distlib" version = "0.3.6" @@ -1054,14 +1072,14 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] [[package]] name = "google-api-python-client" -version = "2.82.0" +version = "2.83.0" description = "Google API Client Library for Python" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-python-client-2.82.0.tar.gz", hash = "sha256:22933a821bd46055b29fdf82aab96b13a9e33ff082dec2fc51cfc2efad9b0eea"}, - {file = "google_api_python_client-2.82.0-py2.py3-none-any.whl", hash = "sha256:80a97d900f10dc709b9ba8fe5e72073d69f2a2b7f6b87d02cf69015790bc0b56"}, + {file = "google-api-python-client-2.83.0.tar.gz", hash = "sha256:d07509f1b2d2b2427363b454db996f7a15e1751a48cfcaf28427050560dd51cf"}, + {file = "google_api_python_client-2.83.0-py2.py3-none-any.whl", hash = "sha256:afa7fe2a5d77e8f136cdb8f40a120dd6660c2292f791c1b22734dfe786bd1dac"}, ] [package.dependencies] @@ -1073,14 +1091,14 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.16.3" +version = "2.17.0" description = "Google Authentication Library" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ - {file = "google-auth-2.16.3.tar.gz", hash = "sha256:611779ce33a3aee265b94b74d4bb8c188f33010f5814761250a0ebbde94cc745"}, - {file = "google_auth-2.16.3-py2.py3-none-any.whl", hash = "sha256:4dfcfd8ecd1cf03ddc97fddfb3b1f2973ea4f3f664aa0d8cfaf582ef9f0c60e7"}, + {file = "google-auth-2.17.0.tar.gz", hash = "sha256:f51d26ebb3e5d723b9a7dbd310b6c88654ef1ad1fc35750d1fdba48ca4d82f52"}, + {file = "google_auth-2.17.0-py2.py3-none-any.whl", hash = "sha256:45ba9b4b3e49406de3c5451697820694b2f6ce8a6b75bb187852fdae231dab94"}, ] [package.dependencies] @@ -1433,6 +1451,31 @@ files = [ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, ] +[[package]] +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.1.2" @@ -1505,6 +1548,18 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mock" version = "5.0.1" @@ -1766,44 +1821,6 @@ files = [ [package.dependencies] setuptools = "*" -[[package]] -name = "numpy" -version = "1.24.2" -description = "Fundamental package for array computing in Python" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, - {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, - {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, - {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, - {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, - {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, - {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, - {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, - {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, - {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, - {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, -] - [[package]] name = "okta" version = "2.9.2" @@ -1895,55 +1912,6 @@ files = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] -[[package]] -name = "pandas" -version = "1.5.3" -description = "Powerful data structures for data analysis, time series, and statistics" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, - {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, -] -python-dateutil = ">=2.8.1" -pytz = ">=2020.1" - -[package.extras] -test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] - [[package]] name = "pathable" version = "0.4.3" @@ -2339,6 +2307,21 @@ requests = ">=2.14.0" [package.extras] integrations = ["cryptography"] +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pyjwt" version = "2.6.0" @@ -2389,14 +2372,14 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pyopenssl" -version = "23.1.0" +version = "23.1.1" description = "Python wrapper module around the OpenSSL library" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "pyOpenSSL-23.1.0-py3-none-any.whl", hash = "sha256:fb96e936866ad65662c22d0de84ca0fba58397893cdfe0f01334fa93382af23c"}, - {file = "pyOpenSSL-23.1.0.tar.gz", hash = "sha256:8cb78010a1eb2c8e24b851693b7b04dfe9b1dc0a5ab3843927b10a85b1dfbb2e"}, + {file = "pyOpenSSL-23.1.1-py3-none-any.whl", hash = "sha256:9e0c526404a210df9d2b18cd33364beadb0dc858a739b885677bc65e105d4a4c"}, + {file = "pyOpenSSL-23.1.1.tar.gz", hash = "sha256:841498b9bec61623b1b6c47ebbc02367c07d60e0e195f19790817f10cc8db0b7"}, ] [package.dependencies] @@ -2758,22 +2741,6 @@ prompt_toolkit = ">=2.0,<4.0" [package.extras] docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] -[[package]] -name = "recursive-diff" -version = "1.1.0" -description = "Recursively compare two Python data structures" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "recursive_diff-1.1.0.tar.gz", hash = "sha256:3c21cac59b4236fca84ee817fe9e659e9c7e0fbb703a76f0b99a82d63f7f234a"}, -] - -[package.dependencies] -numpy = ">=1.16" -pandas = ">=0.25" -xarray = ">=0.12" - [[package]] name = "regex" version = "2023.3.23" @@ -2902,6 +2869,25 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "rich" +version = "13.3.3" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.3.3-py3-none-any.whl", hash = "sha256:540c7d6d26a1178e8e8b37e9ba44573a3cd1464ff6348b99ee7061b95d1c6333"}, + {file = "rich-13.3.3.tar.gz", hash = "sha256:dc84400a9d842b3a9c5ff74addd8eb798d155f36c1c91303888e0a66850d2a15"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0,<3.0.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rsa" version = "4.9" @@ -3018,14 +3004,14 @@ pbr = "*" [[package]] name = "setuptools" -version = "67.6.0" +version = "67.6.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, - {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, ] [package.extras] @@ -3531,31 +3517,6 @@ files = [ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] -[[package]] -name = "xarray" -version = "2023.3.0" -description = "N-D labeled arrays and datasets in Python" -category = "main" -optional = false -python-versions = ">=3.9" -files = [ - {file = "xarray-2023.3.0-py3-none-any.whl", hash = "sha256:64b2a25338cff4f632a5d2ba66ffb875e9ce3ced68cefb5bb5736195bd28cff0"}, - {file = "xarray-2023.3.0.tar.gz", hash = "sha256:f05c74b60b072e6919ef2ae9cf3c67a46173da585ca5912808118ab0c61b2cca"}, -] - -[package.dependencies] -numpy = ">=1.21" -packaging = ">=21.3" -pandas = ">=1.4,<2" - -[package.extras] -accel = ["bottleneck", "flox", "numbagg", "scipy"] -complete = ["bottleneck", "cfgrib", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "matplotlib", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "rasterio", "scipy", "seaborn", "zarr"] -docs = ["bottleneck", "cfgrib", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "ipykernel", "ipython", "jupyter-client", "matplotlib", "nbsphinx", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "rasterio", "scanpydoc", "scipy", "seaborn", "sphinx-autosummary-accessors", "sphinx-rtd-theme", "zarr"] -io = ["cfgrib", "cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "rasterio", "scipy", "zarr"] -parallel = ["dask[complete]"] -viz = ["matplotlib", "nc-time-axis", "seaborn"] - [[package]] name = "xmltodict" version = "0.13.0" @@ -3767,4 +3728,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d6f77fb3ca47d5afc69d54d48b9c337cf12cc59bd89a63fa17c978c8727285d1" +content-hash = "385354529883cfd0bbfb6b8e046f83b2008f1d8f59d1a95bedd1a2f9d81d7649" diff --git a/pyproject.toml b/pyproject.toml index 3e94af647..187dcff0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,11 +53,12 @@ asyncache = "^0.3.1" dateparser = "^1.1.7" pytest-mock = "^3.10.0" questionary = "^1.10.0" -types-dateparser = "^1.1.4.8" -cryptography = "^39.0.2" -recursive-diff = "^1.1.0" +types-dateparser = "^1.1.4.5" +cryptography = "^39.0.1" aws-error-utils = "^2.7.0" types-mock = "^5.0.0.5" +rich = "^13.3.2" +dictdiffer = "^0.9.0" msal = "^1.21.0" aiohttp = "^3.8.4" pyopenssl = "^23.0.0" diff --git a/setup.cfg b/setup.cfg index f0422817d..9d4c128f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ use_parentheses=True line_length=88 [tool:pytest] -testpaths=test/ +testpaths=test/,functional_tests/ [coverage:run] omit = diff --git a/test/output/__init__.py b/test/output/__init__.py index e69de29bb..88ef94d9b 100644 --- a/test/output/__init__.py +++ b/test/output/__init__.py @@ -0,0 +1,541 @@ +from iambic.core.models import TemplateChangeDetails +from iambic.core.utils import yaml + + +update_template_yaml = """ - resource_id: t1000 + resource_type: aws:iam:role + template_path: resources/aws/roles/demo-1/t1000.yaml + proposed_changes: + - account: demo-1 - (972417093400) + resource_id: t1000 + current_value: + Path: / + RoleName: t1000 + RoleId: AROA6E2ETJ4MF7DDR6RK6 + Arn: arn:aws:iam::972417093400:role/t1000 + CreateDate: '2021-10-18T21:04:09+00:00' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - sts:AssumeRole + - sts:TagSession + - Effect: Allow + Principal: + AWS: arn:aws:iam::940552945933:role/NoqCentralRoleCorpNoqDev + Action: + - sts:AssumeRole + - sts:TagSession + Description: Allows EC2 instances to call AWS services on your behalf. + MaxSessionDuration: 3600 + Tags: + - Key: noq-tra-active-users + Value: '' + - Key: noq-tra-supported-groups + Value: engineering@noq.dev + - Key: noq-authorized + Value: engineering_group@noq.dev + RoleLastUsed: + LastUsedDate: '2023-01-27T22:37:21+00:00' + Region: us-west-2 + ManagedPolicies: [] + InlinePolicies: [] + new_value: + RoleName: t1000 + Description: Allows EC2 instances to call AWS services on your behalf. + MaxSessionDuration: 3600 + Path: / + Tags: + - Key: noq-authorized + Value: engineering_group@noq.dev + - Key: noq-tra-active-users + Value: '' + - Key: noq-tra-supported-groups + Value: engineering@noq.dev + ManagedPolicies: [] + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:ListBucket + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:CreateBucket + - PolicyName: spoke-acct-policy-333 + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:DeleteBucket + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:ListBucket + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - sts:AssumeRole + - sts:TagSession + - Effect: Allow + Principal: + AWS: arn:aws:iam::940552945933:role/NoqCentralRoleCorpNoqDev + Action: + - sts:AssumeRole + - sts:TagSession + proposed_changes: + - change_type: Create + attribute: inline_policies + resource_id: spoke-acct-policy + new_value: + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:ListBucket + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:CreateBucket + - change_type: Create + attribute: inline_policies + resource_id: spoke-acct-policy-333 + new_value: + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:DeleteBucket + - Effect: Allow + Principal: + AWS: arn:aws:iam::694815895589:role/NoqCentralRole + Action: + - s3:ListBucket + exceptions_seen: [] + exceptions_seen: [] +""" + + +template_yaml = """ - resource_id: prod_iambic_test_role + resource_type: aws:iam:role + template_path: resources/aws/iam/role/design-prod/iambic_test_role_prod.yaml + proposed_changes: + - account: design-prod - (006933239187) + resource_id: prod_iambic_test_role + new_value: + RoleName: prod_iambic_test_role + Description: IAMbic test role on design-prod + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:GetObject + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: prod_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + exceptions_seen: [] + - resource_id: '{{account_name}}_iambic_test_role' + resource_type: aws:iam:role + template_path: resources/aws/iam/role/all_accounts/iambic_test_role.yaml + proposed_changes: + - account: product-dev - (572565049541) + resource_id: product-dev_iambic_test_role + new_value: + RoleName: product-dev_iambic_test_role + Description: IAMbic test role on product-dev + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: product-dev_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: Iambic Standalone Org - (566255053759) + resource_id: IambicStandaloneOrg_iambic_test_role + new_value: + RoleName: IambicStandaloneOrg_iambic_test_role + Description: IAMbic test role on IambicStandaloneOrg + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: IambicStandaloneOrg_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-dev - (570737236821) + resource_id: design-dev_iambic_test_role + new_value: + RoleName: design-dev_iambic_test_role + Description: IAMbic test role on design-dev + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-dev_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-tools - (728312732489) + resource_id: design-tools_iambic_test_role + new_value: + RoleName: design-tools_iambic_test_role + Description: IAMbic test role on design-tools + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-tools_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-staging - (158048798909) + resource_id: design-staging_iambic_test_role + new_value: + RoleName: design-staging_iambic_test_role + Description: IAMbic test role on design-staging + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-staging_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-prod - (006933239187) + resource_id: design-prod_iambic_test_role + new_value: + RoleName: design-prod_iambic_test_role + Description: IAMbic test role on design-prod + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-prod_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-vpc - (172623945520) + resource_id: design-vpc_iambic_test_role + new_value: + RoleName: design-vpc_iambic_test_role + Description: IAMbic test role on design-vpc + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-vpc_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-workspaces - (667373557420) + resource_id: design-workspaces_iambic_test_role + new_value: + RoleName: design-workspaces_iambic_test_role + Description: IAMbic test role on design-workspaces + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-workspaces_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: design-test - (992251240124) + resource_id: design-test_iambic_test_role + new_value: + RoleName: design-test_iambic_test_role + Description: IAMbic test role on design-test + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:GetObject + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: design-test_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + - account: product-prod - (883466000970) + resource_id: product-prod_iambic_test_role + new_value: + RoleName: product-prod_iambic_test_role + Description: IAMbic test role on product-prod + MaxSessionDuration: 3600 + Path: /iambic_test/ + Tags: [] + ManagedPolicies: + - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + InlinePolicies: + - PolicyName: spoke-acct-policy + Statement: + - Effect: Deny + Action: + - s3:ListBucket + Resource: '*' + - Effect: Deny + Action: + - s3:ListAllMyBuckets + Resource: '*' + AssumeRolePolicyDocument: + Version: '2008-10-17' + Statement: + - Effect: Deny + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + PermissionsBoundary: + PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess + proposed_changes: + - change_type: Create + resource_id: product-prod_iambic_test_role + resource_type: aws:iam:role + exceptions_seen: [] + exceptions_seen: [] +""" + + +def get_templates_mixed(): + return [TemplateChangeDetails.parse_obj(x) for x in yaml.load(template_yaml)] + +def get_update_template(): + return [TemplateChangeDetails.parse_obj(x) for x in yaml.load(update_template_yaml)] diff --git a/test/output/test_markdown_generator.py b/test/output/test_markdown_generator.py index 1fad22ab5..c516e4b57 100644 --- a/test/output/test_markdown_generator.py +++ b/test/output/test_markdown_generator.py @@ -3,417 +3,10 @@ import pytest from iambic.core.models import TemplateChangeDetails -from iambic.core.utils import yaml -from iambic.output.markdown import ( - ActionSummaries, - get_template_data, - gh_render_resource_changes, -) - -template_yaml = """ - resource_id: prod_iambic_test_role - resource_type: aws:iam:role - template_path: resources/aws/iam/role/design-prod/iambic_test_role_prod.yaml - proposed_changes: - - account: design-prod - (006933239187) - resource_id: prod_iambic_test_role - new_value: - RoleName: prod_iambic_test_role - Description: IAMbic test role on design-prod - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:GetObject - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: prod_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - exceptions_seen: [] - - resource_id: '{{account_name}}_iambic_test_role' - resource_type: aws:iam:role - template_path: resources/aws/iam/role/all_accounts/iambic_test_role.yaml - proposed_changes: - - account: product-dev - (572565049541) - resource_id: product-dev_iambic_test_role - new_value: - RoleName: product-dev_iambic_test_role - Description: IAMbic test role on product-dev - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: product-dev_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: Iambic Standalone Org - (566255053759) - resource_id: IambicStandaloneOrg_iambic_test_role - new_value: - RoleName: IambicStandaloneOrg_iambic_test_role - Description: IAMbic test role on IambicStandaloneOrg - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: IambicStandaloneOrg_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-dev - (570737236821) - resource_id: design-dev_iambic_test_role - new_value: - RoleName: design-dev_iambic_test_role - Description: IAMbic test role on design-dev - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-dev_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-tools - (728312732489) - resource_id: design-tools_iambic_test_role - new_value: - RoleName: design-tools_iambic_test_role - Description: IAMbic test role on design-tools - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-tools_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-staging - (158048798909) - resource_id: design-staging_iambic_test_role - new_value: - RoleName: design-staging_iambic_test_role - Description: IAMbic test role on design-staging - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-staging_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-prod - (006933239187) - resource_id: design-prod_iambic_test_role - new_value: - RoleName: design-prod_iambic_test_role - Description: IAMbic test role on design-prod - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-prod_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-vpc - (172623945520) - resource_id: design-vpc_iambic_test_role - new_value: - RoleName: design-vpc_iambic_test_role - Description: IAMbic test role on design-vpc - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-vpc_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-workspaces - (667373557420) - resource_id: design-workspaces_iambic_test_role - new_value: - RoleName: design-workspaces_iambic_test_role - Description: IAMbic test role on design-workspaces - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-workspaces_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: design-test - (992251240124) - resource_id: design-test_iambic_test_role - new_value: - RoleName: design-test_iambic_test_role - Description: IAMbic test role on design-test - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:GetObject - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: design-test_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - - account: product-prod - (883466000970) - resource_id: product-prod_iambic_test_role - new_value: - RoleName: product-prod_iambic_test_role - Description: IAMbic test role on product-prod - MaxSessionDuration: 3600 - Path: /iambic_test/ - Tags: [] - ManagedPolicies: - - PolicyArn: arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - InlinePolicies: - - PolicyName: spoke-acct-policy - Statement: - - Effect: Deny - Action: - - s3:ListBucket - Resource: '*' - - Effect: Deny - Action: - - s3:ListAllMyBuckets - Resource: '*' - AssumeRolePolicyDocument: - Version: '2008-10-17' - Statement: - - Effect: Deny - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - PermissionsBoundary: - PolicyArn: arn:aws:iam::aws:policy/AWSDirectConnectReadOnlyAccess - proposed_changes: - - change_type: Create - resource_id: product-prod_iambic_test_role - resource_type: aws:iam:role - exceptions_seen: [] - exceptions_seen: [] -""" - +from iambic.output.markdown import gh_render_resource_changes +from iambic.output.models import ActionSummaries, get_template_data -def get_templates_mixed(): - return [TemplateChangeDetails.parse_obj(x) for x in yaml.load(template_yaml)] +from . import get_update_template, get_templates_mixed @pytest.mark.parametrize( @@ -423,6 +16,10 @@ def get_templates_mixed(): get_templates_mixed(), ActionSummaries(num_accounts=10, num_actions=1, num_templates=2), ), + ( + get_update_template(), + ActionSummaries(num_accounts=1, num_actions=1, num_templates=1), + ) ], ) def test_get_template_data( @@ -442,6 +39,10 @@ def test_get_template_data( get_templates_mixed(), ActionSummaries(num_accounts=10, num_actions=1, num_templates=2), ), + ( + get_update_template(), + ActionSummaries(num_accounts=10, num_actions=1, num_templates=1), + ) ], ) def test_gh_render_resource_changes( diff --git a/test/output/test_text_generator.py b/test/output/test_text_generator.py new file mode 100644 index 000000000..61c0db075 --- /dev/null +++ b/test/output/test_text_generator.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import pytest +from rich import print + +from iambic.core.models import TemplateChangeDetails +from iambic.output.text import file_render_resource_changes, screen_render_resource_changes + +from . import get_update_template, get_templates_mixed + + +@pytest.mark.parametrize( + "template_change_details, expected_outputs", + [ + ( + get_templates_mixed(), + [ + "aws:iam:role // product-dev_iambic_test_role", + "design-prod - (006933239187)", + "Iambic Standalone Org - (566255053759)", + "aws:iam:role // design-dev_iambic_test_role", + "aws:iam:role // design-workspaces_iambic_test_role", + "design-prod - (006933239187)", + ] + ), + ( + get_update_template(), + [ + "resources/aws/roles/demo-1/t1000.yaml", + ] + + ) + ], +) +def test_screen_render_resource_changes( + template_change_details: list[TemplateChangeDetails], + expected_outputs: str, +): + rendered_text = screen_render_resource_changes(template_change_details) + output = rendered_text + print(output) + for expected_output in expected_outputs: + assert expected_output in output + + +@pytest.mark.parametrize( + "template_change_details, expected_outputs", + [ + ( + get_templates_mixed(), + [ + "aws:iam:role // product-dev_iambic_test_role", + "design-prod - (006933239187)", + "Iambic Standalone Org - (566255053759)", + "aws:iam:role // design-dev_iambic_test_role", + "aws:iam:role // design-workspaces_iambic_test_role", + "design-prod - (006933239187)", + ] + ), + ( + get_update_template(), + [ + "resources/aws/roles/demo-1/t1000.yaml", + ] + + ) + ], +) +def test_file_render_resource_changes( + tmp_path, + template_change_details: list[TemplateChangeDetails], + expected_outputs: str, +): + test_file = tmp_path / "test_file_render_resource_changes.txt" + file_render_resource_changes(str(test_file), template_change_details) + + with open(test_file, "r") as f: + output = f.read() + for expected_output in expected_outputs: + assert expected_output in output