diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..256a537 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '29 8 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 40139cb..c209665 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,13 +22,4 @@ jobs: run: poetry install -n -v - name: Lint with flakeheaven - run: poetry run flakeheaven lint - - - name: Lint with black - run: poetry run black . --check - - - name: Lint with isort - run: poetry run isort . --check - - - name: Lint with mypy - run: poetry run mypy . + run: poetry run pre-commit run --all-files diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 11d0792..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Testing - -on: [push] - -jobs: - testing: - runs-on: ubuntu-latest - - steps: - - name: Checkout master - uses: actions/checkout@master - - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Setup Poetry - uses: Gr1N/setup-poetry@v4 - - - name: Install dev packages - run: poetry install -n -v - - - name: Run Pytest - run: poetry run pytest -vv diff --git a/.gitignore b/.gitignore index be29c14..b7dfa43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6b11b18 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +exclude: "^docs/gitbook/" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.3.0 # Use the ref you want to point at + hooks: + - id: trailing-whitespace + - id: check-ast + - id: check-case-conflict + - id: debug-statements + - id: check-yaml + + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) + + - repo: https://github.com/ambv/black + rev: 21.12b0 + hooks: + - id: black + language_version: python3.8 + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.7.0 + hooks: + - id: python-use-type-annotations + - id: python-no-eval + - id: python-no-log-warn + + - repo: local + hooks: + - id: pytest + name: pytest + entry: poetry run pytest tests + language: system + pass_filenames: false + # alternatively you could `types: [python]` so it only runs when python files change + # though tests might be invalidated if you were to say change a data file + always_run: true + + - id: flakeheaven + name: flakeheaven + entry: poetry run flakeheaven lint + language: system + pass_filenames: false + + - id: mypy + name: mypy + entry: poetry run mypy . + language: system + pass_filenames: false + diff --git a/CHANGELOG.md b/CHANGELOG.md index 69bde92..f42c6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.0] - 2022-06-02 +### Added +- thread_workers argument + +### Fixed +- Memory leak when running in large organisations: botocove now allows + completed Session objects to be garbage collected + ## [1.4.1] - 2022-15-01 ### Added - Support for Policy and PolicyArn restriction on assumed roles diff --git a/README.md b/README.md index fe777d7..63f4ff8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ account. - Easy - Dolphin Themed đŸŦ -A simple decorator for functions to remove time and complexity burden. Uses +A simple decorator for functions to remove time and complexity burden. Uses `ThreadPoolExecutor` to run boto3 sessions against one to all of your AWS accounts at (nearly!) the same speed as running against one. @@ -79,13 +79,13 @@ def get_iam_users(session): def main(): # No session passed as the decorator injects it - all_results = get_iam_users() + all_results = get_iam_users() # Now returns a Dict with keys Results, Exceptions and FailedAssumeRole - + # A list of dictionaries for each account, with account details included. # Each account's get_iam_users return is in a "Result" key. - print(all_results["Results"]) - + print(all_results["Results"]) + # A list of dictionaries for each account that raised an exception print(all_results["Exceptions"]) @@ -96,7 +96,7 @@ def main(): ## Arguments ### Cove -`@cove()`: +`@cove()`: Uses boto3 credential chain to get every AWS account within the organization, assume the `OrganizationAccountAccessRole` in it and run the @@ -119,12 +119,12 @@ be ignored. `rolename`: Optional[str] -An IAM role name that will be attempted to assume in all target accounts. +An IAM role name that will be attempted to assume in all target accounts. Defaults to the AWS Organization default, `OrganizationAccountAccessRole`. `role_session_name`: Optional[str] -An IAM role session name that will be passed to each Cove session's `sts.assume_role()` call. +An IAM role session name that will be passed to each Cove session's `sts.assume_role()` call. Defaults to the name of the role being used if unset. `policy`: Optional[str] @@ -154,12 +154,20 @@ It is vital to run interruptible, idempotent code with this argument as `True`. Defaults to True. When True, will leverage the Boto3 Organizations API to list all accounts in the organization, and enrich each `CoveSession` with information -available (`Id`, `Arn`, `Name`). +available (`Id`, `Arn`, `Name`, `Status`, `Email`). Disabling this and providing your +own full list of accounts may be a desirable optimisation if speed is an issue. `org_master=False` means `target_ids` must be provided (as no list of accounts can be created for you), as well as likely `rolename`. Only `Id` will be available to `CoveSession`. +`thread_workers`: int + +Defaults to 20. Cove utilises a ThreadPoolWorker under the hood, which can be tuned +with this argument. Number of thread workers directly corrolates to memory usage: see +[here](#is-botocove-thread-safe) + + ### CoveSession Cove supplies an enriched Boto3 session to each function called. Account details @@ -180,7 +188,7 @@ def do_nothing(session: CoveSession): Wrapped functions return a dictionary. Each value contains List[Dict[str, Any]]: ``` { - "Results": results: + "Results": results: "Exceptions": exceptions, "FailedAssumeRole": invalid_sessions, } @@ -195,13 +203,35 @@ An example of cove_output["Results"]: 'Status': 'ACTIVE', 'AssumeRoleSuccess': True, 'Result': wrapped_function_return_value # Result of wrapped func - } -] + } +] ``` +### Is botocove thread safe? + +botocove is thread safe, but number of threaded executions will be bound by memory, +network IO and AWS api rate limiting. Defaulting to 20 thread workers is a reasonable +starting point, but can be further optimised for runtime with experimentation. + +botocove has no constraint or understanding of the function it's wrapping: it is +recommended to avoid shared state for botocove wrapped functions, and to write simple +functions that are written to be idempotent and independent. + +[Boto3 Session objects are not natively thread safe and should not be shared across threads](https://boto3.amazonaws.com/v1/documentation/api/1.14.31/guide/session.html#multithreading-or-multiprocessing-with-sessions). +However, botocove is instantiating a new Session object per thread/account and running +decorated functions inside their own closure. A shared client is used from the host account +that botocove is run from (eg, an organization master account) - +[clients are threadsafe](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/clients.html#multithreading-or-multiprocessing-with-clients) and allow this. + +boto3 sessions have a significant memory footprint: +Version 1.5.0 of botocove was re-written to ensure that boto3 sessions are released +after completion which resolved memory starvation issues. This was discussed here: +https://github.com/connelldave/botocove/issues/20 and a relevant boto3 issue is here: +https://github.com/boto/boto3/issues/1670 + ### botocove? It turns out that the Amazon's Boto dolphins are solitary or small-group animals, -unlike the large pods of dolphins in the oceans. This killed my "large group of +unlike the large pods of dolphins in the oceans. This killed my "large group of boto" idea, so the next best idea was where might they all shelter together... a cove! diff --git a/botocove/cove_decorator.py b/botocove/cove_decorator.py index f8e668e..39554ee 100644 --- a/botocove/cove_decorator.py +++ b/botocove/cove_decorator.py @@ -5,8 +5,8 @@ from boto3.session import Session +from botocove.cove_host_account import CoveHostAccount from botocove.cove_runner import CoveRunner -from botocove.cove_sessions import CoveSessions from botocove.cove_types import CoveOutput, CoveSessionInformation, R logger = logging.getLogger(__name__) @@ -14,7 +14,7 @@ def dataclass_converter(d: CoveSessionInformation) -> Dict[str, Any]: """Unpack dataclass into dict and remove None values""" - return {k: v for k, v in asdict(d).items() if v} + return {k: v for k, v in asdict(d).items() if v is not None} def cove( @@ -29,11 +29,13 @@ def cove( assuming_session: Optional[Session] = None, raise_exception: bool = False, org_master: bool = True, + thread_workers: int = 20 ) -> Callable: def decorator(func: Callable[..., R]) -> Callable[..., CoveOutput]: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> CoveOutput: - valid_sessions, invalid_sessions = CoveSessions( + + host_account = CoveHostAccount( target_ids=target_ids, ignore_ids=ignore_ids, rolename=rolename, @@ -42,23 +44,32 @@ def wrapper(*args: Any, **kwargs: Any) -> CoveOutput: policy_arns=policy_arns, org_master=org_master, assuming_session=assuming_session, - ).get_cove_sessions() + ) runner = CoveRunner( - valid_sessions=valid_sessions, + host_account=host_account, func=func, raise_exception=raise_exception, func_args=args, func_kwargs=kwargs, + thread_workers=thread_workers, ) output = runner.run_cove_function() # Rewrite dataclasses into untyped dicts to retain current functionality return CoveOutput( - FailedAssumeRole=[dataclass_converter(f) for f in invalid_sessions], Results=[dataclass_converter(r) for r in output["Results"]], - Exceptions=[dataclass_converter(e) for e in output["Exceptions"]], + Exceptions=[ + dataclass_converter(e) + for e in output["Exceptions"] + if e.AssumeRoleSuccess + ], + FailedAssumeRole=[ + dataclass_converter(f) + for f in output["Exceptions"] + if not f.AssumeRoleSuccess + ], ) return wrapper diff --git a/botocove/cove_sessions.py b/botocove/cove_host_account.py similarity index 53% rename from botocove/cove_sessions.py rename to botocove/cove_host_account.py index 17e8a74..1fb57c0 100644 --- a/botocove/cove_sessions.py +++ b/botocove/cove_host_account.py @@ -1,18 +1,13 @@ import logging -from concurrent import futures -from typing import Any, List, Literal, Optional, Set, Tuple, Union +from typing import Any, List, Literal, Optional, Set, Union import boto3 from boto3.session import Session from botocore.config import Config -from botocore.exceptions import ClientError from mypy_boto3_organizations.client import OrganizationsClient -from mypy_boto3_organizations.type_defs import AccountTypeDef from mypy_boto3_sts.client import STSClient -from tqdm import tqdm -from botocove.cove_session import CoveSession -from botocove.cove_types import CoveResults, CoveSessionInformation +from botocove.cove_types import CoveSessionInformation logger = logging.getLogger(__name__) @@ -20,7 +15,7 @@ DEFAULT_ROLENAME = "OrganizationAccountAccessRole" -class CoveSessions(object): +class CoveHostAccount(object): def __init__( self, target_ids: Optional[List[str]], @@ -50,97 +45,26 @@ def __init__( self.org_master = org_master - def get_cove_sessions(self) -> Tuple[List[CoveSession], CoveResults]: + def get_cove_session_info(self) -> List[CoveSessionInformation]: logger.info( - f"Getting sessions in accounts: {self.role_to_assume=} " + f"Getting session information: {self.role_to_assume=} " f"{self.role_session_name=} {self.target_accounts=} " f"{self.provided_ignore_ids=}" ) logger.info(f"Session policy: {self.policy_arns=} {self.policy=}") - with futures.ThreadPoolExecutor(max_workers=20) as executor: - sessions = list( - tqdm( - executor.map(self._cove_session_factory, self.target_accounts), - total=len(self.target_accounts), - desc="Assuming sessions", - colour="#39ff14", # neon green - ) + sessions = [] + for account_id in self.target_accounts: + account_details: CoveSessionInformation = CoveSessionInformation( + Id=account_id, + RoleName=self.role_to_assume, + RoleSessionName=self.role_session_name, + Policy=self.policy, + PolicyArns=self.policy_arns, ) + sessions.append(account_details) - self.valid_sessions = [ - session for session in sessions if session.assume_role_success is True - ] - if not self.valid_sessions: - raise ValueError("No accounts are accessible: check logs for detail") - - self.invalid_sessions = self._get_invalid_cove_sessions(sessions) - return self.valid_sessions, self.invalid_sessions - - def _cove_session_factory(self, account_id: str) -> CoveSession: - role_arn = f"arn:aws:iam::{account_id}:role/{self.role_to_assume}" - account_details: CoveSessionInformation = CoveSessionInformation( - Id=account_id, - RoleSessionName=self.role_session_name, - Policy=self.policy, - PolicyArns=self.policy_arns, - ) - - if self.org_master: - try: - account_description: AccountTypeDef = self.org_client.describe_account( - AccountId=account_id - )["Account"] - account_details.Arn = account_description["Arn"] - account_details.Email = account_description["Email"] - account_details.Name = account_description["Name"] - account_details.Status = account_description["Status"] - except ClientError: - logger.exception(f"Failed to call describe_account for {account_id}") - - cove_session = CoveSession(account_details) - - try: - logger.debug(f"Attempting to assume {role_arn}") - # This calling style avoids a ParamValidationError from botocore. - # Passing None is not allowed for the optional parameters. - - assume_role_args = { - k: v - for k, v in [ - ("RoleArn", role_arn), - ("RoleSessionName", self.role_session_name), - ("Policy", self.policy), - ("PolicyArns", self.policy_arns), - ] - if v is not None - } - creds = self.sts_client.assume_role(**assume_role_args)["Credentials"] # type: ignore[arg-type] # noqa E501 - cove_session.initialize_boto_session( - aws_access_key_id=creds["AccessKeyId"], - aws_secret_access_key=creds["SecretAccessKey"], - aws_session_token=creds["SessionToken"], - ) - except ClientError as e: - cove_session.store_exception(e) - - return cove_session - - def _get_invalid_cove_sessions(self, sessions: List[CoveSession]) -> CoveResults: - invalid_sessions = [ - session.format_cove_error() - for session in sessions - if session.assume_role_success is False - ] - - if invalid_sessions: - logger.warning("Could not assume role into these accounts:") - for invalid_session in invalid_sessions: - logger.warning(invalid_session) - invalid_ids = [failure.Id for failure in invalid_sessions] - logger.warning(f"\n\nInvalid session Account IDs as list: {invalid_ids}") - - return invalid_sessions + return sessions def _get_boto3_client( self, diff --git a/botocove/cove_runner.py b/botocove/cove_runner.py index b352510..adeffde 100644 --- a/botocove/cove_runner.py +++ b/botocove/cove_runner.py @@ -1,9 +1,10 @@ import logging from concurrent import futures -from typing import Any, Callable, List, Tuple +from typing import Any, Callable from tqdm import tqdm +from botocove.cove_host_account import CoveHostAccount from botocove.cove_session import CoveSession from botocove.cove_types import ( CoveFunctionOutput, @@ -18,60 +19,67 @@ class CoveRunner(object): def __init__( self, - valid_sessions: List[CoveSession], + host_account: CoveHostAccount, func: Callable[..., R], raise_exception: bool, func_args: Any, func_kwargs: Any, + thread_workers: int, ) -> None: - self.sessions = valid_sessions - self.raise_exception = raise_exception + + self.host_account = host_account + self.sessions = host_account.get_cove_session_info() + self.cove_wrapped_func = func + self.raise_exception = raise_exception self.func_args = func_args self.func_kwargs = func_kwargs + self.thread_workers = thread_workers + def run_cove_function(self) -> CoveFunctionOutput: # Run decorated func with all valid sessions - results, exceptions = self._async_boto3_call() + with futures.ThreadPoolExecutor(max_workers=self.thread_workers) as executor: + completed: CoveResults = list( + tqdm( + executor.map(self.cove_thread, self.sessions), + total=len(self.sessions), + desc="Executing function", + colour="#ff69b4", # hotpink + ) + ) + successful_results = [ + result for result in completed if not result.ExceptionDetails + ] + exceptions = [result for result in completed if result.ExceptionDetails] + return CoveFunctionOutput( - Results=results, + Results=successful_results, Exceptions=exceptions, ) - def cove_exception_wrapper_func( + def cove_thread( self, - account_session: CoveSession, + account_session_info: CoveSessionInformation, ) -> CoveSessionInformation: - # Wrapper capturing exceptions and formatting results + cove_session = CoveSession( + account_session_info, + sts_client=self.host_account.sts_client, + org_client=self.host_account.org_client, + org_master=self.host_account.org_master, + ) try: + cove_session.activate_cove_session() + result = self.cove_wrapped_func( - account_session, *self.func_args, **self.func_kwargs + cove_session, *self.func_args, **self.func_kwargs ) - return account_session.format_cove_result(result) + + return cove_session.format_cove_result(result) + except Exception as e: if self.raise_exception is True: - account_session.store_exception(e) - logger.exception(account_session.format_cove_error()) + logger.exception(cove_session.format_cove_error(e)) raise else: - account_session.store_exception(e) - return account_session.format_cove_error() - - def _async_boto3_call( - self, - ) -> Tuple[CoveResults, CoveResults]: - with futures.ThreadPoolExecutor(max_workers=20) as executor: - completed: CoveResults = list( - tqdm( - executor.map(self.cove_exception_wrapper_func, self.sessions), - total=len(self.sessions), - desc="Executing function", - colour="#ff69b4", # hotpink - ) - ) - - successful_results = [ - result for result in completed if not result.ExceptionDetails - ] - exceptions = [result for result in completed if result.ExceptionDetails] - return successful_results, exceptions + return cove_session.format_cove_error(e) diff --git a/botocove/cove_session.py b/botocove/cove_session.py index 82a54e2..cbbbb26 100644 --- a/botocove/cove_session.py +++ b/botocove/cove_session.py @@ -1,9 +1,16 @@ +import logging from typing import Any from boto3.session import Session +from botocore.exceptions import ClientError +from mypy_boto3_organizations.client import OrganizationsClient +from mypy_boto3_organizations.type_defs import AccountTypeDef +from mypy_boto3_sts.client import STSClient from botocove.cove_types import CoveSessionInformation, R +logger = logging.getLogger(__name__) + class CoveSession(Session): """Enriches a boto3 Session with account data from Master account if run from @@ -15,27 +22,81 @@ class CoveSession(Session): session_information: CoveSessionInformation stored_exception: Exception - def __init__(self, session_info: CoveSessionInformation) -> None: + def __init__( + self, + session_info: CoveSessionInformation, + org_client: OrganizationsClient, + sts_client: STSClient, + org_master: bool, + ) -> None: self.session_information = session_info + self.org_master = org_master + self.org_client = org_client + self.sts_client = sts_client def __repr__(self) -> str: # Overwrite boto3's repr to avoid AttributeErrors return f"{self.__class__.__name__}(account_id={self.session_information.Id})" + def activate_cove_session(self) -> "CoveSession": + role_arn = ( + f"arn:aws:iam::{self.session_information.Id}:role/" + f"{self.session_information.RoleName}" + ) + + if self.org_master: + try: + account_description: AccountTypeDef = self.org_client.describe_account( + AccountId=self.session_information.Id + )["Account"] + self.session_information.Arn = account_description["Arn"] + self.session_information.Email = account_description["Email"] + self.session_information.Name = account_description["Name"] + self.session_information.Status = account_description["Status"] + except ClientError: + logger.exception( + f"Failed to call describe_account for {self.session_information.Id}" + ) + + try: + logger.debug(f"Attempting to assume {role_arn}") + + # This calling style avoids a ParamValidationError from botocore. + # Passing None is not allowed for the optional parameters. + assume_role_args = { + k: v + for k, v in [ + ("RoleArn", role_arn), + ("RoleSessionName", self.session_information.RoleSessionName), + ("Policy", self.session_information.Policy), + ("PolicyArns", self.session_information.PolicyArns), + ] + if v is not None + } + creds = self.sts_client.assume_role(**assume_role_args)["Credentials"] # type: ignore[arg-type] # noqa E501 + self.initialize_boto_session( + aws_access_key_id=creds["AccessKeyId"], + aws_secret_access_key=creds["SecretAccessKey"], + aws_session_token=creds["SessionToken"], + ) + self.session_information.AssumeRoleSuccess = True + except ClientError: + logger.error( + f"Failed to initalize cove session for " + f"account {self.session_information.Id}" + ) + raise + + return self + def initialize_boto_session(self, *args: Any, **kwargs: Any) -> None: # Inherit from and initialize standard boto3 Session object super().__init__(*args, **kwargs) - self.assume_role_success = True - self.session_information.AssumeRoleSuccess = self.assume_role_success - - def store_exception(self, err: Exception) -> None: - self.stored_exception = err def format_cove_result(self, result: R) -> CoveSessionInformation: self.session_information.Result = result return self.session_information - def format_cove_error(self) -> CoveSessionInformation: - self.session_information.ExceptionDetails = self.stored_exception - self.session_information.AssumeRoleSuccess = self.assume_role_success + def format_cove_error(self, err: Exception) -> CoveSessionInformation: + self.session_information.ExceptionDetails = err return self.session_information diff --git a/botocove/cove_types.py b/botocove/cove_types.py index 7a111a1..3e593e9 100644 --- a/botocove/cove_types.py +++ b/botocove/cove_types.py @@ -9,11 +9,12 @@ @dataclass class CoveSessionInformation(Generic[R]): Id: str + RoleName: str + AssumeRoleSuccess: bool = False Arn: Optional[str] = None Email: Optional[str] = None Name: Optional[str] = None Status: Optional[AccountStatusType] = None - AssumeRoleSuccess: Optional[bool] = None RoleSessionName: Optional[str] = None Policy: Optional[str] = None PolicyArns: Optional[List[str]] = None diff --git a/poetry.lock b/poetry.lock index 09f1820..058f2dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -62,14 +62,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.20.38" +version = "1.20.41" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.23.38,<1.24.0" +botocore = ">=1.23.41,<1.24.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -78,8 +78,8 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.20.38" -description = "Type annotations for boto3 1.20.38, generated by mypy-boto3-builder 6.3.2" +version = "1.20.40" +description = "Type annotations for boto3 1.20.40, generated by mypy-boto3-builder 6.3.2" category = "main" optional = false python-versions = ">=3.6" @@ -395,7 +395,7 @@ xray = ["mypy-boto3-xray (>=1.20.0)"] [[package]] name = "botocore" -version = "1.23.38" +version = "1.23.41" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -411,8 +411,8 @@ crt = ["awscrt (==0.12.5)"] [[package]] name = "botocore-stubs" -version = "1.23.38" -description = "Type annotations for botocore 1.23.38, generated by mypy-boto3-builder 6.3.2" +version = "1.23.40" +description = "Type annotations for botocore 1.23.40, generated by mypy-boto3-builder 6.3.2" category = "main" optional = false python-versions = ">=3.6" @@ -420,6 +420,14 @@ python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.9\""} +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + [[package]] name = "click" version = "8.0.3" @@ -439,11 +447,19 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "distlib" +version = "0.3.4" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "entrypoints" version = "0.3" description = "Discover and load entry points from installed packages." -category = "main" +category = "dev" optional = false python-versions = ">=2.7" @@ -455,11 +471,23 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "filelock" +version = "3.4.2" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + [[package]] name = "flake8" version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -609,7 +637,7 @@ flake8-plugin-utils = ">=1.3.2,<2.0.0" name = "flakeheaven" version = "0.11.0" description = "Flake8 wrapper to make it nice and configurable" -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -647,6 +675,17 @@ python-versions = ">=3.7" [package.dependencies] gitdb = ">=4.0.1,<5" +[[package]] +name = "identify" +version = "2.4.4" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.extras] +license = ["ukkonen"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -681,7 +720,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -732,6 +771,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packaging" version = "21.3" @@ -795,6 +842,22 @@ python-versions = ">=3.6" dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "2.17.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + [[package]] name = "py" version = "1.11.0" @@ -807,7 +870,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "pycodestyle" version = "2.8.0" description = "Python style guide checker" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -815,7 +878,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "pyflakes" version = "2.4.0" description = "passive checker of Python programs" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -823,13 +886,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pygments" version = "2.11.2" description = "Pygments is a syntax highlighting package written in Python." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false @@ -950,7 +1013,7 @@ test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybi name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -999,10 +1062,28 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "virtualenv" +version = "20.13.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "76028fd434fdd3aa5eb4c5895765f5f27ec701489e834125edcc99452d5a27e4" +content-hash = "77f63aae84d92ce55be9aab5e019f8fc53cf3945c923f645470356780c02a9bd" [metadata.files] atomicwrites = [ @@ -1022,20 +1103,24 @@ black = [ {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] boto3 = [ - {file = "boto3-1.20.38-py3-none-any.whl", hash = "sha256:22b243302f526df9c599c6b81092cb3c62f785bc06cedceeff9054489df4ffb3"}, - {file = "boto3-1.20.38.tar.gz", hash = "sha256:edeae6d38c98691cb9da187c541f3033e0f30d6b2a0b54b5399a44d9b3ba4f61"}, + {file = "boto3-1.20.41-py3-none-any.whl", hash = "sha256:aaddf6cf93568b734ad62fd96991775bccc7f016e93ff4e98dc1aa4f7586440c"}, + {file = "boto3-1.20.41.tar.gz", hash = "sha256:fb02467a6e8109c7db994ba77fa2e8381ed129ce312988d8ef23edf6e3a3c7f1"}, ] boto3-stubs = [ - {file = "boto3-stubs-1.20.38.tar.gz", hash = "sha256:123f56892453ce268d7bfe43b1280241e51d66a18c502e00dafa58ab5784f1bc"}, - {file = "boto3_stubs-1.20.38-py3-none-any.whl", hash = "sha256:937cfbe7e3685b0bc6ab1bd1853c72143154af68e700d3920f40caec145be2f4"}, + {file = "boto3-stubs-1.20.40.tar.gz", hash = "sha256:24f23e14de15d29a85e301b5beb144d2c778ed350e0c08a2136a978c8105e3c9"}, + {file = "boto3_stubs-1.20.40-py3-none-any.whl", hash = "sha256:2e940afd4a47688bb536155b10bdc65cc99390217bfcb392f4fc8c188646a65f"}, ] botocore = [ - {file = "botocore-1.23.38-py3-none-any.whl", hash = "sha256:49b304d9d4a782d7108f6a5ca0df6557da20a22b74d5bf745f02fea5cffc35ca"}, - {file = "botocore-1.23.38.tar.gz", hash = "sha256:f733bc565f144f0ec97ffe0d51235d358ad2f5f12b331563b69d9e9227262a36"}, + {file = "botocore-1.23.41-py3-none-any.whl", hash = "sha256:41104e1c976c9c410387b3c7d265466b314f287a1c13fd4b543768135301058a"}, + {file = "botocore-1.23.41.tar.gz", hash = "sha256:9137c59c4eb1dee60ae3c710e94f56119a1b33b0b17ff3ad878fc2f4ce77843a"}, ] botocore-stubs = [ - {file = "botocore-stubs-1.23.38.tar.gz", hash = "sha256:ad954929705c0496df58d46ec0b23d2c53dd8700288ba84d49116c45450a9f5b"}, - {file = "botocore_stubs-1.23.38-py3-none-any.whl", hash = "sha256:5b587016bacd4bb82207b8953e9f2b0b5cf811b350557e0d8760aeaaed86beef"}, + {file = "botocore-stubs-1.23.40.tar.gz", hash = "sha256:48529a2b7e14c6e3dd4544c21d4cf342ad512e2a526f5262c565357683d78787"}, + {file = "botocore_stubs-1.23.40-py3-none-any.whl", hash = "sha256:b5762895175cbacfa989b7ff313ca20f30f82137fcfd8a389cfe4a920cb57e73"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] click = [ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, @@ -1045,6 +1130,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +distlib = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, @@ -1052,6 +1141,10 @@ entrypoints = [ eradicate = [ {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"}, ] +filelock = [ + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, @@ -1111,6 +1204,10 @@ gitpython = [ {file = "GitPython-3.1.26-py3-none-any.whl", hash = "sha256:26ac35c212d1f7b16036361ca5cff3ec66e11753a0d677fb6c48fa4e1a9dd8d6"}, {file = "GitPython-3.1.26.tar.gz", hash = "sha256:fc8868f63a2e6d268fb25f481995ba185a85a66fcad126f039323ff6635669ee"}, ] +identify = [ + {file = "identify-2.4.4-py2.py3-none-any.whl", hash = "sha256:aa68609c7454dbcaae60a01ff6b8df1de9b39fe6e50b1f6107ec81dcda624aa6"}, + {file = "identify-2.4.4.tar.gz", hash = "sha256:6b4b5031f69c48bf93a646b90de9b381c6b5f560df4cbe0ed3cf7650ae741e4d"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1161,6 +1258,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1185,6 +1286,10 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] +pre-commit = [ + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1202,8 +1307,8 @@ pygments = [ {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -1292,3 +1397,7 @@ urllib3 = [ {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] +virtualenv = [ + {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, + {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, +] diff --git a/pyproject.toml b/pyproject.toml index 4629726..17d31e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "botocove" -version = "1.4.1" +version = "1.5.0" description = "A decorator to allow running a function against all AWS accounts in an organization" authors = ["Dave Connell "] license = "LGPL-3.0-or-later" @@ -13,14 +13,12 @@ python = "^3.8" boto3 = "*" tqdm = "*" boto3-stubs = {extras = ["sts", "organizations"], version = "*"} -flakeheaven = "^0.11.0" [tool.poetry.dev-dependencies] pytest = "*" pytest-mock = "*" isort = "*" black = "*" -flake8-bandit = "*" flake8-bugbear = "*" flake8-builtins = "*" flake8-comprehensions = "*" @@ -31,6 +29,8 @@ flake8-pytest-style = "*" pep8-naming = "*" flake8-print = "*" mypy = "*" +pre-commit = "*" +flakeheaven = "*" [build-system] @@ -57,7 +57,6 @@ flake8-bugbear = ["+*"] flake8-builtins = ["+*"] flake8-comprehensions = ["+*"] flake8-eradicate = ["+*"] -flake8-isort = ["+*"] flake8-mutable = ["+*"] flake8-pytest-style = ["+*"] mccabe = ["+*"] diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 9216055..a2099a2 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -139,6 +139,7 @@ def simple_func(session: CoveSession, arg1: int, arg2: int, arg3: int) -> int: "Status": "ACTIVE", "AssumeRoleSuccess": True, "Result": 6, + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", } ] diff --git a/tests/test_decorator_no_args.py b/tests/test_decorator_no_args.py index c509852..be4320a 100644 --- a/tests/test_decorator_no_args.py +++ b/tests/test_decorator_no_args.py @@ -10,7 +10,7 @@ @pytest.fixture() def patch_boto3_client(mocker: MockerFixture) -> MagicMock: - mock_boto3 = mocker.patch("botocove.cove_sessions.boto3") + mock_boto3 = mocker.patch("botocove.cove_host_account.boto3") list_accounts_result = {"Accounts": [{"Id": "12345689012", "Status": "ACTIVE"}]} mock_boto3.client.return_value.get_paginator.return_value.paginate.return_value.build_full_result.return_value = ( # noqa E501 list_accounts_result @@ -48,6 +48,7 @@ def simple_func(session: CoveSession) -> str: "Name": "an-account-name", "Status": "ACTIVE", "AssumeRoleSuccess": True, + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", "Result": "hello", } @@ -69,6 +70,7 @@ def simple_func(session: CoveSession, output: str) -> str: "Name": "an-account-name", "Status": "ACTIVE", "AssumeRoleSuccess": True, + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", "Result": "blue", } @@ -93,6 +95,7 @@ def simple_func( "Name": "an-account-name", "Status": "ACTIVE", "AssumeRoleSuccess": True, + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", "Result": ("blue", "circle", "11:11"), } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index ffb9b59..9a8a274 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock import pytest -from botocore.exceptions import ClientError from botocove import cove from botocove.cove_session import CoveSession @@ -67,23 +66,6 @@ def simple_func(session: CoveSession) -> str: simple_func() -def test_no_valid_sessions_exception(mock_boto3_session: MagicMock) -> None: - mock_boto3_session.client.return_value.assume_role.side_effect = [ - ClientError({"Error": {}}, "error1"), - ClientError({"Error": {}}, "error2"), - ] - - @cove(assuming_session=mock_boto3_session, target_ids=["123", "456"]) - def simple_func(session: CoveSession) -> str: - return "hello" - - with pytest.raises( - ValueError, - match=("No accounts are accessible: check logs for detail"), - ): - simple_func() - - def test_handled_exception_in_wrapped_func(mock_boto3_session: MagicMock) -> None: @cove(assuming_session=mock_boto3_session, target_ids=["123"]) def simple_func(session: CoveSession) -> None: @@ -93,16 +75,18 @@ def simple_func(session: CoveSession) -> None: expected = [ { "Id": "123", + "RoleName": "OrganizationAccountAccessRole", + "AssumeRoleSuccess": True, "Arn": "hello-arn", "Email": "email@address.com", "Name": "an-account-name", "Status": "ACTIVE", - "AssumeRoleSuccess": True, "RoleSessionName": "OrganizationAccountAccessRole", "ExceptionDetails": Exception("oh no"), } ] # Compare repr of exceptions + print(results["Exceptions"]) assert repr(results["Exceptions"]) == repr(expected) diff --git a/tests/test_session.py b/tests/test_session.py index 713727d..003bd75 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -10,7 +10,7 @@ @pytest.fixture() def patch_boto3_client(mocker: MockerFixture) -> MagicMock: - mock_boto3 = mocker.patch("botocove.cove_sessions.boto3") + mock_boto3 = mocker.patch("botocove.cove_host_account.boto3") list_accounts_result = {"Accounts": [{"Id": "12345689012", "Status": "ACTIVE"}]} mock_boto3.client.return_value.get_paginator.return_value.paginate.return_value.build_full_result.return_value = ( # noqa E501 list_accounts_result @@ -48,6 +48,7 @@ def simple_func(session: CoveSession, a_string: str) -> str: "Status": "ACTIVE", "AssumeRoleSuccess": True, "Result": "test-string", + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", } ] @@ -72,6 +73,7 @@ def simple_func(session: CoveSession, a_string: str) -> str: "Status": "ACTIVE", "AssumeRoleSuccess": True, "Result": "test-string", + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", "Policy": session_policy, } @@ -99,6 +101,7 @@ def simple_func(session: CoveSession, a_string: str) -> str: "Status": "ACTIVE", "AssumeRoleSuccess": True, "Result": "test-string", + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", "PolicyArns": session_policy_arns, } @@ -125,6 +128,7 @@ def simple_func(session: CoveSession, a_string: str) -> str: "Id": "12345689012", "AssumeRoleSuccess": True, "Result": "test-string", + "RoleName": "OrganizationAccountAccessRole", "RoleSessionName": "OrganizationAccountAccessRole", } ]