diff --git a/docs/admin-guide.md b/docs/admin-guide.md index 1f8b62c72..2713b2502 100644 --- a/docs/admin-guide.md +++ b/docs/admin-guide.md @@ -212,6 +212,17 @@ Config has five components in `main-notification-endpoint`, `scp`, `scm`, `master` instead. We recommend configuring the main scm branch name to `main`. As new repositories will most likely use this branch name as their default branch. +- `org` configures settings in case of staged multi-organization ADF deployments. + - `stage` defines the AWS Organization stage in case of staged multi- + organization ADF deployments. This is an optional setting. In enterprise- + grade deployments, it is a common practice to define an explicit dev, int + and prod AWS Organization with its own ADF instance per AWS organization. + This approach allows for well-tested and stable prod AWS Organization + deployments. If set, a matching SSM parameter `/adf/org/stage` gets + created that you can reference in your buildspec files to allow for + org-specific deployments; without hardcoding the AWS Organization stage in + your buildspec. If this variable is not set, the SSM parameter + `/adf/org/stage` defaults to "none". ## Accounts diff --git a/src/lambda_codebase/initial_commit/adfconfig.yml.j2 b/src/lambda_codebase/initial_commit/adfconfig.yml.j2 index 2d70b36d2..4f275a87d 100644 --- a/src/lambda_codebase/initial_commit/adfconfig.yml.j2 +++ b/src/lambda_codebase/initial_commit/adfconfig.yml.j2 @@ -27,3 +27,6 @@ config: scm: auto-create-repositories: enabled default-scm-branch: main + #org: + # Optional: Use this variable to define the AWS Organization in case of staged multi-organization ADF deployments + #stage: dev diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/main.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/main.py index 48668444d..fd268ebc7 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/main.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/main.py @@ -56,6 +56,7 @@ "ACCOUNT_BOOTSTRAPPING_STATE_MACHINE_ARN" ) ADF_DEFAULT_SCM_FALLBACK_BRANCH = 'master' +ADF_DEFAULT_ORG_STAGE = "none" LOGGER = configure_logger(__name__) @@ -120,8 +121,8 @@ def prepare_deployment_account(sts, deployment_account_id, config): f'{config.cross_account_access_role}', 'master' ) - for region in list( - set([config.deployment_account_region] + config.target_regions)): + for region in sorted(list( + set([config.deployment_account_region] + config.target_regions))): deployment_account_parameter_store = ParameterStore( region, deployment_account_role @@ -151,6 +152,13 @@ def prepare_deployment_account(sts, deployment_account_id, config): ADF_DEFAULT_SCM_FALLBACK_BRANCH, ) ) + deployment_account_parameter_store.put_parameter( + '/adf/org/stage', + config.config.get('org', {}).get( + 'stage', + ADF_DEFAULT_ORG_STAGE, + ) + ) auto_create_repositories = config.config.get( 'scm', {}).get('auto-create-repositories') if auto_create_repositories is not None: diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/tests/test_main.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/tests/test_main.py index 7ff8ba183..0706c519f 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/tests/test_main.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/tests/test_main.py @@ -7,10 +7,11 @@ from pytest import fixture from parameter_store import ParameterStore -from mock import Mock, patch, call +from mock import MagicMock, Mock, patch, call from main import ( Config, ensure_generic_account_can_be_setup, + prepare_deployment_account, update_deployment_account_output_parameters, ) @@ -30,18 +31,20 @@ def cls(): @fixture def sts(): sts = Mock() - sts.assume_cross_account_role.return_value = { - 'Credentials': { - 'AccessKeyId': 'string', - 'SecretAccessKey': 'string', - 'SessionToken': 'string', - 'Expiration': 12345 - }, - 'AssumedRoleUser': { - 'AssumedRoleId': 'string', - 'Arn': 'string' - } + + role_mock = Mock() + role_mock.client = Mock() + role_mock.Credentials = { + 'AccessKeyId': 'string', + 'SecretAccessKey': 'string', + 'SessionToken': 'string', + 'Expiration': 12345 + } + role_mock.AssumedRoleUser = { + 'AssumedRoleId': 'string', + 'Arn': 'string' } + sts.assume_cross_account_role.return_value = role_mock return sts @@ -79,3 +82,171 @@ def test_update_deployment_account_output_parameters(cls, sts): ) assert 4 == mock.call_count mock.assert_has_calls(expected_calls, any_order=True) + + +@patch('main.ParameterStore') +def test_prepare_deployment_account_defaults(param_store_cls, cls, sts): + deploy_param_store = MagicMock() + parameter_stores = { + 'eu-central-1': deploy_param_store, + 'eu-west-1': MagicMock(), + 'us-west-2': MagicMock(), + } + parameter_store_list = [ + deploy_param_store, + parameter_stores['eu-west-1'], + parameter_stores['us-west-2'], + ] + param_store_cls.side_effect = [ + parameter_stores['eu-west-1'], + parameter_stores['us-west-2'], + deploy_param_store, + deploy_param_store, + ] + deployment_account_id = "111122223333" + prepare_deployment_account( + sts=sts, + deployment_account_id=deployment_account_id, + config=cls, + ) + assert param_store_cls.call_count == 4 + param_store_cls.assert_has_calls( + [ + call( + 'eu-central-1', + sts.assume_cross_account_role.return_value, + ), + call( + 'eu-west-1', + sts.assume_cross_account_role.return_value, + ), + call( + 'us-west-2', + sts.assume_cross_account_role.return_value, + ), + call( + 'eu-central-1', + sts.assume_cross_account_role.return_value, + ), + ], + any_order=False, + ) + for param_store in parameter_store_list: + assert param_store.put_parameter.call_count == ( + 11 if param_store == deploy_param_store else 2 + ) + param_store.put_parameter.assert_has_calls( + [ + call('organization_id', 'o-123456789'), + call('/adf/extensions/terraform/enabled', 'False'), + ], + any_order=False, + ) + deploy_param_store.put_parameter.assert_has_calls( + [ + call('adf_version', '1.0.0'), + call('adf_log_level', 'INFO'), + call('deployment_account_bucket', 'some_deployment_account_bucket'), + call('default_scm_branch', 'master'), + call('/adf/org/stage', 'none'), + call('cross_account_access_role', 'some_role'), + call('notification_type', 'email'), + call('notification_endpoint', 'john@example.com'), + call('/adf/extensions/terraform/enabled', 'False'), + ], + ) + + +@patch('main.ParameterStore') +def test_prepare_deployment_account_specific_config(param_store_cls, cls, sts): + deploy_param_store = MagicMock() + parameter_stores = { + 'eu-central-1': deploy_param_store, + 'eu-west-1': MagicMock(), + 'us-west-2': MagicMock(), + } + parameter_store_list = [ + deploy_param_store, + parameter_stores['eu-west-1'], + parameter_stores['us-west-2'], + ] + param_store_cls.side_effect = [ + parameter_stores['eu-west-1'], + parameter_stores['us-west-2'], + deploy_param_store, + deploy_param_store, + ] + deployment_account_id = "111122223333" + # Set optional config + cls.notification_type = 'slack' + cls.notification_endpoint = 'slack-channel' + cls.notification_channel = 'slack-channel' + cls.config['scm'] = { + 'auto-create-repositories': 'disabled', + 'default-scm-branch': 'main', + } + cls.config['extensions'] = { + 'terraform': { + 'enabled': 'True', + }, + } + cls.config['org'] = { + 'stage': 'test-stage', + } + prepare_deployment_account( + sts=sts, + deployment_account_id=deployment_account_id, + config=cls, + ) + assert param_store_cls.call_count == 4 + param_store_cls.assert_has_calls( + [ + call( + 'eu-central-1', + sts.assume_cross_account_role.return_value, + ), + call( + 'eu-west-1', + sts.assume_cross_account_role.return_value, + ), + call( + 'us-west-2', + sts.assume_cross_account_role.return_value, + ), + call( + 'eu-central-1', + sts.assume_cross_account_role.return_value, + ), + ], + any_order=False, + ) + for param_store in parameter_store_list: + assert param_store.put_parameter.call_count == ( + 13 if param_store == deploy_param_store else 2 + ) + param_store.put_parameter.assert_has_calls( + [ + call('organization_id', 'o-123456789'), + call('/adf/extensions/terraform/enabled', 'False'), + ], + any_order=False, + ) + deploy_param_store.put_parameter.assert_has_calls( + [ + call('adf_version', '1.0.0'), + call('adf_log_level', 'INFO'), + call('deployment_account_bucket', 'some_deployment_account_bucket'), + call('default_scm_branch', 'main'), + call('/adf/org/stage', 'test-stage'), + call('auto_create_repositories', 'disabled'), + call('cross_account_access_role', 'some_role'), + call('notification_type', 'slack'), + call( + 'notification_endpoint', + "arn:aws:lambda:eu-central-1:" + f"{deployment_account_id}:function:SendSlackNotification", + ), + call('/notification_endpoint/main', 'slack-channel'), + call('/adf/extensions/terraform/enabled', 'False'), + ], + )