tasks/idea.py (399 lines of code) (raw):

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # with the License. A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, express or mplied. See the License for the specific language governing permissions # and limitations under the License. """ IDEA Development Task Utils Should not contain any idea-sdk dependencies! """ import os import shutil import subprocess import sys import textwrap from pathlib import Path from typing import Dict, List, Optional, Set import yaml from invoke import Context from questionary import unsafe_prompt from rich.console import Console from rich.style import Style as RichStyle IDEA_DEVELOPMENT_ERROR = 'IDEA_DEVELOPMENT_ERROR' ERROR_CODE_VENV_NOT_SETUP = 'ERROR_CODE_VENV_NOT_SETUP' INVALID_PYTHON_VERSION = 'INVALID_PYTHON_VERSION' INVALID_PARAMS = 'INVALID_PARAMS' BUILD_FAILED = 'BUILD_FAILED' GENERAL_EXCEPTION = 'GENERAL_EXCEPTION' DEVELOPER_ONBOARDING_NOTE = f'Please go through the Developer On-boarding docs before contributing to IDEA source code.' class SocaDevelopmentProps: def __init__(self): self.software_versions: Optional[Dict] = None self.load_software_versions() @staticmethod def _run_cmd(cmd, shell=True) -> str: result = subprocess.run( args=cmd, shell=shell, capture_output=True, text=True ) if result.returncode == 0: return str(result.stdout) else: return str(result.stdout) @property def software_versions_file(self) -> str: return os.path.join(self.project_root_dir, 'software_versions.yml') def load_software_versions(self): """ load software_versions.yml from project root """ with open(self.software_versions_file, 'r') as f: self.software_versions = yaml.safe_load(f.read()) @property def idea_release_version(self) -> str: with open(os.path.join(self.project_root_dir, 'RES_VERSION.txt')) as f: return f.read().strip() @property def idea_python_version(self) -> str: return self.software_versions['python_version'] @property def idea_node_version(self) -> str: return self.software_versions['node_version'] @property def idea_nvm_version(self) -> str: return self.software_versions['nvm_version'] @property def idea_cdk_version(self) -> str: cdk_version = self.software_versions['aws_cdk_version'] return cdk_version.strip() @property def project_root_dir(self) -> str: path = Path(os.path.dirname(os.path.realpath(__file__))) return str(path.parent.absolute()) @property def project_source_dir(self) -> str: return os.path.join(self.project_root_dir, 'source', 'idea') @property def project_unit_tests_dir(self) -> str: return os.path.join(self.project_root_dir, 'source', 'tests', 'unit') @property def project_deployment_dir(self) -> str: return os.path.join(self.project_root_dir, 'deployment') @property def project_scripts_dir(self) -> str: return os.path.join(self.project_root_dir, 'source', 'scripts') @property def which_python(self) -> str: return shutil.which('python') @property def python_version(self) -> str: python = self.which_python result = self._run_cmd(f'{python} --version') return result.split(' ')[1] @property def site_packages(self) -> str: virtual_env = os.environ.get('VIRTUAL_ENV', None) if virtual_env is None: python_bin = Path(self.which_python) virtual_env = python_bin.parent.parent return os.path.join(virtual_env, 'lib', 'python3.9', 'site-packages') @property def requirements_dir(self) -> str: return os.path.join(self.project_root_dir, 'requirements') @property def bootstrap_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-bootstrap') @property def backend_dir(self) -> str: return os.path.join(self.project_source_dir, 'backend') @property def global_settings_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-administrator','resources', 'config', 'templates', 'global-settings') @property def administrator_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-administrator') @property def administrator_integ_tests_dir(self) -> str: return os.path.join(self.administrator_project_dir, 'src', 'ideaadministrator', 'integration_tests') @property def end_to_end_integ_tests_dir(self) -> str: return os.path.join(self.project_root_dir, 'source', 'tests', 'integration', 'tests') @property def deployment_ecr_dir(self) -> str: return os.path.join(self.project_deployment_dir, 'ecr') @property def deployment_administrator_dir(self) -> str: return os.path.join(self.deployment_ecr_dir, 'idea-administrator') @property def deployment_ad_sync_dir(self) -> str: return os.path.join(self.deployment_ecr_dir, 'ad-sync') @property def administrator_webapp_dir(self) -> str: return os.path.join(self.administrator_project_dir, 'webapp') @property def administrator_src(self) -> str: return os.path.join(self.administrator_project_dir, 'src') @property def administrator_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'idea-administrator') @property def lambda_functions_src(self) -> str: return os.path.join(self.administrator_project_dir, 'resources') @property def lambda_functions_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'lambda_functions') @property def pipeline_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'pipeline') @property def pipeline_src(self) -> str: return os.path.join(self.pipeline_project_dir) @property def pipeline_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'pipeline') @property def infrastructure_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'infrastructure') @property def infrastructure_src(self) -> str: return os.path.join(self.infrastructure_project_dir) @property def infrastructure_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'infrastructure') @property def library_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'library') @property def library_src(self) -> str: return os.path.join(self.library_project_dir, 'src') @property def library_tests_src(self) -> str: return os.path.join(self.library_project_dir, 'tests') @property def virtual_desktop_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-virtual-desktop-controller') @property def virtual_desktop_src(self) -> str: return os.path.join(self.virtual_desktop_project_dir, 'src') @property def virtual_desktop_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'idea-virtual-desktop-controller') @property def cluster_manager_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-cluster-manager') @property def dcv_connection_gateway_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-dcv-connection-gateway') @property def cluster_manager_webapp_dir(self) -> str: return os.path.join(self.cluster_manager_project_dir, 'webapp') @property def cluster_manager_src(self) -> str: return os.path.join(self.cluster_manager_project_dir, 'src') @property def cluster_manager_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'idea-cluster-manager') @property def data_model_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-data-model') @property def sdk_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-sdk') @property def ad_sync_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'ad-sync') @property def test_utils_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-test-utils') @property def test_utils_src(self) -> str: return os.path.join(self.test_utils_project_dir, 'src') @property def data_model_src(self) -> str: return os.path.join(self.data_model_project_dir, 'src') @property def ad_sync_src(self) -> str: return os.path.join(self.ad_sync_project_dir, 'src') @property def ad_sync_integ_tests_src(self) -> str: return os.path.join(self.ad_sync_project_dir, 'tests', 'integration') @property def sdk_src(self) -> str: return os.path.join(self.sdk_project_dir, 'src') @property def sdk_tests_src(self) -> str: return os.path.join(self.project_unit_tests_dir, 'idea-sdk') @property def scheduler_project_dir(self) -> str: return os.path.join(self.project_source_dir, 'idea-scheduler') @property def scheduler_src(self) -> str: return os.path.join(self.scheduler_project_dir, 'src') @property def scheduler_tests_src(self) -> str: return os.path.join(self.scheduler_project_dir, 'tests') @property def project_build_dir(self) -> str: return os.path.join(self.project_root_dir, 'build') @property def project_dist_dir(self) -> str: return os.path.join(self.project_root_dir, 'dist') @property def idea_downloads_dir(self) -> str: downloads_dir = os.path.join(self.idea_user_home, 'downloads', 'idea') os.makedirs(downloads_dir, exist_ok=True) return downloads_dir @property def idea_user_home(self) -> str: idea_user_home = os.environ.get('IDEA_USER_HOME', os.path.expanduser(os.path.join('~', '.idea'))) os.makedirs(idea_user_home, exist_ok=True) return idea_user_home @property def idea_cdk_dir(self) -> str: idea_user_home = self.idea_user_home idea_cdk_dir = os.path.join(idea_user_home, 'lib', 'idea-cdk') os.makedirs(idea_cdk_dir, exist_ok=True) return idea_cdk_dir @property def idea_user_config_file(self) -> str: return os.path.join(self.idea_user_home, 'config.yml') class SocaDevelopmentConsole: def __init__(self): self.console = Console() def error(self, msg): self.console.print(msg, style='bold red') def echo(self, msg): self.console.out(msg) def info(self, msg): self.console.print(msg, style='cyan') def warning(self, msg): self.console.print(msg, style='bold yellow') def success(self, msg): self.console.print(msg, style='bold green') def print(self, msg): self.console.print(msg) def spinner(self, message: str): return self.console.status(message) @staticmethod def confirm(message: str, default=False, auto_enter=True, icon='?') -> bool: try: result = unsafe_prompt(questions=[{ 'type': 'confirm', 'name': 'result', 'message': message, 'default': default, 'auto_enter': auto_enter, 'qmark': icon }]) if 'result' in result: return result['result'] return False except KeyboardInterrupt: return False @staticmethod def ask(questions: List[Dict]) -> Dict: try: return unsafe_prompt(questions=questions) except KeyboardInterrupt: return {} def print_header_block(self, content: str, width: int = 120, break_long_words: bool = False, style=None): if style == 'main': header_char = '-' style = RichStyle(bold=True, color='bright_white') elif style == 'success': header_char = '-' style = 'bold green' elif style == 'error': header_char = '-' style = 'bold red' else: header_char = '-' style = None self.console.print(header_char * width, style=style) lines = textwrap.wrap(f'* {content}', width, break_on_hyphens=False, break_long_words=break_long_words, subsequent_indent=' ') for line in lines: self.console.print(line, style=style) self.console.print(header_char * width, style=style) class SocaDevelopmentException(Exception): def __init__(self, message: str, error_code: str = 'IDEA_DEVELOPMENT_ERROR', ref=None): self.error_code = error_code self.message = message self.ref = ref def __repr__(self): return str(self) def __str__(self): return f'[{self.error_code}] {self.message}' class SocaDevelopmentExceptions: @staticmethod def virtual_env_not_setup(): return SocaDevelopmentException( error_code=IDEA_DEVELOPMENT_ERROR, message=f'VirtualEnv is not setup. Please setup a python virtual environment before proceeding. {os.linesep}' f'{DEVELOPER_ONBOARDING_NOTE}' ) @staticmethod def invalid_python_version(): return SocaDevelopmentException( error_code=INVALID_PYTHON_VERSION, message=f'You are not running a valid Python version required by IDEA. {os.linesep}' f'You must have Python {props.idea_python_version} installed for IDEA development.{os.linesep}' f'{DEVELOPER_ONBOARDING_NOTE}' ) @staticmethod def invalid_params(message: str): return SocaDevelopmentException( error_code=INVALID_PARAMS, message=message ) @staticmethod def build_failed(message: str): return SocaDevelopmentException( error_code=BUILD_FAILED, message=message ) @staticmethod def general_exception(message: str): return SocaDevelopmentException( error_code=GENERAL_EXCEPTION, message=message ) @staticmethod def exception(error_code: str, message: str): return SocaDevelopmentException(error_code, message) props = SocaDevelopmentProps() class SocaDevelopmentUtils: @staticmethod def get_base_prefix_compat() -> str: """ Get base/real prefix, or sys.prefix if there is none. Sourced From: https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv """ return getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix def in_virtualenv(self) -> bool: return self.get_base_prefix_compat() != sys.prefix def check_venv(self): if self.in_virtualenv: return raise exceptions.virtual_env_not_setup() @staticmethod def check_python_version(): python_version = props.python_version if python_version.endswith(props.idea_python_version): return raise exceptions.invalid_python_version() @staticmethod def update_source_paths(paths: List[str]): for path in paths: sys.path.insert(0, path) @property def idea_python(self) -> str: self.check_venv() return props.which_python def get_package_meta(self, c: Context, source_root: str, prop: str) -> str: with c.cd(source_root): result = c.run(f'{self.idea_python} setup.py --{prop}', hide=True) return str(result.stdout).strip() @staticmethod def get_supported_modules() -> Optional[Set[str]]: return { 'cluster-manager', 'virtual-desktop-controller' } @staticmethod def get_module_name(token: str) -> Optional[str]: if token in ('virtual-desktop', 'virtual-desktop-controller', 'vdc', 'vdi'): return 'virtual-desktop-controller' if token in ('cluster-manager', 'cm'): return 'cluster-manager' if token in ('admin', 'administrator'): return 'administrator' return None console = SocaDevelopmentConsole() exceptions = SocaDevelopmentExceptions() utils = SocaDevelopmentUtils()