From 53ca929ac022a8cb96f088c7d4df053b3af295f7 Mon Sep 17 00:00:00 2001 From: jkeifer Date: Tue, 30 Apr 2024 20:55:38 -0700 Subject: [PATCH] wip --- setup.py | 10 +- .../{plugins => }/management/__init__.py | 0 src/cirrus/management/__main__.py | 9 + src/cirrus/management/cli.py | 73 ++++++ .../management/commands/__init__.py | 0 src/cirrus/management/commands/deployments.py | 19 ++ .../management/commands/manage.py | 42 +--- .../management/commands/payload.py | 4 +- .../{plugins => }/management/deployment.py | 209 ++++++------------ src/cirrus/management/deployment_pointer.py | 125 +++++++++++ .../{plugins => }/management/exceptions.py | 0 .../utils/__init__.py => management/py.typed} | 0 src/cirrus/management/utils/__init__.py | 0 src/cirrus/management/utils/boto3.py | 20 ++ .../{plugins => }/management/utils/click.py | 3 + .../management/utils/templating.py | 0 .../management/commands/deployments.py | 53 ----- src/cirrus/plugins/management/utils/boto3.py | 52 ----- 18 files changed, 326 insertions(+), 293 deletions(-) rename src/cirrus/{plugins => }/management/__init__.py (100%) create mode 100644 src/cirrus/management/__main__.py create mode 100644 src/cirrus/management/cli.py rename src/cirrus/{plugins => }/management/commands/__init__.py (100%) create mode 100644 src/cirrus/management/commands/deployments.py rename src/cirrus/{plugins => }/management/commands/manage.py (89%) rename src/cirrus/{plugins => }/management/commands/payload.py (88%) rename src/cirrus/{plugins => }/management/deployment.py (59%) create mode 100644 src/cirrus/management/deployment_pointer.py rename src/cirrus/{plugins => }/management/exceptions.py (100%) rename src/cirrus/{plugins/management/utils/__init__.py => management/py.typed} (100%) create mode 100644 src/cirrus/management/utils/__init__.py create mode 100644 src/cirrus/management/utils/boto3.py rename src/cirrus/{plugins => }/management/utils/click.py (95%) rename src/cirrus/{plugins => }/management/utils/templating.py (100%) delete mode 100644 src/cirrus/plugins/management/commands/deployments.py delete mode 100644 src/cirrus/plugins/management/utils/boto3.py diff --git a/setup.py b/setup.py index 87eb644..c709f25 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_namespace_packages, setup -from src.cirrus.plugins.management import DESCRIPTION, NAME +from src.cirrus.management import DESCRIPTION, NAME HERE = os.path.abspath(os.path.dirname(__file__)) VERSION = os.environ.get("PLUGIN_VERSION", "0.0.0") @@ -41,11 +41,7 @@ license="Apache-2.0", include_package_data=True, entry_points=""" - [cirrus.plugins] - {NAME}=cirrus.plugins.management - [cirrus.commands] - manage=cirrus.plugins.management.commands.manage:manage - payload=cirrus.plugins.management.commands.payload:payload - deployments=cirrus.plugins.management.commands.deployments:deployments + [console_scripts] + cirrus-mgmt=cirrus.management.__main__:main """, ) diff --git a/src/cirrus/plugins/management/__init__.py b/src/cirrus/management/__init__.py similarity index 100% rename from src/cirrus/plugins/management/__init__.py rename to src/cirrus/management/__init__.py diff --git a/src/cirrus/management/__main__.py b/src/cirrus/management/__main__.py new file mode 100644 index 0000000..b6233fa --- /dev/null +++ b/src/cirrus/management/__main__.py @@ -0,0 +1,9 @@ +from cirrus.management.cli import cli + + +def main() -> None: + cli() + + +if __name__ == "__main__": + main() diff --git a/src/cirrus/management/cli.py b/src/cirrus/management/cli.py new file mode 100644 index 0000000..6144946 --- /dev/null +++ b/src/cirrus/management/cli.py @@ -0,0 +1,73 @@ +import sys +from functools import wraps +from typing import Any, Callable + +import boto3 +import botocore.exceptions +import click +from cirrus.cli.utils import click as utils_click +from cirrus.cli.utils import logging +from cirrus.core import exceptions + +from cirrus.management import DESCRIPTION, NAME +from cirrus.management.commands.deployments import list_deployments +from cirrus.management.commands.manage import manage as manage_group +from cirrus.management.commands.payload import payload as payload_group +from cirrus.management.exceptions import SSOError + +logger = logging.getLogger(__name__) + + +def handle_sso_error(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> Any: + try: + return func(*args, **kwargs) + except ( + botocore.exceptions.UnauthorizedSSOTokenError, + botocore.exceptions.TokenRetrievalError, + botocore.exceptions.SSOTokenLoadError, + ) as e: + raise SSOError( + "SSO session not authorized. Run `aws sso login` and try again.", + ) from e + + return wrapper + + +class MainGroup(utils_click.AliasedShortMatchGroup): + def invoke(self, *args, **kwargs) -> Any: + try: + return handle_sso_error(super().invoke)(*args, **kwargs) + except exceptions.CirrusError as e: + logger.error( + e, + exc_info=( + e if logger.getEffectiveLevel() < logging.logging.INFO else False + ), + ) + sys.exit(e.exit_code) + + +@click.group( + name=NAME, + help=DESCRIPTION, + cls=MainGroup, +) +@click.option( + "--profile", + help="AWS CLI profile name to use for session", +) +@click.pass_context +@logging.verbosity() +def cli(ctx, verbose, profile: str | None = None) -> None: + ctx.obj = boto3.Session(profile_name=profile) + + +cli.add_command(list_deployments) +cli.add_command(manage_group) +cli.add_command(payload_group) + + +if __name__ == "__main__": + cli() diff --git a/src/cirrus/plugins/management/commands/__init__.py b/src/cirrus/management/commands/__init__.py similarity index 100% rename from src/cirrus/plugins/management/commands/__init__.py rename to src/cirrus/management/commands/__init__.py diff --git a/src/cirrus/management/commands/deployments.py b/src/cirrus/management/commands/deployments.py new file mode 100644 index 0000000..f4eb2a6 --- /dev/null +++ b/src/cirrus/management/commands/deployments.py @@ -0,0 +1,19 @@ +import logging + +import boto3 +import click + +from cirrus.management.deployment import Deployment +from cirrus.management.utils.click import pass_session + +logger = logging.getLogger(__name__) + + +@click.command() +@pass_session +def list_deployments(session: boto3.Session) -> None: + """ + List all project deployments (accessible via current AWS role) + """ + for deployment in Deployment.yield_deployments(session=session): + click.echo(f"{deployment.name} ({deployment.secret_arn})") diff --git a/src/cirrus/plugins/management/commands/manage.py b/src/cirrus/management/commands/manage.py similarity index 89% rename from src/cirrus/plugins/management/commands/manage.py rename to src/cirrus/management/commands/manage.py index d0e0147..49f51dd 100644 --- a/src/cirrus/plugins/management/commands/manage.py +++ b/src/cirrus/management/commands/manage.py @@ -2,18 +2,18 @@ import logging import sys from functools import wraps +from subprocess import CalledProcessError +from typing import Optional +import boto3 import click from cirrus.cli.utils import click as utils_click from click_option_group import RequiredMutuallyExclusiveOptionGroup, optgroup -from cirrus.plugins.management.deployment import ( - WORKFLOW_POLL_INTERVAL, - CalledProcessError, - Deployment, -) -from cirrus.plugins.management.utils.click import ( +from cirrus.management.deployment import WORKFLOW_POLL_INTERVAL, Deployment +from cirrus.management.utils.click import ( additional_variables, + pass_session, silence_templating_errors, ) @@ -72,17 +72,17 @@ def wrapper(*args, **kwargs): aliases=["mgmt"], cls=utils_click.AliasedShortMatchGroup, ) -@utils_click.requires_project @click.argument( "deployment", metavar="DEPLOYMENT_NAME", ) +@pass_session @click.pass_context -def manage(ctx, project, deployment): +def manage(ctx, session: boto3.Session, deployment: str, profile: Optional[str] = None): """ - Commands to run management operations against project deployments. + Commands to run management operations against a cirrus deployment. """ - ctx.obj = Deployment.from_name(deployment, project) + ctx.obj = Deployment.from_name(deployment, session=session) @manage.command() @@ -93,28 +93,6 @@ def show(deployment): click.secho(deployment.asjson(indent=4), fg=color) -@manage.command("get-path") -@pass_deployment -def get_path(deployment): - """Get path to deployment directory""" - click.echo(deployment.path) - - -@manage.command() -@pass_deployment -@click.option( - "--stackname", -) -@click.option( - "--profile", -) -def refresh(deployment, stackname=None, profile=None): - """Refresh the environment values from the AWS deployment, - optionally changing the stackname or profile. - """ - deployment.refresh(stackname=stackname, profile=profile) - - @manage.command("run-workflow") @click.option( "-t", diff --git a/src/cirrus/plugins/management/commands/payload.py b/src/cirrus/management/commands/payload.py similarity index 88% rename from src/cirrus/plugins/management/commands/payload.py rename to src/cirrus/management/commands/payload.py index 31b6168..222603f 100644 --- a/src/cirrus/plugins/management/commands/payload.py +++ b/src/cirrus/management/commands/payload.py @@ -5,7 +5,7 @@ import click from cirrus.cli.utils import click as utils_click -from cirrus.plugins.management.utils.click import ( +from cirrus.management.utils.click import ( additional_variables, silence_templating_errors, ) @@ -43,7 +43,7 @@ def get_id(): @additional_variables @silence_templating_errors def template(additional_variables, silence_templating_errors): - from cirrus.plugins.management.utils.templating import template_payload + from cirrus.management.utils.templating import template_payload click.echo( template_payload( diff --git a/src/cirrus/plugins/management/deployment.py b/src/cirrus/management/deployment.py similarity index 59% rename from src/cirrus/plugins/management/deployment.py rename to src/cirrus/management/deployment.py index 7074a51..09a121c 100644 --- a/src/cirrus/plugins/management/deployment.py +++ b/src/cirrus/management/deployment.py @@ -1,17 +1,23 @@ +from __future__ import annotations + import dataclasses import json import logging import os +from collections.abc import Iterator from datetime import datetime, timezone from pathlib import Path -from subprocess import CalledProcessError, check_call +from subprocess import check_call from time import sleep, time +from typing import IO, Any import backoff +import boto3 from cirrus.lib2.process_payload import ProcessPayload -from . import exceptions -from .utils.boto3 import get_mfa_session, validate_session +from cirrus.management import exceptions +from cirrus.management.deployment_pointer import DeploymentPointer +from cirrus.management.utils.boto3 import get_client logger = logging.getLogger(__name__) @@ -22,17 +28,11 @@ WORKFLOW_POLL_INTERVAL = 15 # seconds between state checks -def deployments_dir_from_project(project): - _dir = project.dot_dir.joinpath(DEFAULT_DEPLOYMENTS_DIR_NAME) - _dir.mkdir(exist_ok=True) - return _dir - - def now_isoformat(): return datetime.now(timezone.utc).isoformat() -def _maybe_use_buffer(fileobj): +def _maybe_use_buffer(fileobj: IO): return fileobj.buffer if hasattr(fileobj, "buffer") else fileobj @@ -47,122 +47,62 @@ class DeploymentMeta: user_vars: dict config_version: int - @classmethod - def load(cls, path: Path): - config = json.loads(path.read_text()) - if version := config.get("config_version") != CONFIG_VERSION: - raise exceptions.DeploymentConfigurationError( - f"Unable to load config version: {version}", - ) - try: - return cls(**config) - except TypeError as e: - raise exceptions.DeploymentConfigurationError( - f"Failed to load configuration: {e}", - ) + def save(self, path: Path) -> int: + return path.write_text(self.asjson(indent=4)) - def save(self): - self.path.write_text(self.asjson(indent=4)) - - def asdict(self): + def asdict(self) -> dict[str, Any]: return dataclasses.asdict(self) - def asjson(self, *args, **kwargs): + def asjson(self, *args, **kwargs) -> str: return json.dumps(self.asdict(), *args, **kwargs) +# @staticmethod +# def _get_session(profile: str = None): +# # TODO: MFA session should likely be used only with the cli, +# # so this probably needs to be parameterized by the caller +# +# def get_session(self): +# if not self._session: +# self._session = self._get_session(profile=self.profile) +# return self._session + + @dataclasses.dataclass class Deployment(DeploymentMeta): - def __init__(self, path: Path, *args, **kwargs): - self.path = path - + def __init__( + self, + *args, + session: boto3.Session | None = None, + **kwargs, + ) -> None: super().__init__(*args, **kwargs) + self.session = session if session else boto3.Session() + self._functions: list[str] | None = None - self._session = None - self._functions = None - - @classmethod - def create(cls, name: str, project, stackname: str = None, profile: str = None): - if not stackname: - stackname = project.config.get_stackname(name) - - env = cls.get_env_from_lambda(stackname, cls._get_session(profile)) - - now = now_isoformat() - meta = { - "name": name, - "created": now, - "updated": now, - "stackname": stackname, - "profile": profile, - "environment": env, - "user_vars": {}, - "config_version": CONFIG_VERSION, - } - - path = cls.get_path_from_project(project, name) - self = cls(path, **meta) - self.save() - - return self - - @classmethod - def from_file(cls, path: Path): - return cls(path, **DeploymentMeta.load(path).asdict()) + @staticmethod + def yield_deployments( + region: str | None = None, + session: boto3.Session | None = None, + ) -> Iterator[DeploymentPointer]: + yield from DeploymentPointer.list(region=region, session=session) @classmethod - def from_name(cls, name: str, project): - path = cls.get_path_from_project(project, name) - try: - return cls.from_file(path) - except FileNotFoundError: - raise exceptions.DeploymentNotFoundError(name) from None + def from_pointer( + cls, + pointer: DeploymentPointer, + session: boto3.Session | None = None, + ) -> Deployment: + return cls(session=session, **pointer.get_config(session=session)) @classmethod - def remove(cls, name: str, project): - cls.get_path_from_project(project, name).unlink(missing_ok=True) - - @staticmethod - def yield_deployments(project): - for f in deployments_dir_from_project(project).glob("*.json"): - if f.is_file(): - try: - yield DeploymentMeta.load(f).name - except exceptions.DeploymentConfigurationError: - yield f"{f.stem} (invalid configuration)" - except Exception: - logger.exception("failed on %s", f) - pass - - @staticmethod - def get_path_from_project(project, name: str): - return deployments_dir_from_project(project).joinpath(f"{name}.json") - - @staticmethod - def _get_session(profile: str = None): - # TODO: MFA session should likely be used only with the cli, - # so this probably needs to be parameterized by the caller - # Likely we need a Session class wrapping the boto3 session - # object that caches clients. That would be useful in the lib generally. - return validate_session(get_mfa_session(profile=profile), profile) - - @staticmethod - def get_env_from_lambda(stackname: str, session): - aws_lambda = session.client("lambda") - - try: - process_conf = aws_lambda.get_function_configuration( - FunctionName=f"{stackname}-process", - ) - except aws_lambda.exceptions.ResourceNotFoundException: - # TODO: fatal error bad lambda name, needs better handling - raise - - return process_conf["Environment"]["Variables"] + def from_name(cls, name: str, session: boto3.Session | None = None) -> Deployment: + dp = DeploymentPointer.get(name, session=session) + return cls.from_pointer(dp, session=session) def get_lambda_functions(self): if self._functions is None: - aws_lambda = self.get_session().client("lambda") + aws_lambda = get_client("lambda") def deployment_functions_filter(response): return [ @@ -178,21 +118,6 @@ def deployment_functions_filter(response): self._functions += deployment_functions_filter(resp) return self._functions - def get_session(self): - if not self._session: - self._session = self._get_session(profile=self.profile) - return self._session - - def reload(self): - self.__dict__.update(DeploymentMeta.load(self.path).asdict()) - - def refresh(self, stackname: str = None, profile: str = None): - self.stackname = stackname if stackname else self.stackname - self.profile = profile if profile else self.profile - self.environment = self.get_env_from_lambda(self.stackname, self.get_session()) - self.updated = now_isoformat() - self.save() - def set_env(self, include_user_vars=False): os.environ.update(self.environment) if include_user_vars: @@ -200,19 +125,6 @@ def set_env(self, include_user_vars=False): if self.profile: os.environ["AWS_PROFILE"] = self.profile - def add_user_vars(self, _vars, save=False): - self.user_vars.update(_vars) - if save: - self.save() - - def del_user_var(self, name, save=False): - try: - del self.user_vars[name] - except KeyError: - pass - if save: - self.save() - def exec(self, command, include_user_vars=True, isolated=False): import os @@ -240,7 +152,7 @@ def get_payload_state(self, payload_id): statedb = StateDB( table_name=self.environment["CIRRUS_STATE_DB"], - session=self.get_session(), + session=self.session, ) @backoff.on_predicate(backoff.expo, lambda x: x is None, max_time=60) @@ -259,7 +171,7 @@ def process_payload(self, payload): if hasattr(payload, "read"): stream = _maybe_use_buffer(payload) # add two to account for EOF and needing to know - # if greater than not just equal tomax length + # if greater than not just equal to max length payload = payload.read(MAX_SQS_MESSAGE_LENGTH + 2) if len(payload.encode("utf-8")) > MAX_SQS_MESSAGE_LENGTH: @@ -271,11 +183,11 @@ def process_payload(self, payload): url = f"s3://{bucket}/{key}" logger.warning("Message exceeds SQS max length.") logger.warning("Uploading to '%s'", url) - s3 = self.get_session().client("s3") + s3 = get_client("s3", session=self.session) s3.upload_fileobj(stream, bucket, key) payload = json.dumps({"url": url}) - sqs = self.get_session().client("sqs") + sqs = get_client("sqs", session=self.session) return sqs.send_message( QueueUrl=self.environment["CIRRUS_PROCESS_QUEUE_URL"], MessageBody=payload, @@ -291,12 +203,12 @@ def get_payload_by_id(self, payload_id, output_fileobj): ) logger.debug("bucket: '%s', key: '%s'", bucket, key) - s3 = self.get_session().client("s3") + s3 = get_client("s3", session=self.session) return s3.download_fileobj(bucket, key, output_fileobj) def get_execution(self, arn): - sfn = self.get_session().client("stepfunctions") + sfn = get_client("stepfunctions", session=self.session) return sfn.describe_execution(executionArn=arn) def get_execution_by_payload_id(self, payload_id): @@ -309,7 +221,7 @@ def get_execution_by_payload_id(self, payload_id): return self.get_execution(exec_arn) def invoke_lambda(self, event, function_name): - aws_lambda = self.get_session().client("lambda") + aws_lambda = get_client("lambda", session=self.session) if function_name not in self.get_lambda_functions(): raise ValueError( f"lambda named '{function_name}' not found in deployment '{self.name}'" @@ -326,7 +238,7 @@ def run_workflow( payload: dict, timeout: int = 3600, poll_interval: int = WORKFLOW_POLL_INTERVAL, - ) -> dict: + ) -> dict[str, Any]: """ Args: @@ -372,7 +284,7 @@ def run_workflow( def template_payload( self, payload: str, - additional_vars: dict = None, + additional_vars: dict[str, str] | None = None, silence_templating_errors: bool = False, include_user_vars: bool = True, ): @@ -383,5 +295,8 @@ def template_payload( _vars.update(self.user_vars) return template_payload( - payload, _vars, silence_templating_errors, **dict(additional_vars) + payload, + _vars, + silence_templating_errors, + **(additional_vars or {}), ) diff --git a/src/cirrus/management/deployment_pointer.py b/src/cirrus/management/deployment_pointer.py new file mode 100644 index 0000000..87d4571 --- /dev/null +++ b/src/cirrus/management/deployment_pointer.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import json +import re +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any + +import boto3 + +from cirrus.management.utils.boto3 import get_client + +DEFAULT_CIRRUS_DEPLOYMENT_PREFIX = "/cirrus/deployments/" + + +def get_secret( + secret_arn: str, + region: str | None = None, + session: boto3.Session | None = None, +) -> str: + sm = get_client("secretsmanager", region, session) + resp = sm.get_secret_value( + SecretId=secret_arn, + ) + return resp["SecretString"] + + +@dataclass +class SecretArn: + _format = "arn:aws:secretsmanager:{region}:{account_id}:secret:{name}".format + _regex = re.compile( + r"^arn:aws:secretsmanager:(?P[a-z0-9\-]+):(?P\d{12}):secret:(?P.+)$", + ) + + def __init__(self, account_id: str, region: str, name: str) -> None: + self.account_id = account_id + self.name = name + self.region = region + + def __str__(self) -> str: + return self._format( + region=self.region, + account_id=self.account_id, + name=self.name, + ) + + @classmethod + def from_string(cls, string: str) -> SecretArn: + match = cls._regex.match(string) + + if not match: + raise ValueError(f"Unparsable secret arn string '{string}'") + + groups = match.groupdict() + + return cls( + account_id=groups["account_id"], + name=groups["name"], + region=groups["region"], + ) + + def fetch( + self, + session: boto3.Session | None = None, + ) -> str: + return get_secret( + secret_arn=str(self), + region=self.region, + session=session, + ) + + +@dataclass +class DeploymentPointer: + prefix: str + name: str + secret_arn: SecretArn + + @classmethod + def _from_parameter( + cls, parameter: dict[str, Any], prefix: str = "" + ) -> DeploymentPointer: + name = parameter["Name"][len(prefix) :] + return cls( + name=name, + secret_arn=SecretArn.from_string(parameter["Value"]), + prefix=prefix, + ) + + @classmethod + def get( + cls, + deployment_name: str, + deployment_prefix: str = DEFAULT_CIRRUS_DEPLOYMENT_PREFIX, + region: str | None = None, + session: boto3.Session | None = None, + ): + ssm = get_client("ssm", region, session) + return cls._from_parameter( + ssm.get_parameter( + Name=f"{deployment_prefix}{deployment_name}", + )["Parameter"], + prefix=deployment_prefix, + ) + + @classmethod + def list( + cls, + deployment_prefix: str = DEFAULT_CIRRUS_DEPLOYMENT_PREFIX, + region: str | None = None, + session: boto3.Session | None = None, + ) -> Iterator[DeploymentPointer]: + ssm = get_client("ssm", region, session) + resp = ssm.get_parameters_by_path(Path=deployment_prefix) + for param in resp["Parameters"]: + yield cls._from_parameter( + param, + prefix=deployment_prefix, + ) + + def get_config( + self, + session: boto3.Session | None = None, + ) -> dict[str, Any]: + return json.loads(self.secret_arn.fetch(session=session)) diff --git a/src/cirrus/plugins/management/exceptions.py b/src/cirrus/management/exceptions.py similarity index 100% rename from src/cirrus/plugins/management/exceptions.py rename to src/cirrus/management/exceptions.py diff --git a/src/cirrus/plugins/management/utils/__init__.py b/src/cirrus/management/py.typed similarity index 100% rename from src/cirrus/plugins/management/utils/__init__.py rename to src/cirrus/management/py.typed diff --git a/src/cirrus/management/utils/__init__.py b/src/cirrus/management/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cirrus/management/utils/boto3.py b/src/cirrus/management/utils/boto3.py new file mode 100644 index 0000000..7e3fd9e --- /dev/null +++ b/src/cirrus/management/utils/boto3.py @@ -0,0 +1,20 @@ +from functools import cache +from typing import Optional + +import boto3 + + +# TODO: replace with version from cirrus.lib2 once it lands +@cache +def get_client( + service: str, + region: Optional[str] = None, + session: Optional[boto3.Session] = None, +): + if not session: + session = boto3.Session() + + return session.client( + service_name=service, + region_name=region, + ) diff --git a/src/cirrus/plugins/management/utils/click.py b/src/cirrus/management/utils/click.py similarity index 95% rename from src/cirrus/plugins/management/utils/click.py rename to src/cirrus/management/utils/click.py index 27d5130..a03b0b8 100644 --- a/src/cirrus/plugins/management/utils/click.py +++ b/src/cirrus/management/utils/click.py @@ -1,5 +1,8 @@ +import boto3 import click +pass_session = click.make_pass_decorator(boto3.Session) + class VariableFile(click.File): name = "variable file" diff --git a/src/cirrus/plugins/management/utils/templating.py b/src/cirrus/management/utils/templating.py similarity index 100% rename from src/cirrus/plugins/management/utils/templating.py rename to src/cirrus/management/utils/templating.py diff --git a/src/cirrus/plugins/management/commands/deployments.py b/src/cirrus/plugins/management/commands/deployments.py deleted file mode 100644 index d4601ca..0000000 --- a/src/cirrus/plugins/management/commands/deployments.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging - -import click -from cirrus.cli.utils import click as utils_click - -from cirrus.plugins.management.deployment import Deployment - -logger = logging.getLogger(__name__) - - -@click.group( - cls=utils_click.AliasedShortMatchGroup, -) -@utils_click.requires_project -def deployments(project): - """ - List/add/remove project deployments. - """ - pass - - -@deployments.command(aliases=["ls", "list"]) -@utils_click.requires_project -def show(project): - for deployment_name in Deployment.yield_deployments(project): - click.echo(deployment_name) - - -# TODO: better help -@deployments.command(aliases=["mk"]) -@utils_click.requires_project -@click.argument( - "name", - metavar="name", -) -@click.option( - "--stackname", -) -@click.option( - "--profile", -) -def add(project, name, stackname=None, profile=None): - Deployment.create(name, project, stackname=stackname, profile=profile) - - -@deployments.command(aliases=["rm"]) -@utils_click.requires_project -@click.argument( - "name", - metavar="name", -) -def remove(project, name): - Deployment.remove(name, project) diff --git a/src/cirrus/plugins/management/utils/boto3.py b/src/cirrus/plugins/management/utils/boto3.py deleted file mode 100644 index 023cbdd..0000000 --- a/src/cirrus/plugins/management/utils/boto3.py +++ /dev/null @@ -1,52 +0,0 @@ -import os - -import boto3 -import botocore.session -from botocore import credentials - -from cirrus.plugins.management.exceptions import SSOError - -# From https://github.com/boto/botocore/pull/1157#issuecomment-387580482 - - -def get_mfa_session(**kwargs): - """Get a session that supports caching the MFA session token. - - Returns a boto3 session object. - - Supports all kwargs of botocore.session. Of interest: - - profile: The name of the profile to use for this - session. Note that the profile can only be set when - the session is created. - - session_vars: A dictionary that is used to override some or all - of the environment variables associated with this session. The - key/value pairs defined in this dictionary will override the - corresponding variables defined in ``SESSION_VARIABLES``. - """ - profile = kwargs.get("profile", None) - - # Construct botocore session with cache - session = botocore.session.Session(**kwargs) - provider = session.get_component("credential_provider").get_provider("assume-role") - if profile: - # If ``profile`` is provided, then we need to - # change the cache path from the default of - # ~/.aws/boto/cache to the one used by awscli. - # Without ``profile``, we defer to normal boto operations. - working_dir = os.path.join(os.path.expanduser("~"), ".aws/cli/cache") - provider.cache = credentials.JSONFileCache(working_dir) - - return boto3.Session(botocore_session=session) - - -def validate_session(session, profile): - try: - session.client("sts").get_caller_identity() - except boto3.exceptions.botocore.exceptions.UnauthorizedSSOTokenError: - raise SSOError( - f"SSO session not authorized. Run `aws sso login --profile {profile}` and try again.", - ) from None - - return session