cdk_utils.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import os
  7. import pytest
  8. import boto3
  9. import uuid
  10. import logging
  11. import subprocess
  12. import botocore
  13. import ly_test_tools.environment.process_utils as process_utils
  14. from typing import List
  15. BOOTSTRAP_STACK_NAME = 'CDKToolkit'
  16. BOOTSTRAP_STAGING_BUCKET_LOGIC_ID = 'StagingBucket'
  17. logger = logging.getLogger(__name__)
  18. class Cdk:
  19. """
  20. Cdk class that provides methods to run cdk application commands.
  21. Expects system to have NodeJS, AWS CLI and CDK installed globally and have their paths setup as env variables.
  22. """
  23. def __init__(self):
  24. self._cdk_env = ''
  25. self._stacks = []
  26. self._cdk_path = os.path.dirname(os.path.realpath(__file__))
  27. self._session = ''
  28. cdk_npm_latest_version_cmd = ['npm', 'view', 'aws-cdk', 'version']
  29. output = process_utils.check_output(
  30. cdk_npm_latest_version_cmd,
  31. cwd=self._cdk_path,
  32. shell=True)
  33. cdk_npm_latest_version = output.split()[0]
  34. cdk_version_cmd = ['cdk', 'version']
  35. output = process_utils.check_output(
  36. cdk_version_cmd,
  37. cwd=self._cdk_path,
  38. shell=True)
  39. cdk_version = output.split()[0]
  40. logger.info(f'Current CDK version {cdk_version}')
  41. if cdk_version != cdk_npm_latest_version:
  42. try:
  43. logger.info(f'Updating CDK to latest')
  44. # uninstall and reinstall cdk in case npm has been updated.
  45. output = process_utils.check_output(
  46. 'npm uninstall -g aws-cdk',
  47. cwd=self._cdk_path,
  48. shell=True)
  49. logger.info(f'Uninstall CDK output: {output}')
  50. output = process_utils.check_output(
  51. 'npm install -g aws-cdk@latest',
  52. cwd=self._cdk_path,
  53. shell=True)
  54. logger.info(f'Install CDK output: {output}')
  55. except subprocess.CalledProcessError as error:
  56. logger.warning(f'Failed reinstalling latest CDK on npm'
  57. f'\nError:{error.stderr}')
  58. def setup(self, cdk_path: str, project: str, account_id: str,
  59. workspace: pytest.fixture, session: boto3.session.Session, bootstrap_required: bool):
  60. """
  61. :param cdk_path: Path where cdk app.py is stored.
  62. :param project: Project name used for cdk project name env variable.
  63. :param account_id: AWS account id to use with cdk application.
  64. :param workspace: ly_test_tools workspace fixture.
  65. :param workspace: bootstrap_required deploys bootstrap stack.
  66. """
  67. self._cdk_env = os.environ.copy()
  68. unique_id = uuid.uuid4().hex[-4:]
  69. self._cdk_env['O3DE_AWS_PROJECT_NAME'] = project[:4] + unique_id if len(project) > 4 else project + unique_id
  70. self._cdk_env['O3DE_AWS_DEPLOY_REGION'] = session.region_name
  71. self._cdk_env['O3DE_AWS_DEPLOY_ACCOUNT'] = account_id
  72. self._cdk_env['PATH'] = f'{workspace.paths.engine_root()}\\python;' + self._cdk_env['PATH']
  73. credentials = session.get_credentials().get_frozen_credentials()
  74. self._cdk_env['AWS_ACCESS_KEY_ID'] = credentials.access_key
  75. self._cdk_env['AWS_SECRET_ACCESS_KEY'] = credentials.secret_key
  76. self._cdk_env['AWS_SESSION_TOKEN'] = credentials.token
  77. self._cdk_path = cdk_path
  78. self._session = session
  79. output = process_utils.check_output(
  80. 'python -m pip install -r requirements.txt',
  81. cwd=self._cdk_path,
  82. env=self._cdk_env,
  83. shell=True)
  84. logger.info(f'Installing cdk python dependencies: {output}')
  85. if bootstrap_required:
  86. self.bootstrap()
  87. def bootstrap(self) -> None:
  88. """
  89. Deploy the bootstrap stack.
  90. """
  91. try:
  92. bootstrap_cmd = ['cdk', 'bootstrap',
  93. f'aws://{self._cdk_env["O3DE_AWS_DEPLOY_ACCOUNT"]}/{self._cdk_env["O3DE_AWS_DEPLOY_REGION"]}']
  94. process_utils.check_call(
  95. bootstrap_cmd,
  96. cwd=self._cdk_path,
  97. env=self._cdk_env,
  98. shell=True)
  99. except botocore.exceptions.ClientError as clientError:
  100. logger.warning(f'Failed creating Bootstrap stack {BOOTSTRAP_STACK_NAME} not found. '
  101. f'\nError:{clientError["Error"]["Message"]}')
  102. def list(self) -> List[str]:
  103. """
  104. lists cdk stack names
  105. :return List of cdk stack names
  106. """
  107. if not self._cdk_path:
  108. return []
  109. list_cdk_application_cmd = ['cdk', 'list']
  110. output = process_utils.check_output(
  111. list_cdk_application_cmd,
  112. cwd=self._cdk_path,
  113. env=self._cdk_env,
  114. shell=True)
  115. return output.splitlines()
  116. def synthesize(self) -> None:
  117. """
  118. Synthesizes all cdk stacks
  119. """
  120. if not self._cdk_path:
  121. return
  122. list_cdk_application_cmd = ['cdk', 'synth']
  123. process_utils.check_output(
  124. list_cdk_application_cmd,
  125. cwd=self._cdk_path,
  126. env=self._cdk_env,
  127. shell=True)
  128. def deploy(self, context_variable: str = '', additonal_params: List[str] = None) -> List[str]:
  129. """
  130. Deploys all the CDK stacks.
  131. :param context_variable: Context variable for enabling optional features.
  132. :param additonal_params: Additonal parameters like --all can be passed in this way.
  133. :return List of deployed stack arns.
  134. """
  135. if not self._cdk_path:
  136. return []
  137. deploy_cdk_application_cmd = ['cdk', 'deploy', '--require-approval', 'never']
  138. if additonal_params:
  139. deploy_cdk_application_cmd.extend(additonal_params)
  140. if context_variable:
  141. deploy_cdk_application_cmd.extend(['-c', f'{context_variable}'])
  142. output = process_utils.check_output(
  143. deploy_cdk_application_cmd,
  144. cwd=self._cdk_path,
  145. env=self._cdk_env,
  146. shell=True)
  147. stacks = []
  148. for line in output.splitlines():
  149. line_sections = line.split('/')
  150. assert len(line_sections), 3
  151. stacks.append(line.split('/')[-2])
  152. return stacks
  153. def destroy(self) -> None:
  154. """
  155. Destroys the cdk application.
  156. """
  157. logger.info(f'CDK Path {self._cdk_path}')
  158. destroy_cdk_application_cmd = ['cdk', 'destroy', '--all', '-f']
  159. try:
  160. process_utils.check_output(
  161. destroy_cdk_application_cmd,
  162. cwd=self._cdk_path,
  163. env=self._cdk_env,
  164. shell=True)
  165. except subprocess.CalledProcessError as e:
  166. logger.error(e.output)
  167. raise e
  168. self._stacks = []
  169. def remove_bootstrap_stack(self) -> None:
  170. """
  171. Remove the CDK bootstrap stack.
  172. :param aws_utils: aws_utils fixture.
  173. """
  174. # Check if the bootstrap stack exists.
  175. response = self._session.client('cloudformation').describe_stacks(
  176. StackName=BOOTSTRAP_STACK_NAME
  177. )
  178. stacks = response.get('Stacks', [])
  179. if not stacks or len(stacks) is 0:
  180. return
  181. # Clear the bootstrap staging bucket before deleting the bootstrap stack.
  182. response = self._session.client('cloudformation').describe_stack_resource(
  183. StackName=BOOTSTRAP_STACK_NAME,
  184. LogicalResourceId=BOOTSTRAP_STAGING_BUCKET_LOGIC_ID
  185. )
  186. staging_bucket_name = response.get('StackResourceDetail', {}).get('PhysicalResourceId', '')
  187. if staging_bucket_name:
  188. s3 = self._session.resource('s3')
  189. bucket = s3.Bucket(staging_bucket_name)
  190. for key in bucket.objects.all():
  191. key.delete()
  192. # Delete the bootstrap stack.
  193. # Should not need to delete the stack if S3 bucket can be cleaned.
  194. # self._session.client('cloudformation').delete_stack(
  195. # StackName=BOOTSTRAP_STACK_NAME
  196. # )