From cab31dc2b86024664ec56aead8528d05a5ccb041 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 25 May 2024 16:09:33 -0300 Subject: [PATCH 01/15] chore: building local image + adapting code --- leverage/_utils.py | 72 ----- leverage/container.py | 230 +++++++------- leverage/containers/kubectl.py | 7 +- leverage/modules/terraform.py | 36 +-- leverage/path.py | 2 +- .../scripts/aws-mfa/aws-mfa-entrypoint.sh | 296 ++++++++++++++++++ leverage/scripts/aws-sso/aws-sso-login.sh | 70 +++++ leverage/scripts/aws-sso/aws-sso-logout.sh | 57 ++++ pyproject.toml | 3 +- tests/test_containers/test_leverage.py | 9 - 10 files changed, 550 insertions(+), 232 deletions(-) create mode 100755 leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh create mode 100755 leverage/scripts/aws-sso/aws-sso-login.sh create mode 100755 leverage/scripts/aws-sso/aws-sso-logout.sh diff --git a/leverage/_utils.py b/leverage/_utils.py index c67442da..6e03e114 100644 --- a/leverage/_utils.py +++ b/leverage/_utils.py @@ -1,10 +1,6 @@ """ General use utilities. """ -import io -import os -import tarfile -from pathlib import Path from subprocess import run from subprocess import PIPE @@ -14,7 +10,6 @@ from docker.models.containers import Container from leverage import logger -from leverage.logger import raw_logger def clean_exception_traceback(exception): @@ -65,10 +60,6 @@ def git(command): run(command, stdout=PIPE, stderr=PIPE, check=True) -def chain_commands(commands: list, chain: str = " && ") -> str: - return f'bash -c "{chain.join(commands)}"' - - class CustomEntryPoint: """ Set a custom entrypoint on the container while entering the context. @@ -109,35 +100,6 @@ def __exit__(self, *args, **kwargs): "AWS_CONFIG_FILE": self.container.environment["AWS_CONFIG_FILE"].replace(".aws", "tmp"), } ) - # now return file ownership on the aws credentials files - self.container.change_file_ownership(self.container.paths.guest_aws_credentials_dir) - - -class AwsCredsContainer: - """ - Fetching AWS credentials by setting the SSO/MFA entrypoints on a living container. - This flow runs a command on a living container before any other command, leaving your AWS credentials ready - for authentication. - - In the case of MFA, the env var tweaks (that happens at .auth_method()) must stay until the end of the block - given the container is reused for more commands. - """ - - def __init__(self, container: Container, tf_container): - self.container = container - self.tf_container = tf_container - - def __enter__(self): - auth_method = self.tf_container.auth_method() - if not auth_method: - return - - exit_code, output = self.container.exec_run(auth_method, environment=self.tf_container.environment) - raw_logger.info(output.decode("utf-8")) - - def __exit__(self, *args, **kwargs): - # now return file ownership on the aws credentials files - self.tf_container.change_file_ownership(self.tf_container.paths.guest_aws_credentials_dir) class ExitError(Exit): @@ -169,40 +131,6 @@ def __exit__(self, exc_type, exc_value, exc_tb): self.docker_client.api.remove_container(self.container_data) -class LiveContainer(ContainerSession): - """ - A container that run a command that "do nothing" indefinitely. The idea is to keep the container alive. - """ - - COMMAND = "tail -f /dev/null" - - def __init__(self, leverage_container, tty=True): - with CustomEntryPoint(leverage_container, self.COMMAND): - container_data = leverage_container._create_container(tty) - super().__init__(leverage_container.client, container_data) - - -def tar_directory(host_dir_path: Path) -> bytes: - """ - Compress a local directory on memory as a tar file and return it as bytes. - """ - bytes_array = io.BytesIO() - with tarfile.open(fileobj=bytes_array, mode="w") as tar_handler: - # walk through the directory tree - for root, dirs, files in os.walk(host_dir_path): - for f in files: - # and add each file to the tar file - file_path = Path(os.path.join(root, f)) - tar_handler.add( - os.path.join(root, f), - arcname=file_path.relative_to(host_dir_path), - ) - - bytes_array.seek(0) - # return the whole tar file as a byte array - return bytes_array.read() - - def key_finder(d: dict, target: str, avoid: str = None): """ Iterate over a dict of dicts and/or lists of dicts, looking for a key with value "target". diff --git a/leverage/container.py b/leverage/container.py index 5e3964f3..c12c2133 100644 --- a/leverage/container.py +++ b/leverage/container.py @@ -1,7 +1,9 @@ import json import os import re +import timeit import webbrowser +from io import BytesIO from pathlib import Path from datetime import datetime from time import sleep @@ -94,7 +96,7 @@ def __init__(self, client, mounts: tuple = None, env_vars: dict = None): mounts = [Mount(source=source, target=target, type="bind") for source, target in mounts] if mounts else [] self.host_config = self.client.api.create_host_config(security_opt=["label:disable"], mounts=mounts) self.container_config = { - "image": f"{self.image}:{self.image_tag}", + "image": f"{self.image}:local", "command": "", "stdin_open": True, "environment": env_vars or {}, @@ -138,43 +140,52 @@ def region(self): raise ExitError(1, f"No valid region could be found at: {self.paths.cwd.as_posix()}") - def ensure_image(self): - """Make sure the required Docker image is available in the system. If not, pull it from registry.""" - found_image = self.client.api.images(f"{self.image}:{self.image_tag}") - if found_image: - return - - logger.info("Required Docker image not found.") - - try: - stream = self.client.api.pull(repository=self.image, tag=self.image_tag, stream=True, decode=True) - except NotFound as e: - logger.error( - f"The specified toolbox version, '{self.image_tag}' (toolbox image '{self.image}:{self.image_tag}') can not be found. " - "If you come from a project created with an older version of Leverage CLI or have modified the 'build.env' file manually, " - f"please consider either deleting the file, or configuring a valid toolbox version to use. (i.e. 'TERRAFORM_IMAGE_TAG={__toolbox_version__}')" + @property + def local_image(self): + return BytesIO( + f""" + ARG IMAGE_TAG + FROM binbash/leverage-toolbox:$IMAGE_TAG + + # Needed as is mounted later on + #RUN mkdir /root/.ssh + # Needed for git to run propertly + #RUN touch /root/.gitconfig + + ARG UNAME + ARG UID + ARG GID + RUN groupadd -g $GID -o $UNAME + RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME + USER $UNAME + """.encode( + "utf-8" ) - raise Exit(1) - except APIError as pull: - pull.__traceback__ = None - pull.__context__.__traceback__ = None - logger.exception("Error pulling image:", exc_info=pull) - raise Exit(1) - except Exception as e: - logger.error(f"Not handled error while pulling the image: {e}") - raise Exit(1) + ) - logger.info(next(stream)["status"]) + def ensure_image(self): + logger.info(f"Checking for local docker image, tag: {self.image_tag}...") + """Make sure the required local Docker image is available in the system. If not, pull it from registry.""" + build_args = { + "IMAGE_TAG": self.image_tag, + "UNAME": "leverage", # TODO: what is this exactly? + "GID": str(os.getgid()), + "UID": str(os.getuid()), + } - imageinfo = [] - with console.status("Pulling image..."): - for status in stream: - status = status["status"] - if status.startswith("Digest") or status.startswith("Status"): - imageinfo.append(status) + stream = self.client.api.build( + fileobj=self.local_image, + tag=f"binbash/leverage-toolbox:local", + pull=True, + buildargs=build_args, + decode=True, + ) - for info in imageinfo: - logger.info(info) + for line in stream: + if "stream" in line and line["stream"].startswith("Successfully built"): + logger.info("[green]✔ OK[/green]\n") + elif "errorDetail" in line: + raise ExitError(1, f"Failed building local image: {line['errorDetail']}") def _create_container(self, tty, command="", *args): """Create the container that will run the command. @@ -285,24 +296,26 @@ def exec(self, command: str, *arguments) -> Tuple[int, str]: def docker_logs(self, container): return self.client.api.logs(container).decode("utf-8") - def change_ownership_cmd(self, path: Union[Path, str], recursive=True) -> str: - recursive = "-R " if recursive else "" - user_id = os.getuid() - group_id = os.getgid() - return f"chown {user_id}:{group_id} {recursive}{path}" +class SSOContainer(LeverageContainer): + # SSO scripts + AWS_SSO_LOGIN_SCRIPT = "/opt/scripts/aws-sso/aws-sso-login.sh" + AWS_SSO_LOGOUT_SCRIPT = "/opt/scripts/aws-sso/aws-sso-logout.sh" - def change_file_ownership(self, path: Union[Path, str], recursive=True): - """ - Change the file/folder ownership from the internal docker user (usually root) - to the user executing the CLI. - """ - cmd = self.change_ownership_cmd(path, recursive=recursive) - with CustomEntryPoint(self, entrypoint=""): - self._exec(cmd) + # SSO constants + AWS_SSO_LOGIN_URL = "https://device.sso.{region}.amazonaws.com/?user_code={user_code}" + AWS_SSO_CODE_WAIT_SECONDS = 2 + AWS_SSO_CODE_ATTEMPTS = 10 + FALLBACK_LINK_MSG = "Opening the browser... if it fails, open this link in your browser:\n{link}" + def __init__(self, client, mounts=None, env_vars=None): + super().__init__(client, mounts=mounts, env_vars=env_vars) + self.mounts.extend( + [ + Mount(source=(Path(__file__).parent / "scripts").as_posix(), target="/opt/scripts", type="bind"), + ] + ) -class SSOContainer(LeverageContainer): def get_sso_access_token(self): with open(self.paths.sso_token_file) as token_file: return json.loads(token_file.read())["accessToken"] @@ -316,22 +329,56 @@ def sso_region_from_main_profile(self): conf.read(self.paths.host_aws_profiles_file) return conf.get(f"profile {self.project}-sso", "sso_region").value + def get_sso_code(self, container) -> str: + """ + Find and return the SSO user code by periodically checking the logs. + Up until N attempts. + """ + logger.info("Fetching SSO code...") + for _ in range(self.AWS_SSO_CODE_ATTEMPTS): + # pull logs periodically until we find our SSO code + logs = self.docker_logs(container) + if "Then enter the code:" in logs: + return logs.split("Then enter the code:")[1].split("\n")[2] + + sleep(self.AWS_SSO_CODE_WAIT_SECONDS) + + raise ExitError(1, "Get SSO code timed-out") + + def get_sso_region(self): + # TODO: what about using the .region property we have now? that takes the value from the path of the layer + _, region = self.exec(f"configure get sso_region --profile {self.project}-sso") + return region + + def sso_login(self) -> int: + region = self.get_sso_region() + + with CustomEntryPoint(self, ""): + container = self._create_container(False, command=self.AWS_SSO_LOGIN_SCRIPT) + + with ContainerSession(self.client, container): + # once inside this block, the SSO_LOGIN_SCRIPT is being executed in the "background" + # now let's grab the user code from the logs + user_code = self.get_sso_code(container) + # with the user code, we can now autocomplete the url + link = self.AWS_SSO_LOGIN_URL.format(region=region.strip(), user_code=user_code) + webbrowser.open_new_tab(link) + # The SSO code is only valid once: if the browser was able to open it, the fallback link will be invalid + logger.info(self.FALLBACK_LINK_MSG.format(link=link)) + # now let's wait until the command locking the container resolve itself: + # aws sso login will wait for the user code + # once submitted to the browser, the authentication finish and the lock is released + exit_code = self.client.api.wait(container)["StatusCode"] + raw_logger.info(self.docker_logs(container)) + + return exit_code + class AWSCLIContainer(SSOContainer): """Leverage Container specially tailored to run AWS CLI commands.""" AWS_CLI_BINARY = "/usr/local/bin/aws" - # SSO scripts - AWS_SSO_LOGIN_SCRIPT = "/root/scripts/aws-sso/aws-sso-login.sh" - AWS_SSO_LOGOUT_SCRIPT = "/root/scripts/aws-sso/aws-sso-logout.sh" - - # SSO constants - AWS_SSO_LOGIN_URL = "https://device.sso.{region}.amazonaws.com/?user_code={user_code}" - AWS_SSO_CODE_WAIT_SECONDS = 2 - AWS_SSO_CODE_ATTEMPTS = 10 - FALLBACK_LINK_MSG = "Opening the browser... if it fails, open this link in your browser:\n{link}" - def __init__(self, client): super().__init__(client) @@ -345,14 +392,16 @@ def __init__(self, client): "SCRIPT_LOG_LEVEL": get_script_log_level(), } self.entrypoint = self.AWS_CLI_BINARY - self.mounts = [ - Mount(source=self.paths.root_dir.as_posix(), target=self.paths.guest_base_path, type="bind"), - Mount( - source=self.paths.host_aws_credentials_dir.as_posix(), - target=self.paths.guest_aws_credentials_dir, - type="bind", - ), - ] + self.mounts.extend( + [ + Mount(source=self.paths.root_dir.as_posix(), target=self.paths.guest_base_path, type="bind"), + Mount( + source=self.paths.host_aws_credentials_dir.as_posix(), + target=self.paths.guest_aws_credentials_dir, + type="bind", + ), + ] + ) logger.debug(f"[bold cyan]Container configuration:[/bold cyan]\n{json.dumps(self.container_config, indent=2)}") @@ -381,52 +430,6 @@ def system_exec(self, command): self.entrypoint = self.AWS_CLI_BINARY return exit_code, output - def get_sso_code(self, container) -> str: - """ - Find and return the SSO user code by periodically checking the logs. - Up until N attempts. - """ - logger.info("Fetching SSO code...") - for _ in range(self.AWS_SSO_CODE_ATTEMPTS): - # pull logs periodically until we find our SSO code - logs = self.docker_logs(container) - if "Then enter the code:" in logs: - return logs.split("Then enter the code:")[1].split("\n")[2] - - sleep(self.AWS_SSO_CODE_WAIT_SECONDS) - - raise ExitError(1, "Get SSO code timed-out") - - def get_sso_region(self): - # TODO: what about using the .region property we have now? that takes the value from the path of the layer - _, region = self.exec(f"configure get sso_region --profile {self.project}-sso") - return region - - def sso_login(self) -> int: - region = self.get_sso_region() - - with CustomEntryPoint(self, ""): - container = self._create_container(False, command=self.AWS_SSO_LOGIN_SCRIPT) - - with ContainerSession(self.client, container): - # once inside this block, the SSO_LOGIN_SCRIPT is being executed in the "background" - # now let's grab the user code from the logs - user_code = self.get_sso_code(container) - # with the user code, we can now autocomplete the url - link = self.AWS_SSO_LOGIN_URL.format(region=region.strip(), user_code=user_code) - webbrowser.open_new_tab(link) - # The SSO code is only valid once: if the browser was able to open it, the fallback link will be invalid - logger.info(self.FALLBACK_LINK_MSG.format(link=link)) - # now let's wait until the command locking the container resolve itself: - # aws sso login will wait for the user code - # once submitted to the browser, the authentication finish and the lock is released - exit_code = self.client.api.wait(container)["StatusCode"] - raw_logger.info(self.docker_logs(container)) - # now return ownership of the token file back to the user - self.change_file_ownership(self.paths.guest_aws_credentials_dir) - - return exit_code - class TerraformContainer(SSOContainer): """Leverage container specifically tailored to run Terraform commands. @@ -434,8 +437,7 @@ class TerraformContainer(SSOContainer): TF_BINARY = "/bin/terraform" - TF_MFA_ENTRYPOINT = "/root/scripts/aws-mfa/aws-mfa-entrypoint.sh" - TF_SSO_ENTRYPOINT = "/root/scripts/aws-sso/aws-sso-entrypoint.sh" + TF_MFA_ENTRYPOINT = "/opt/scripts/aws-mfa/aws-mfa-entrypoint.sh" def __init__(self, client, mounts=None, env_vars=None): super().__init__(client, mounts=mounts, env_vars=env_vars) diff --git a/leverage/containers/kubectl.py b/leverage/containers/kubectl.py index c8d62c74..9006528f 100644 --- a/leverage/containers/kubectl.py +++ b/leverage/containers/kubectl.py @@ -4,7 +4,7 @@ from docker.types import Mount from leverage import logger -from leverage._utils import chain_commands, AwsCredsEntryPoint, ExitError +from leverage._utils import AwsCredsEntryPoint, ExitError from leverage.container import TerraformContainer @@ -46,13 +46,10 @@ def configure(self): # generate the command that will configure the new cluster with AwsCredsEntryPoint(self, override_entrypoint=""): add_eks_cluster_cmd = self._get_eks_kube_config() - # and the command that will set the proper ownership on the config file (otherwise the owner will be "root") - change_owner_cmd = self.change_ownership_cmd(self.KUBECTL_CONFIG_FILE, recursive=False) - full_cmd = chain_commands([add_eks_cluster_cmd, change_owner_cmd]) logger.info("Configuring context...") with AwsCredsEntryPoint(self, override_entrypoint=""): - exit_code = self._start(full_cmd) + exit_code = self._start(add_eks_cluster_cmd) if exit_code: raise Exit(exit_code) diff --git a/leverage/modules/terraform.py b/leverage/modules/terraform.py index 7e769c42..c444f813 100644 --- a/leverage/modules/terraform.py +++ b/leverage/modules/terraform.py @@ -1,15 +1,12 @@ -import os import re import click -import dockerpty import hcl2 from click.exceptions import Exit from leverage import logger -from leverage._internals import pass_container -from leverage._internals import pass_state -from leverage._utils import tar_directory, AwsCredsContainer, LiveContainer, ExitError +from leverage._internals import pass_container, pass_state +from leverage._utils import ExitError from leverage.container import TerraformContainer from leverage.container import get_docker_client from leverage.modules.utils import env_var_option, mount_option, auth_mfa, auth_sso @@ -84,14 +81,7 @@ def init(context, tf: TerraformContainer, skip_validation, layers, args): """ Initialize this layer. """ - layers = invoke_for_all_commands(layers, _init, args, skip_validation) - - # now change ownership on all the downloaded modules and providers - for layer in layers: - tf.change_file_ownership(tf.paths.guest_base_path / layer.relative_to(tf.paths.root_dir) / ".terraform") - # and then providers in the cache folder - if tf.paths.tf_cache_dir: - tf.change_file_ownership(tf.paths.tf_cache_dir) + invoke_for_all_commands(layers, _init, args, skip_validation) @terraform.command(context_settings=CONTEXT_SETTINGS) @@ -323,23 +313,9 @@ def _init(tf, args): tf.paths.check_for_layer_location() - with LiveContainer(tf) as container: - # create the .ssh directory - container.exec_run("mkdir -p /root/.ssh") - # copy the entire ~/.ssh/ folder - tar_bytes = tar_directory(tf.paths.home / ".ssh") - # into /root/.ssh - container.put_archive("/root/.ssh/", tar_bytes) - # correct the owner of the files to match with the docker internal user - container.exec_run("chown root:root -R /root/.ssh/") - - with AwsCredsContainer(container, tf): - dockerpty.exec_command( - client=tf.client.api, - container=container.id, - command="terraform init " + " ".join(args), - interactive=bool(int(os.environ.get("LEVERAGE_INTERACTIVE", 1))), - ) + exit_code = tf.start_in_layer("init", *args) + if exit_code: + raise Exit(exit_code) @pass_container diff --git a/leverage/path.py b/leverage/path.py index a6564d87..9c103e6f 100644 --- a/leverage/path.py +++ b/leverage/path.py @@ -186,7 +186,7 @@ def backend_tfvars(self): @property def guest_aws_credentials_dir(self): - return f"/root/tmp/{self.project}" + return str(get_home_path() / Path(self.project)) @property def host_aws_profiles_file(self): diff --git a/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh b/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh new file mode 100755 index 00000000..4b862f40 --- /dev/null +++ b/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------- +# - In a nutshell, what the script does is: +# ----------------------------------------------------------------------------- +# 1. Figure out all the AWS profiles used by Terraform +# 2. For each profile: +# 2.1. Get the role, MFA serial number, and source profile +# 2.2. Figure out the OTP or prompt the user +# 2.3. Assume the role to create temporary credentials +# 2.4. Generate the AWS profiles config files +# 3. Pass the control back to the main process (e.g. Terraform) +# ----------------------------------------------------------------------------- + +set -o errexit +set -o pipefail +set -o nounset + + +# --------------------------- +# Formatting helpers +# --------------------------- +BOLD="\033[1m" +DATE="\033[0;90m" +ERROR="\033[41;37m" +INFO="\033[0;34m" +DEBUG="\033[0;32m" +RESET="\033[0m" + +# --------------------------- +# Helper Functions +# --------------------------- + +# Simple logging functions +function error { log "${ERROR}ERROR${RESET}\t$1" 0; } +function info { log "${INFO}INFO${RESET}\t$1" 1; } +function debug { log "${DEBUG}DEBUG${RESET}\t$1" 2; } +function log { + if [[ $MFA_SCRIPT_LOG_LEVEL -gt "$2" ]]; then + echo -e "${DATE}[$(date +"%H:%M:%S")]${RESET} $1" + fi +} + +# Get the value of an entry in a config file +function get_config { + local config_file=$1 + local config_key=$2 + local config_value=$(grep -oEi "^$config_key\s+=.*\"([a-zA-Z0-9\-]+)\"" $config_file \ + | grep -oEi "\".+\"" \ + | sed 's/\"//g') + echo $config_value +} + +# Get the value of an AWS profile attribute +function get_profile { + local aws_config="$1" + local aws_credentials="$2" + local profile_name="$3" + local profile_key="$4" + local profile_value=$(AWS_CONFIG_FILE=$aws_config; \ + AWS_SHARED_CREDENTIALS_FILE=$aws_credentials; \ + aws configure get "profile.$profile_name.$profile_key") + echo "$profile_value" +} + + +# ----------------------------------------------------------------------------- +# Initialize variables +# ----------------------------------------------------------------------------- +MFA_SCRIPT_LOG_LEVEL=$(printenv MFA_SCRIPT_LOG_LEVEL || echo 2) +BACKEND_CONFIG_FILE=$(printenv BACKEND_CONFIG_FILE) +COMMON_CONFIG_FILE=$(printenv COMMON_CONFIG_FILE) +SRC_AWS_CONFIG_FILE=$(printenv SRC_AWS_CONFIG_FILE) +SRC_AWS_SHARED_CREDENTIALS_FILE=$(printenv SRC_AWS_SHARED_CREDENTIALS_FILE) +TF_AWS_CONFIG_FILE=$(printenv AWS_CONFIG_FILE) +TF_AWS_SHARED_CREDENTIALS_FILE=$(printenv AWS_SHARED_CREDENTIALS_FILE) +AWS_CACHE_DIR=$(printenv AWS_CACHE_DIR || echo /tmp/cache) +AWS_REGION=$(get_config "$BACKEND_CONFIG_FILE" region) +AWS_OUTPUT=json +debug "${BOLD}BACKEND_CONFIG_FILE=${RESET}$BACKEND_CONFIG_FILE" +debug "${BOLD}SRC_AWS_CONFIG_FILE=${RESET}$SRC_AWS_CONFIG_FILE" +debug "${BOLD}SRC_AWS_SHARED_CREDENTIALS_FILE=${RESET}$SRC_AWS_SHARED_CREDENTIALS_FILE" +debug "${BOLD}TF_AWS_CONFIG_FILE=${RESET}$TF_AWS_CONFIG_FILE" +debug "${BOLD}TF_AWS_SHARED_CREDENTIALS_FILE=${RESET}$TF_AWS_SHARED_CREDENTIALS_FILE" +debug "${BOLD}AWS_REGION=${RESET}$AWS_REGION" +debug "${BOLD}AWS_OUTPUT=${RESET}$AWS_OUTPUT" + + +# ----------------------------------------------------------------------------- +# Pre-run Steps +# ----------------------------------------------------------------------------- + +# Make some pre-validations +if [[ ! -f "$SRC_AWS_CONFIG_FILE" ]]; then + error "Unable to find 'AWS Config' file in path: $SRC_AWS_CONFIG_FILE" + exit 90 +fi +if [[ ! -f "$SRC_AWS_SHARED_CREDENTIALS_FILE" ]]; then + error "Unable to find 'AWS Credentials' file in path: $SRC_AWS_SHARED_CREDENTIALS_FILE" + exit 91 +fi + +# Ensure cache credentials dir exists +mkdir -p "$AWS_CACHE_DIR" + + +# ----------------------------------------------------------------------------- +# 1. Figure out all the AWS profiles used by Terraform +# ----------------------------------------------------------------------------- + +# Parse all available profiles in config.tf +RAW_PROFILES=() +if [[ -f "config.tf" ]] && PARSED_PROFILES=$(grep -v "lookup" config.tf | grep -E "^\s+profile"); then + while IFS= read -r line ; do + RAW_PROFILES+=("$(echo "$line" | sed 's/ //g' | sed 's/[\"\$\{\}]//g')") + done <<< "$PARSED_PROFILES" +fi +# Some profiles may be found in local.tf also +if [[ -f "locals.tf" ]] && PARSED_PROFILES=$(grep -E "^\s+profile" locals.tf); then + while IFS= read -r line ; do + RAW_PROFILES+=("$(echo "$line" | sed 's/ //g' | sed 's/[\"\$\{\}]//g')") + done <<< "$PARSED_PROFILES" +fi + +set +e +# Now we need to replace any placeholders in the profiles +PROFILE_VALUE=$(get_config "$BACKEND_CONFIG_FILE" profile) +PROJECT_VALUE=$(get_config "$COMMON_CONFIG_FILE" project) +PROFILES=() +for i in "${RAW_PROFILES[@]}" ; do + TMP_PROFILE=$(echo "$i" | sed "s/profile=//" | sed "s/var.profile/${PROFILE_VALUE}/" | sed "s/var.project/${PROJECT_VALUE}/") + PROFILES+=("$TMP_PROFILE") +done + +# And then we have to remove repeated profiles +UNIQ_PROFILES=($(echo "${PROFILES[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')) +if [[ "${#UNIQ_PROFILES[@]}" -eq 0 ]]; then + error "Unable to find any profiles in config.tf" + exit 100 +fi +info "${BOLD}MFA:${RESET} Found ${#UNIQ_PROFILES[@]} profile/s" + + +# ----------------------------------------------------------------------------- +# 2. For each profile: +# ----------------------------------------------------------------------------- +for i in "${UNIQ_PROFILES[@]}" ; do + info "${BOLD}MFA:${RESET} Attempting to get temporary credentials for profile ${BOLD}$i${RESET}" + + # ----------------------------------------------------------------------------- + # 2.1. Get the role, serial number and source profile from AWS config file + # ----------------------------------------------------------------------------- + if ! MFA_ROLE_ARN=$(AWS_CONFIG_FILE="$SRC_AWS_CONFIG_FILE" && \ + AWS_SHARED_CREDENTIALS_FILE="$SRC_AWS_SHARED_CREDENTIALS_FILE" && \ + aws configure get role_arn --profile "$i" 2>&1); then + if [[ "$MFA_ROLE_ARN" == *"$i"* ]]; then + error "Credentials for profile $i have not been properly configured. Please check your configuration." + error "Check your AWS config file to look for the following profile entry: $i" + error "Check the following link for possible solutions: https://leverage.binbash.co/user-guide/troubleshooting/credentials/" + else + error "Missing 'role_arn'" + fi + exit 150 + fi + debug "${BOLD}MFA_ROLE_ARN=${RESET}$MFA_ROLE_ARN" + MFA_SERIAL_NUMBER=$(get_profile "$SRC_AWS_CONFIG_FILE" "$SRC_AWS_SHARED_CREDENTIALS_FILE" "$i" mfa_serial) + debug "${BOLD}MFA_SERIAL_NUMBER=${RESET}$MFA_SERIAL_NUMBER" + MFA_PROFILE_NAME=$(get_profile "$SRC_AWS_CONFIG_FILE" "$SRC_AWS_SHARED_CREDENTIALS_FILE" "$i" source_profile) + debug "${BOLD}MFA_PROFILE_NAME=${RESET}$MFA_PROFILE_NAME" + # Validate all required fields + if [[ $MFA_SERIAL_NUMBER == "" ]]; then error "Missing 'mfa_serial'" && exit 151; fi + if [[ $MFA_PROFILE_NAME == "" ]]; then error "Missing 'source_profile'" && exit 152; fi + + # ----------------------------------------------------------------------------- + # 2.2. Figure out the OTP or prompt the user + # ----------------------------------------------------------------------------- + # Loop a predefined number of times in case the OTP becomes invalid between + # the time it is generated and the time it is provided to the script + # ----------------------------------------------------------------------------- + MAX_RETRIES=3 + RETRIES_COUNT=0 + OTP_FAILED=true + MFA_DURATION=3600 + TEMP_FILE="$AWS_CACHE_DIR/$i" + debug "${BOLD}TEMP_FILE=${RESET}$TEMP_FILE" + + while [[ $OTP_FAILED == true && $RETRIES_COUNT -lt $MAX_RETRIES ]]; do + + # + # Check if cached credentials exist: look for a file that correspond to + # the current profile + # + if [[ -f "$TEMP_FILE" ]] && EXPIRATION_DATE=$(jq -r '.Credentials.Expiration' "$TEMP_FILE"); then + debug "Found cached credentials in ${BOLD}$TEMP_FILE${RESET}" + + # Get expiration date/timestamp + EXPIRATION_DATE=$(echo "$EXPIRATION_DATE" | sed -e 's/T/ /' | sed -E 's/(Z|\+[0-9]{2}:[0-9]{2})$//') + debug "${BOLD}EXPIRATION_DATE=${RESET}$EXPIRATION_DATE" + EXPIRATION_TS=$(date -d "$EXPIRATION_DATE" +"%s" || date +"%s") + debug "${BOLD}EXPIRATION_TS=${RESET}$EXPIRATION_TS" + + # Compare current timestamp (plus a margin) with the expiration timestamp + CURRENT_TS=$(date +"%s") + CURRENT_TS_PLUS_MARGIN=$(( "$CURRENT_TS" + (30 * 60) )) + debug "${BOLD}CURRENT_TS=${RESET}$CURRENT_TS" + debug "${BOLD}CURRENT_TS_PLUS_MARGIN=${RESET}$CURRENT_TS_PLUS_MARGIN" + if [[ CURRENT_TS_PLUS_MARGIN -lt $EXPIRATION_TS ]]; then + info "${BOLD}MFA:${RESET} Using cached credentials" + + # Pretend the OTP succeeded and exit the while loop + OTP_FAILED=false + break + fi + fi + + # Prompt user for MFA Token + echo -ne "${BOLD}MFA:${RESET} Please type in your OTP: " + if ! MFA_TOKEN_CODE=$(read MFA_TOKEN_CODE && echo "$MFA_TOKEN_CODE"); then + echo + error "Aborted!" + exit 156; + fi + debug "${BOLD}MFA_TOKEN_CODE=${RESET}$MFA_TOKEN_CODE" + + # ----------------------------------------------------------------------------- + # 2.3. Assume the role to generate the temporary credentials + # ----------------------------------------------------------------------------- + MFA_ROLE_SESSION_NAME="$MFA_PROFILE_NAME-temp" + if ! MFA_ASSUME_ROLE_OUTPUT=$(AWS_CONFIG_FILE="$SRC_AWS_CONFIG_FILE" && \ + AWS_SHARED_CREDENTIALS_FILE="$SRC_AWS_SHARED_CREDENTIALS_FILE" && \ + aws sts assume-role \ + --role-arn "$MFA_ROLE_ARN" \ + --serial-number "$MFA_SERIAL_NUMBER" \ + --role-session-name "$MFA_ROLE_SESSION_NAME" \ + --duration-seconds "$MFA_DURATION" \ + --token-code "$MFA_TOKEN_CODE" \ + --profile "$MFA_PROFILE_NAME" 2>&1); then + # Check if STS call failed because of invalid token or user interruption + if [[ $MFA_ASSUME_ROLE_OUTPUT == *"invalid MFA"* ]]; then + OTP_FAILED=true + info "Unable to get valid credentials. Let's try again..." + elif [[ $MFA_ASSUME_ROLE_OUTPUT == *"Invalid length for parameter TokenCode, value:"* ]]; then + OTP_FAILED=true + info "Invalid token length, it must be 6 digits long. Let's try again..." + elif [[ $MFA_ASSUME_ROLE_OUTPUT == *"AccessDenied"* ]]; then + info "Access Denied error!" + exit 161 + elif [[ $MFA_ASSUME_ROLE_OUTPUT == *"An error occurred"* ]]; then + info "An error occurred!" + exit 162 + fi + debug "${BOLD}MFA_ASSUME_ROLE_OUTPUT=${RESET}${MFA_ASSUME_ROLE_OUTPUT}" + else + OTP_FAILED=false + echo "$MFA_ASSUME_ROLE_OUTPUT" > "$TEMP_FILE" + fi + debug "${BOLD}OTP_FAILED=${RESET}$OTP_FAILED" + RETRIES_COUNT=$((RETRIES_COUNT+1)) + debug "${BOLD}RETRIES_COUNT=${RESET}$RETRIES_COUNT" + + done + + # Check if credentials were actually created + if [[ $OTP_FAILED == true ]]; then + error "Unable to get valid credentials after $MAX_RETRIES attempts" + exit 160 + fi + + # ----------------------------------------------------------------------------- + # 2.4. Generate the AWS profiles config files + # ----------------------------------------------------------------------------- + + # Parse id, secret and session from the output above + AWS_ACCESS_KEY_ID=$(jq -r .Credentials.AccessKeyId "$TEMP_FILE") + AWS_SECRET_ACCESS_KEY=$(jq -r .Credentials.SecretAccessKey "$TEMP_FILE") + AWS_SESSION_TOKEN=$(jq -r .Credentials.SessionToken "$TEMP_FILE") + debug "${BOLD}AWS_ACCESS_KEY_ID=${RESET}${AWS_ACCESS_KEY_ID:0:4}**************" + debug "${BOLD}AWS_SECRET_ACCESS_KEY=${RESET}${AWS_SECRET_ACCESS_KEY:0:4}**************" + debug "${BOLD}AWS_SESSION_TOKEN=${RESET}${AWS_SESSION_TOKEN:0:4}**************" + + # Create a profile block in the AWS credentials file using the credentials above + (AWS_CONFIG_FILE=$TF_AWS_CONFIG_FILE; \ + AWS_SHARED_CREDENTIALS_FILE=$TF_AWS_SHARED_CREDENTIALS_FILE; \ + aws configure set "profile.$i.aws_access_key_id" "$AWS_ACCESS_KEY_ID"; \ + aws configure set "profile.$i.aws_secret_access_key" "$AWS_SECRET_ACCESS_KEY"; \ + aws configure set "profile.$i.aws_session_token" "$AWS_SESSION_TOKEN"; \ + aws configure set region "$AWS_REGION"; \ + aws configure set output "$AWS_OUTPUT") + + info "${BOLD}MFA:${RESET} Credentials written succesfully!" +done + +# ----------------------------------------------------------------------------- +# 3. Pass the control back to the main process +# ----------------------------------------------------------------------------- +exec "$@" diff --git a/leverage/scripts/aws-sso/aws-sso-login.sh b/leverage/scripts/aws-sso/aws-sso-login.sh new file mode 100755 index 00000000..9d653330 --- /dev/null +++ b/leverage/scripts/aws-sso/aws-sso-login.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail +set -o nounset + +# ----------------------------------------------------------------------------- +# Formatting helpers +# ----------------------------------------------------------------------------- +BOLD="\033[1m" +DATE="\033[0;90m" +ERROR="\033[41;37m" +INFO="\033[0;34m" +DEBUG="\033[0;32m" +RESET="\033[0m" + +# ----------------------------------------------------------------------------- +# Helper Functions +# ----------------------------------------------------------------------------- +# Simple logging functions +function error { log "${ERROR}ERROR${RESET}\t$1" 0; } +function info { log "${INFO}INFO${RESET}\t$1" 1; } +function debug { log "${DEBUG}DEBUG${RESET}\t$1" 2; } +function log { + if [[ $SCRIPT_LOG_LEVEL -gt $2 ]]; then + printf "%b[%(%T)T]%b %b\n" "$DATE" "$(date +%s)" "$RESET" "$1" + fi +} + +# ----------------------------------------------------------------------------- +# Initialize variables +# ----------------------------------------------------------------------------- +SCRIPT_LOG_LEVEL=${SCRIPT_LOG_LEVEL:-2} +PROJECT=$(hcledit -f "$COMMON_CONFIG_FILE" attribute get project | sed 's/"//g') +SSO_PROFILE_NAME=${SSO_PROFILE_NAME:-$PROJECT-sso} +SSO_CACHE_DIR=${SSO_CACHE_DIR:-/home/leverage/tmp/$PROJECT/sso/cache} +AWS_SSO_CACHE_DIR=/home/leverage/.aws/sso/cache +SSO_TOKEN_FILE_NAME='token' +debug "SCRIPT_LOG_LEVEL=$SCRIPT_LOG_LEVEL" +debug "COMMON_CONFIG_FILE=$COMMON_CONFIG_FILE" +debug "ACCOUNT_CONFIG_FILE=$ACCOUNT_CONFIG_FILE" +debug "BACKEND_CONFIG_FILE=$BACKEND_CONFIG_FILE" +debug "SSO_PROFILE_NAME=$SSO_PROFILE_NAME" +debug "SSO_CACHE_DIR=$SSO_CACHE_DIR" +debug "SSO_TOKEN_FILE_NAME=$SSO_TOKEN_FILE_NAME" + +# Make sure cache dir exists +mkdir -p "$SSO_CACHE_DIR" + +# ----------------------------------------------------------------------------- +# Log in +# ----------------------------------------------------------------------------- +info "Logging in..." +aws sso login --profile "$SSO_PROFILE_NAME" + +# Store token in cache +debug "Caching token" +TOKEN_FILE="$SSO_CACHE_DIR/$SSO_TOKEN_FILE_NAME" +FILES=$(find "$AWS_SSO_CACHE_DIR" -maxdepth 1 -type f -name '*.json' -not -name 'botocore-client*' -exec ls {} \;) +for file in $FILES; +do + if (jq -er '.accessToken' $file >/dev/null); + then + cp $file "$TOKEN_FILE" + break + fi +done +debug "Token Expiration: $BOLD$(jq -r '.expiresAt' "$TOKEN_FILE")$RESET" + +info "${BOLD}Successfully logged in!$RESET" diff --git a/leverage/scripts/aws-sso/aws-sso-logout.sh b/leverage/scripts/aws-sso/aws-sso-logout.sh new file mode 100755 index 00000000..560c54d1 --- /dev/null +++ b/leverage/scripts/aws-sso/aws-sso-logout.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail +set -o nounset + +# ----------------------------------------------------------------------------- +# Formatting helpers +# ----------------------------------------------------------------------------- +BOLD="\033[1m" +DATE="\033[0;90m" +ERROR="\033[41;37m" +INFO="\033[0;34m" +DEBUG="\033[0;32m" +RESET="\033[0m" + +# ----------------------------------------------------------------------------- +# Helper Functions +# ----------------------------------------------------------------------------- +# Simple logging functions +function error { log "${ERROR}ERROR${RESET}\t$1" 0; } +function info { log "${INFO}INFO${RESET}\t$1" 1; } +function debug { log "${DEBUG}DEBUG${RESET}\t$1" 2; } +function log { + if [[ $SCRIPT_LOG_LEVEL -gt $2 ]]; then + printf "%b[%(%T)T]%b %b\n" "$DATE" "$(date +%s)" "$RESET" "$1" + fi +} + +# ----------------------------------------------------------------------------- +# Initialize variables +# ----------------------------------------------------------------------------- +SCRIPT_LOG_LEVEL=${SCRIPT_LOG_LEVEL:-2} +PROJECT=$(hcledit -f "$COMMON_CONFIG_FILE" attribute get project | sed 's/"//g') +SSO_CACHE_DIR=${SSO_CACHE_DIR:-/home/leverage/tmp/$PROJECT/sso/cache} +debug "SCRIPT_LOG_LEVEL=$SCRIPT_LOG_LEVEL" +debug "AWS_SHARED_CREDENTIALS_FILE=$AWS_SHARED_CREDENTIALS_FILE" +debug "AWS_CONFIG_FILE=$AWS_CONFIG_FILE" +debug "SSO_CACHE_DIR=$SSO_CACHE_DIR" +debug "PROJECT=$PROJECT" + +# ----------------------------------------------------------------------------- +# Log out +# ----------------------------------------------------------------------------- +aws sso logout + +# Clear sso token +debug "Removing SSO Tokens." +rm -f $SSO_CACHE_DIR/* + +# Clear AWS CLI credentials +debug "Wiping current SSO credentials." +awk '/^\[/{if($0~/profile '"$PROJECT-sso"'/ || $0 == "[default]"){found=1}else{found=""}} found' "$AWS_CONFIG_FILE" > tempconf && mv tempconf "$AWS_CONFIG_FILE" + +rm -f "$AWS_SHARED_CREDENTIALS_FILE" + +debug "All credentials wiped!" diff --git a/pyproject.toml b/pyproject.toml index 41a0ce93..1e829a4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,8 @@ classifiers = [ packages = [ { include = "leverage" }, { include = "leverage/modules" }, - { include = "leverage/containers" } + { include = "leverage/containers" }, + { include = "leverage/scripts" } ] [tool.poetry.dependencies] diff --git a/tests/test_containers/test_leverage.py b/tests/test_containers/test_leverage.py index 01596a3a..35eaaf53 100644 --- a/tests/test_containers/test_leverage.py +++ b/tests/test_containers/test_leverage.py @@ -17,15 +17,6 @@ def test_change_ownership_non_recursive_cmd(leverage_container, fake_os_user): assert leverage_container.change_ownership_cmd("/tmp/file.txt", recursive=False) == "chown 1234:5678 /tmp/file.txt" -def test_change_file_ownership(leverage_container, fake_os_user): - leverage_container.change_file_ownership("/tmp/file.txt", recursive=False) - container_args = leverage_container.client.api.create_container.call_args_list[0][1] - - assert container_args["command"] == "chown 1234:5678 /tmp/file.txt" - # we use chown directly so no entrypoint must be set - assert container_args["entrypoint"] == "" - - def test_mounts(muted_click_context): container = container_fixture_factory( LeverageContainer, mounts=(("/usr/bin", "/usr/bin"), ("/tmp/file.txt", "/tmp/file.txt")) From 331d049be6f8a9b973668d60c5369e8a89c2863c Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 25 May 2024 17:27:04 -0300 Subject: [PATCH 02/15] path fixes --- leverage/containers/kubectl.py | 2 +- leverage/path.py | 2 +- tests/test_modules/test_terraform.py | 48 ++++++++++------------------ 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/leverage/containers/kubectl.py b/leverage/containers/kubectl.py index 9006528f..6f45437e 100644 --- a/leverage/containers/kubectl.py +++ b/leverage/containers/kubectl.py @@ -12,7 +12,7 @@ class KubeCtlContainer(TerraformContainer): """Container specifically tailored to run kubectl commands.""" KUBECTL_CLI_BINARY = "/usr/local/bin/kubectl" - KUBECTL_CONFIG_PATH = Path("/root/.kube") + KUBECTL_CONFIG_PATH = Path("/home/leverage/.kube") KUBECTL_CONFIG_FILE = KUBECTL_CONFIG_PATH / Path("config") def __init__(self, client): diff --git a/leverage/path.py b/leverage/path.py index 9c103e6f..4e862275 100644 --- a/leverage/path.py +++ b/leverage/path.py @@ -186,7 +186,7 @@ def backend_tfvars(self): @property def guest_aws_credentials_dir(self): - return str(get_home_path() / Path(self.project)) + return str("/home/leverage/tmp" / Path(self.project)) @property def host_aws_profiles_file(self): diff --git a/tests/test_modules/test_terraform.py b/tests/test_modules/test_terraform.py index d8030bcb..9e8c2a06 100644 --- a/tests/test_modules/test_terraform.py +++ b/tests/test_modules/test_terraform.py @@ -4,7 +4,6 @@ from click import get_current_context from leverage._internals import State -from leverage._utils import AwsCredsContainer from leverage.container import TerraformContainer from leverage.modules.terraform import _init from tests.test_containers import container_fixture_factory @@ -22,36 +21,23 @@ def terraform_container(muted_click_context): # assume we are on a valid location with patch.object(tf_container.paths, "check_for_layer_location", Mock()): - # assume we have valid credentials - with patch.object(AwsCredsContainer, "__enter__", Mock()): - yield tf_container - - -def test_init(terraform_container): - """ - Test happy path. - """ - live_container = Mock() - with patch("leverage._utils.LiveContainer.__enter__", return_value=live_container): - with patch("dockerpty.exec_command") as mocked_pty: - _init([]) - - assert live_container.exec_run.call_args_list[0].args[0] == "mkdir -p /root/.ssh" - assert live_container.exec_run.call_args_list[1].args[0] == "chown root:root -R /root/.ssh/" - assert ( - mocked_pty.call_args_list[0].kwargs["command"] - == f"terraform init -backend-config=/project/./config/backend.tfvars" - ) - - -def test_init_with_args(terraform_container): + yield tf_container + + +@pytest.mark.parametrize( + "args, expected_value", + [ + ([], ["-backend-config=/project/./config/backend.tfvars"]), + (["-migrate-state"], ["-migrate-state", "-backend-config=/project/./config/backend.tfvars"]), + (["-r1", "-r2"], ["-r1", "-r2", "-backend-config=/project/./config/backend.tfvars"]), + ], +) +def test_init_arguments(terraform_container, args, expected_value): """ - Test tf init with arguments. + Test that the arguments for the init command are prepared correctly. """ - with patch("dockerpty.exec_command") as mocked_pty: - _init(["-migrate-state"]) + with patch.object(terraform_container, "start_in_layer", return_value=0) as mocked: + _init(args) - assert ( - mocked_pty.call_args_list[0].kwargs["command"] - == f"terraform init -migrate-state -backend-config=/project/./config/backend.tfvars" - ) + assert mocked.call_args_list[0][0][0] == "init" + assert " ".join(mocked.call_args_list[0][0][1:]) == " ".join(expected_value) From 5e5b7462721eb6427ba0d7e17856b350a0c80d96 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 25 May 2024 17:27:19 -0300 Subject: [PATCH 03/15] update tests --- tests/test_containers/test_aws.py | 6 +----- tests/test_containers/test_kubectl.py | 21 +++++++++++---------- tests/test_containers/test_leverage.py | 8 -------- tests/test_containers/test_terraform.py | 2 +- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/tests/test_containers/test_aws.py b/tests/test_containers/test_aws.py index 4532109d..f6d1e8ba 100644 --- a/tests/test_containers/test_aws.py +++ b/tests/test_containers/test_aws.py @@ -55,12 +55,8 @@ def test_sso_login(mocked_new_tab, aws_container, fake_os_user, propagate_logs, container_args = aws_container.client.api.create_container.call_args_list[0][1] # make sure we: point to the correct script - assert container_args["command"] == "/root/scripts/aws-sso/aws-sso-login.sh" + assert container_args["command"] == "/opt/scripts/aws-sso/aws-sso-login.sh" # the browser tab points to the correct code and the correct region assert mocked_new_tab.call_args[0][0] == "https://device.sso.us-east-1.amazonaws.com/?user_code=TEST-CODE" - # ownership of the files was given back - container_args = aws_container.client.api.create_container.call_args_list[1][1] - assert container_args["command"] == "chown 1234:5678 -R /root/tmp/test" - assert mocked_new_tab.call_args[0][0] == test_link # and the fallback method is printed assert caplog.messages[0] == aws_container.FALLBACK_LINK_MSG.format(link=test_link) diff --git a/tests/test_containers/test_kubectl.py b/tests/test_containers/test_kubectl.py index 8435e4ee..51895f10 100644 --- a/tests/test_containers/test_kubectl.py +++ b/tests/test_containers/test_kubectl.py @@ -57,14 +57,15 @@ def test_start_shell(kubectl_container): assert container_args["entrypoint"] == "" # make sure we are pointing to the AWS credentials - assert container_args["environment"]["AWS_CONFIG_FILE"] == "/root/tmp/test/config" - assert container_args["environment"]["AWS_SHARED_CREDENTIALS_FILE"] == "/root/tmp/test/credentials" + assert container_args["environment"]["AWS_CONFIG_FILE"] == "/home/leverage/tmp/test/config" + assert container_args["environment"]["AWS_SHARED_CREDENTIALS_FILE"] == "/home/leverage/tmp/test/credentials" # make sure we mounted the .kube config folder - assert next(m for m in container_args["host_config"]["Mounts"] if m["Target"] == "/root/.kube") + print(container_args["host_config"]) + assert next(m for m in container_args["host_config"]["Mounts"] if m["Target"] == "/home/leverage/.kube") # and the aws config folder - assert next(m for m in container_args["host_config"]["Mounts"] if m["Target"] == "/root/tmp/test") + assert next(m for m in container_args["host_config"]["Mounts"] if m["Target"] == "/home/leverage/tmp/test") # don't rely on the filesystem @@ -75,7 +76,7 @@ def test_configure(kubectl_container, fake_os_user): with patch.object(kubectl_container, "_start", return_value=0) as mock_start: kubectl_container.configure() - assert mock_start.call_args[0][0] == f'bash -c "{AWS_EKS_UPDATE_KUBECONFIG} && chown 1234:5678 /root/.kube/config"' + assert mock_start.call_args[0][0] == AWS_EKS_UPDATE_KUBECONFIG ##################### @@ -96,11 +97,11 @@ def test_start_shell_mfa(kubectl_container): # we want a shell, so -> /bin/bash with no entrypoint assert container_args["command"] == "/bin/bash" - assert container_args["entrypoint"] == "/root/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " + assert container_args["entrypoint"] == "/opt/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " # make sure we are pointing to the right AWS credentials: /.aws/ folder for MFA - assert container_args["environment"]["AWS_CONFIG_FILE"] == "/root/.aws/test/config" - assert container_args["environment"]["AWS_SHARED_CREDENTIALS_FILE"] == "/root/.aws/test/credentials" + assert container_args["environment"]["AWS_CONFIG_FILE"] == "/home/leverage/.aws/test/config" + assert container_args["environment"]["AWS_SHARED_CREDENTIALS_FILE"] == "/home/leverage/.aws/test/credentials" @patch("leverage.container.refresh_layer_credentials") @@ -118,5 +119,5 @@ def test_start_shell_sso(mock_refresh, kubectl_container): assert mock_refresh.called_once # make sure we are pointing to the right AWS credentials: /tmp/ folder for SSO - assert container_args["environment"]["AWS_CONFIG_FILE"] == "/root/tmp/test/config" - assert container_args["environment"]["AWS_SHARED_CREDENTIALS_FILE"] == "/root/tmp/test/credentials" + assert container_args["environment"]["AWS_CONFIG_FILE"] == "/home/leverage/tmp/test/config" + assert container_args["environment"]["AWS_SHARED_CREDENTIALS_FILE"] == "/home/leverage/tmp/test/credentials" diff --git a/tests/test_containers/test_leverage.py b/tests/test_containers/test_leverage.py index 35eaaf53..8eca98dc 100644 --- a/tests/test_containers/test_leverage.py +++ b/tests/test_containers/test_leverage.py @@ -9,14 +9,6 @@ def leverage_container(muted_click_context): return container_fixture_factory(LeverageContainer) -def test_change_ownership_cmd(leverage_container, fake_os_user): - assert leverage_container.change_ownership_cmd("/tmp/", recursive=True) == "chown 1234:5678 -R /tmp/" - - -def test_change_ownership_non_recursive_cmd(leverage_container, fake_os_user): - assert leverage_container.change_ownership_cmd("/tmp/file.txt", recursive=False) == "chown 1234:5678 /tmp/file.txt" - - def test_mounts(muted_click_context): container = container_fixture_factory( LeverageContainer, mounts=(("/usr/bin", "/usr/bin"), ("/tmp/file.txt", "/tmp/file.txt")) diff --git a/tests/test_containers/test_terraform.py b/tests/test_containers/test_terraform.py index 4c91e2de..1d61b658 100644 --- a/tests/test_containers/test_terraform.py +++ b/tests/test_containers/test_terraform.py @@ -51,7 +51,7 @@ def test_auth_method_mfa_enabled(terraform_container): terraform_container.sso_enabled = False terraform_container.mfa_enabled = True - assert terraform_container.auth_method() == "/root/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " + assert terraform_container.auth_method() == "/opt/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " def test_auth_method_else(terraform_container): From 6f3b7c75692474d71d565610f76e26857854bb20 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 25 May 2024 17:41:39 -0300 Subject: [PATCH 04/15] make it a package so poetry doesn't complain --- leverage/scripts/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 leverage/scripts/__init__.py diff --git a/leverage/scripts/__init__.py b/leverage/scripts/__init__.py new file mode 100644 index 00000000..e69de29b From 296f1cd6920a01d7d43dd39bbcc667853944da8e Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Thu, 30 May 2024 19:18:12 -0300 Subject: [PATCH 05/15] user newer image (that supports groupadd/useradd binaries) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6d048607..a1ed27d3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: help build LEVERAGE_TESTING_IMAGE := binbash/leverage-cli-testing LEVERAGE_TESTING_TAG := 2.5.0 -LEVERAGE_IMAGE_TAG := 1.2.7-0.0.5 +LEVERAGE_IMAGE_TAG := 1.2.7-0.1.14 PYPROJECT_FILE := pyproject.toml INIT_FILE := leverage/__init__.py PLACEHOLDER := 0.0.0 From 91180b3194d1014d212b64f62996888f15f1ccc5 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 1 Jun 2024 18:48:32 -0300 Subject: [PATCH 06/15] tweaks --- leverage/container.py | 30 ++++++++++++++---------------- leverage/path.py | 5 +++-- tests/test_path.py | 2 +- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/leverage/container.py b/leverage/container.py index c12c2133..63de8c3d 100644 --- a/leverage/container.py +++ b/leverage/container.py @@ -1,7 +1,6 @@ import json import os import re -import timeit import webbrowser from io import BytesIO from pathlib import Path @@ -13,15 +12,14 @@ import dockerpty from configupdater import ConfigUpdater from docker import DockerClient -from docker.errors import APIError, NotFound +from docker.errors import APIError from docker.types import Mount -from typing import Tuple, Union +from typing import Tuple -from leverage import __toolbox_version__ from leverage import logger from leverage._utils import AwsCredsEntryPoint, CustomEntryPoint, ExitError, ContainerSession from leverage.modules.auth import refresh_layer_credentials -from leverage.logger import console, raw_logger +from leverage.logger import raw_logger from leverage.logger import get_script_log_level from leverage.path import PathsHandler from leverage.conf import load as load_env @@ -69,6 +67,7 @@ class LeverageContainer: LEVERAGE_IMAGE = "binbash/leverage-toolbox" SHELL = "/bin/bash" + CONTAINER_USER = "leverage" def __init__(self, client, mounts: tuple = None, env_vars: dict = None): """Project related paths are determined and stored. Project configuration is loaded. @@ -80,7 +79,7 @@ def __init__(self, client, mounts: tuple = None, env_vars: dict = None): # Load configs self.env_conf = load_env() - self.paths = PathsHandler(self.env_conf) + self.paths = PathsHandler(self.env_conf, self.CONTAINER_USER) self.project = self.paths.project # Set image to use @@ -141,17 +140,13 @@ def region(self): raise ExitError(1, f"No valid region could be found at: {self.paths.cwd.as_posix()}") @property - def local_image(self): + def local_image(self) -> BytesIO: + """Return the local image that will be built, as a file-like object.""" return BytesIO( - f""" + """ ARG IMAGE_TAG FROM binbash/leverage-toolbox:$IMAGE_TAG - - # Needed as is mounted later on - #RUN mkdir /root/.ssh - # Needed for git to run propertly - #RUN touch /root/.gitconfig - + ARG UNAME ARG UID ARG GID @@ -164,11 +159,14 @@ def local_image(self): ) def ensure_image(self): + """ + Make sure the required local Docker image is available in the system. If not, build it. + If the image already exists, re-build it so changes in the arguments can take effect. + """ logger.info(f"Checking for local docker image, tag: {self.image_tag}...") - """Make sure the required local Docker image is available in the system. If not, pull it from registry.""" build_args = { "IMAGE_TAG": self.image_tag, - "UNAME": "leverage", # TODO: what is this exactly? + "UNAME": self.CONTAINER_USER, "GID": str(os.getgid()), "UID": str(os.getuid()), } diff --git a/leverage/path.py b/leverage/path.py index 4e862275..ee622fc1 100644 --- a/leverage/path.py +++ b/leverage/path.py @@ -134,7 +134,8 @@ class PathsHandler: ACCOUNT_TF_VARS = "account.tfvars" BACKEND_TF_VARS = "backend.tfvars" - def __init__(self, env_conf): + def __init__(self, env_conf: dict, container_user: str): + self.container_user = container_user self.home = Path.home() self.cwd = Path.cwd() try: @@ -186,7 +187,7 @@ def backend_tfvars(self): @property def guest_aws_credentials_dir(self): - return str("/home/leverage/tmp" / Path(self.project)) + return str(f"/home/{self.container_user}/tmp" / Path(self.project)) @property def host_aws_profiles_file(self): diff --git a/tests/test_path.py b/tests/test_path.py index c90a4c8f..0038fe0a 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -113,7 +113,7 @@ def test_check_for_cluster_layer(muted_click_context, propagate_logs, caplog): """ Test that if we are not on a cluster layer, we raise an error. """ - paths = PathsHandler({"PROJECT": "test"}) + paths = PathsHandler({"PROJECT": "test"}, "leverage") with patch.object(paths, "check_for_layer_location"): # assume parent method is already tested with pytest.raises(ExitError): paths.cwd = Path("/random") From ff8b995adc7cb3808a31f6528a8fef8600e137ce Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 8 Jun 2024 11:01:25 -0300 Subject: [PATCH 07/15] verify if image already exists locally --- leverage/container.py | 19 ++++++++++++++++--- leverage/containers/kubectl.py | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/leverage/container.py b/leverage/container.py index 63de8c3d..3cf83145 100644 --- a/leverage/container.py +++ b/leverage/container.py @@ -95,7 +95,7 @@ def __init__(self, client, mounts: tuple = None, env_vars: dict = None): mounts = [Mount(source=source, target=target, type="bind") for source, target in mounts] if mounts else [] self.host_config = self.client.api.create_host_config(security_opt=["label:disable"], mounts=mounts) self.container_config = { - "image": f"{self.image}:local", + "image": f"{self.image}:{self.local_image_tag}", "command": "", "stdin_open": True, "environment": env_vars or {}, @@ -139,6 +139,10 @@ def region(self): raise ExitError(1, f"No valid region could be found at: {self.paths.cwd.as_posix()}") + @property + def local_image_tag(self): + return f"{self.image_tag}-{os.getgid()}-{os.getuid()}" + @property def local_image(self) -> BytesIO: """Return the local image that will be built, as a file-like object.""" @@ -163,7 +167,16 @@ def ensure_image(self): Make sure the required local Docker image is available in the system. If not, build it. If the image already exists, re-build it so changes in the arguments can take effect. """ - logger.info(f"Checking for local docker image, tag: {self.image_tag}...") + logger.info(f"Checking for local docker image, tag: {self.local_image_tag}...") + image_name = f"{self.image}:{self.local_image_tag}" + + # check first is our image is already available locally + found_image = self.client.api.images(f"{self.image}:{self.local_image_tag}") + if found_image: + logger.info("[green]✔ OK[/green]\n") + return + + logger.info(f"Image not found, building it...") build_args = { "IMAGE_TAG": self.image_tag, "UNAME": self.CONTAINER_USER, @@ -173,7 +186,7 @@ def ensure_image(self): stream = self.client.api.build( fileobj=self.local_image, - tag=f"binbash/leverage-toolbox:local", + tag=image_name, pull=True, buildargs=build_args, decode=True, diff --git a/leverage/containers/kubectl.py b/leverage/containers/kubectl.py index 6f45437e..d2cb2dc2 100644 --- a/leverage/containers/kubectl.py +++ b/leverage/containers/kubectl.py @@ -12,7 +12,7 @@ class KubeCtlContainer(TerraformContainer): """Container specifically tailored to run kubectl commands.""" KUBECTL_CLI_BINARY = "/usr/local/bin/kubectl" - KUBECTL_CONFIG_PATH = Path("/home/leverage/.kube") + KUBECTL_CONFIG_PATH = Path(f"/home/{TerraformContainer.CONTAINER_USER}/.kube") KUBECTL_CONFIG_FILE = KUBECTL_CONFIG_PATH / Path("config") def __init__(self, client): From e62e7ddd9d7e89a37ded41fc25e8f4829f10d3b8 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 22 Jun 2024 13:55:50 -0300 Subject: [PATCH 08/15] fix coverage path --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index ae7f4872..9c545550 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -34,7 +34,7 @@ jobs: run: | echo "[INFO] Running unit tests and generate coverage report" export PATH="${PATH}:${HOME}/.poetry/bin" - poetry run pytest --verbose --cov=./ --cov-report=xml + poetry run pytest --verbose --cov=./leverage/ --cov-report=xml shell: bash - name: Report Coveralls From a13270714e772f0da382f2e4c0bd57412f783958 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 22 Jun 2024 18:13:12 -0300 Subject: [PATCH 09/15] ensure_image unit tests --- tests/test_containers/test_leverage.py | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_containers/test_leverage.py b/tests/test_containers/test_leverage.py index 8eca98dc..5ccf74f6 100644 --- a/tests/test_containers/test_leverage.py +++ b/tests/test_containers/test_leverage.py @@ -1,5 +1,8 @@ +from unittest import mock + import pytest +from leverage._utils import ExitError from leverage.container import LeverageContainer from tests.test_containers import container_fixture_factory @@ -26,3 +29,51 @@ def test_env_vars(muted_click_context): container_args = container.client.api.create_container.call_args_list[0][1] assert container_args["environment"] == {"foo": "bar", "testing": 123} + + +def test_ensure_image_already_available(leverage_container: LeverageContainer, fake_os_user, propagate_logs, caplog): + """ + Test that the local image is not re-built when is already available locally. + """ + # already available + with mock.patch.object(leverage_container.client.api, "images", return_value=True) as mocked_images: + leverage_container.ensure_image() + + assert mocked_images.call_args_list[0][0][0] == "binbash/leverage-toolbox:test-5678-1234" + assert caplog.messages[0] == "Checking for local docker image, tag: test-5678-1234..." + assert "OK" in caplog.messages[1] + + +def test_ensure_image_failed(leverage_container: LeverageContainer, fake_os_user, propagate_logs, caplog): + """ + Test that we get a friendly error if re-building the image fails. + """ + build_response = [{"errorDetail": "Something went wrong"}] + # not available + with mock.patch.object(leverage_container.client.api, "images", return_value=False): + with mock.patch.object(leverage_container.client.api, "build", return_value=build_response) as mocked_build: + with pytest.raises(ExitError, match="Failed"): + leverage_container.ensure_image() + + assert caplog.messages[1] == "Image not found, building it..." + assert caplog.messages[2] == "Failed building local image: Something went wrong" + + +def test_ensure_image(leverage_container: LeverageContainer, fake_os_user, propagate_logs, caplog): + """ + Test that the local image is not available locally, thus it has to be re-built. + """ + build_response = [{"stream": "Successfully built"}] + # not available + with mock.patch.object(leverage_container.client.api, "images", return_value=False): + with mock.patch.object(leverage_container.client.api, "build", return_value=build_response) as mocked_build: + leverage_container.ensure_image() + + assert mocked_build.call_args_list[0][1]["buildargs"] == { + "GID": "5678", + "UID": "1234", + "UNAME": "leverage", + "IMAGE_TAG": "test", + } + assert caplog.messages[1] == "Image not found, building it..." + assert "OK" in caplog.messages[2] From 67ddf46af659e7381402e5d470f0360ab8cfe18a Mon Sep 17 00:00:00 2001 From: Exequiel Barrirero Date: Mon, 26 Aug 2024 16:18:52 -0300 Subject: [PATCH 10/15] Feature/support python 3.12 (#279) * adding python 3.12 in pyproject.toml * autogenerating python 3.12 in poetry.lock --- poetry.lock | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index f09077b6..dceb7ddf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "astroid" @@ -1737,5 +1737,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = "~3.8 || ~3.9 || ~3.10 || ~3.11" -content-hash = "5d56ff348ace7187971f87c7052f0709eb4bcd4730a3986fffec2d10cd0b4f3a" +python-versions = "~3.8 || ~3.9 || ~3.10 || ~3.11 || ~3.12" +content-hash = "474c61b1fdbd18c16f11d8ec2df9254d656efc87d51d8f4ab02c40c10988aef7" diff --git a/pyproject.toml b/pyproject.toml index ca1490ca..42f677ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ packages = [ ] [tool.poetry.dependencies] -python = "~3.8 || ~3.9 || ~3.10 || ~3.11" +python = "~3.8 || ~3.9 || ~3.10 || ~3.11 || ~3.12" click = "8.0.1" yaenv = "1.4.1" "ruamel.yaml" = "0.17.10" From 7bdefe5be9cfff61d8beaaaacc6252549727cb92 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sun, 8 Sep 2024 16:29:05 -0300 Subject: [PATCH 11/15] keep using scripts from toolbox --- leverage/container.py | 34 +- leverage/scripts/__init__.py | 0 .../scripts/aws-mfa/aws-mfa-entrypoint.sh | 296 ------------------ leverage/scripts/aws-sso/aws-sso-login.sh | 70 ----- leverage/scripts/aws-sso/aws-sso-logout.sh | 57 ---- 5 files changed, 12 insertions(+), 445 deletions(-) delete mode 100644 leverage/scripts/__init__.py delete mode 100755 leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh delete mode 100755 leverage/scripts/aws-sso/aws-sso-login.sh delete mode 100755 leverage/scripts/aws-sso/aws-sso-logout.sh diff --git a/leverage/container.py b/leverage/container.py index 3cf83145..0ae58bdc 100644 --- a/leverage/container.py +++ b/leverage/container.py @@ -3,7 +3,6 @@ import re import webbrowser from io import BytesIO -from pathlib import Path from datetime import datetime from time import sleep @@ -156,6 +155,7 @@ def local_image(self) -> BytesIO: ARG GID RUN groupadd -g $GID -o $UNAME RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME + RUN chown -R $UID:$GID /home/leverage USER $UNAME """.encode( "utf-8" @@ -310,8 +310,8 @@ def docker_logs(self, container): class SSOContainer(LeverageContainer): # SSO scripts - AWS_SSO_LOGIN_SCRIPT = "/opt/scripts/aws-sso/aws-sso-login.sh" - AWS_SSO_LOGOUT_SCRIPT = "/opt/scripts/aws-sso/aws-sso-logout.sh" + AWS_SSO_LOGIN_SCRIPT = "/home/leverage/scripts/aws-sso/aws-sso-login.sh" + AWS_SSO_LOGOUT_SCRIPT = "/home/leverage/scripts/aws-sso/aws-sso-logout.sh" # SSO constants AWS_SSO_LOGIN_URL = "https://device.sso.{region}.amazonaws.com/?user_code={user_code}" @@ -319,14 +319,6 @@ class SSOContainer(LeverageContainer): AWS_SSO_CODE_ATTEMPTS = 10 FALLBACK_LINK_MSG = "Opening the browser... if it fails, open this link in your browser:\n{link}" - def __init__(self, client, mounts=None, env_vars=None): - super().__init__(client, mounts=mounts, env_vars=env_vars) - self.mounts.extend( - [ - Mount(source=(Path(__file__).parent / "scripts").as_posix(), target="/opt/scripts", type="bind"), - ] - ) - def get_sso_access_token(self): with open(self.paths.sso_token_file) as token_file: return json.loads(token_file.read())["accessToken"] @@ -403,16 +395,14 @@ def __init__(self, client): "SCRIPT_LOG_LEVEL": get_script_log_level(), } self.entrypoint = self.AWS_CLI_BINARY - self.mounts.extend( - [ - Mount(source=self.paths.root_dir.as_posix(), target=self.paths.guest_base_path, type="bind"), - Mount( - source=self.paths.host_aws_credentials_dir.as_posix(), - target=self.paths.guest_aws_credentials_dir, - type="bind", - ), - ] - ) + self.mounts = [ + Mount(source=self.paths.root_dir.as_posix(), target=self.paths.guest_base_path, type="bind"), + Mount( + source=self.paths.host_aws_credentials_dir.as_posix(), + target=self.paths.guest_aws_credentials_dir, + type="bind", + ), + ] logger.debug(f"[bold cyan]Container configuration:[/bold cyan]\n{json.dumps(self.container_config, indent=2)}") @@ -448,7 +438,7 @@ class TerraformContainer(SSOContainer): TF_BINARY = "/bin/terraform" - TF_MFA_ENTRYPOINT = "/opt/scripts/aws-mfa/aws-mfa-entrypoint.sh" + TF_MFA_ENTRYPOINT = "/home/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh" def __init__(self, client, mounts=None, env_vars=None): super().__init__(client, mounts=mounts, env_vars=env_vars) diff --git a/leverage/scripts/__init__.py b/leverage/scripts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh b/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh deleted file mode 100755 index 4b862f40..00000000 --- a/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env bash - -# ----------------------------------------------------------------------------- -# - In a nutshell, what the script does is: -# ----------------------------------------------------------------------------- -# 1. Figure out all the AWS profiles used by Terraform -# 2. For each profile: -# 2.1. Get the role, MFA serial number, and source profile -# 2.2. Figure out the OTP or prompt the user -# 2.3. Assume the role to create temporary credentials -# 2.4. Generate the AWS profiles config files -# 3. Pass the control back to the main process (e.g. Terraform) -# ----------------------------------------------------------------------------- - -set -o errexit -set -o pipefail -set -o nounset - - -# --------------------------- -# Formatting helpers -# --------------------------- -BOLD="\033[1m" -DATE="\033[0;90m" -ERROR="\033[41;37m" -INFO="\033[0;34m" -DEBUG="\033[0;32m" -RESET="\033[0m" - -# --------------------------- -# Helper Functions -# --------------------------- - -# Simple logging functions -function error { log "${ERROR}ERROR${RESET}\t$1" 0; } -function info { log "${INFO}INFO${RESET}\t$1" 1; } -function debug { log "${DEBUG}DEBUG${RESET}\t$1" 2; } -function log { - if [[ $MFA_SCRIPT_LOG_LEVEL -gt "$2" ]]; then - echo -e "${DATE}[$(date +"%H:%M:%S")]${RESET} $1" - fi -} - -# Get the value of an entry in a config file -function get_config { - local config_file=$1 - local config_key=$2 - local config_value=$(grep -oEi "^$config_key\s+=.*\"([a-zA-Z0-9\-]+)\"" $config_file \ - | grep -oEi "\".+\"" \ - | sed 's/\"//g') - echo $config_value -} - -# Get the value of an AWS profile attribute -function get_profile { - local aws_config="$1" - local aws_credentials="$2" - local profile_name="$3" - local profile_key="$4" - local profile_value=$(AWS_CONFIG_FILE=$aws_config; \ - AWS_SHARED_CREDENTIALS_FILE=$aws_credentials; \ - aws configure get "profile.$profile_name.$profile_key") - echo "$profile_value" -} - - -# ----------------------------------------------------------------------------- -# Initialize variables -# ----------------------------------------------------------------------------- -MFA_SCRIPT_LOG_LEVEL=$(printenv MFA_SCRIPT_LOG_LEVEL || echo 2) -BACKEND_CONFIG_FILE=$(printenv BACKEND_CONFIG_FILE) -COMMON_CONFIG_FILE=$(printenv COMMON_CONFIG_FILE) -SRC_AWS_CONFIG_FILE=$(printenv SRC_AWS_CONFIG_FILE) -SRC_AWS_SHARED_CREDENTIALS_FILE=$(printenv SRC_AWS_SHARED_CREDENTIALS_FILE) -TF_AWS_CONFIG_FILE=$(printenv AWS_CONFIG_FILE) -TF_AWS_SHARED_CREDENTIALS_FILE=$(printenv AWS_SHARED_CREDENTIALS_FILE) -AWS_CACHE_DIR=$(printenv AWS_CACHE_DIR || echo /tmp/cache) -AWS_REGION=$(get_config "$BACKEND_CONFIG_FILE" region) -AWS_OUTPUT=json -debug "${BOLD}BACKEND_CONFIG_FILE=${RESET}$BACKEND_CONFIG_FILE" -debug "${BOLD}SRC_AWS_CONFIG_FILE=${RESET}$SRC_AWS_CONFIG_FILE" -debug "${BOLD}SRC_AWS_SHARED_CREDENTIALS_FILE=${RESET}$SRC_AWS_SHARED_CREDENTIALS_FILE" -debug "${BOLD}TF_AWS_CONFIG_FILE=${RESET}$TF_AWS_CONFIG_FILE" -debug "${BOLD}TF_AWS_SHARED_CREDENTIALS_FILE=${RESET}$TF_AWS_SHARED_CREDENTIALS_FILE" -debug "${BOLD}AWS_REGION=${RESET}$AWS_REGION" -debug "${BOLD}AWS_OUTPUT=${RESET}$AWS_OUTPUT" - - -# ----------------------------------------------------------------------------- -# Pre-run Steps -# ----------------------------------------------------------------------------- - -# Make some pre-validations -if [[ ! -f "$SRC_AWS_CONFIG_FILE" ]]; then - error "Unable to find 'AWS Config' file in path: $SRC_AWS_CONFIG_FILE" - exit 90 -fi -if [[ ! -f "$SRC_AWS_SHARED_CREDENTIALS_FILE" ]]; then - error "Unable to find 'AWS Credentials' file in path: $SRC_AWS_SHARED_CREDENTIALS_FILE" - exit 91 -fi - -# Ensure cache credentials dir exists -mkdir -p "$AWS_CACHE_DIR" - - -# ----------------------------------------------------------------------------- -# 1. Figure out all the AWS profiles used by Terraform -# ----------------------------------------------------------------------------- - -# Parse all available profiles in config.tf -RAW_PROFILES=() -if [[ -f "config.tf" ]] && PARSED_PROFILES=$(grep -v "lookup" config.tf | grep -E "^\s+profile"); then - while IFS= read -r line ; do - RAW_PROFILES+=("$(echo "$line" | sed 's/ //g' | sed 's/[\"\$\{\}]//g')") - done <<< "$PARSED_PROFILES" -fi -# Some profiles may be found in local.tf also -if [[ -f "locals.tf" ]] && PARSED_PROFILES=$(grep -E "^\s+profile" locals.tf); then - while IFS= read -r line ; do - RAW_PROFILES+=("$(echo "$line" | sed 's/ //g' | sed 's/[\"\$\{\}]//g')") - done <<< "$PARSED_PROFILES" -fi - -set +e -# Now we need to replace any placeholders in the profiles -PROFILE_VALUE=$(get_config "$BACKEND_CONFIG_FILE" profile) -PROJECT_VALUE=$(get_config "$COMMON_CONFIG_FILE" project) -PROFILES=() -for i in "${RAW_PROFILES[@]}" ; do - TMP_PROFILE=$(echo "$i" | sed "s/profile=//" | sed "s/var.profile/${PROFILE_VALUE}/" | sed "s/var.project/${PROJECT_VALUE}/") - PROFILES+=("$TMP_PROFILE") -done - -# And then we have to remove repeated profiles -UNIQ_PROFILES=($(echo "${PROFILES[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')) -if [[ "${#UNIQ_PROFILES[@]}" -eq 0 ]]; then - error "Unable to find any profiles in config.tf" - exit 100 -fi -info "${BOLD}MFA:${RESET} Found ${#UNIQ_PROFILES[@]} profile/s" - - -# ----------------------------------------------------------------------------- -# 2. For each profile: -# ----------------------------------------------------------------------------- -for i in "${UNIQ_PROFILES[@]}" ; do - info "${BOLD}MFA:${RESET} Attempting to get temporary credentials for profile ${BOLD}$i${RESET}" - - # ----------------------------------------------------------------------------- - # 2.1. Get the role, serial number and source profile from AWS config file - # ----------------------------------------------------------------------------- - if ! MFA_ROLE_ARN=$(AWS_CONFIG_FILE="$SRC_AWS_CONFIG_FILE" && \ - AWS_SHARED_CREDENTIALS_FILE="$SRC_AWS_SHARED_CREDENTIALS_FILE" && \ - aws configure get role_arn --profile "$i" 2>&1); then - if [[ "$MFA_ROLE_ARN" == *"$i"* ]]; then - error "Credentials for profile $i have not been properly configured. Please check your configuration." - error "Check your AWS config file to look for the following profile entry: $i" - error "Check the following link for possible solutions: https://leverage.binbash.co/user-guide/troubleshooting/credentials/" - else - error "Missing 'role_arn'" - fi - exit 150 - fi - debug "${BOLD}MFA_ROLE_ARN=${RESET}$MFA_ROLE_ARN" - MFA_SERIAL_NUMBER=$(get_profile "$SRC_AWS_CONFIG_FILE" "$SRC_AWS_SHARED_CREDENTIALS_FILE" "$i" mfa_serial) - debug "${BOLD}MFA_SERIAL_NUMBER=${RESET}$MFA_SERIAL_NUMBER" - MFA_PROFILE_NAME=$(get_profile "$SRC_AWS_CONFIG_FILE" "$SRC_AWS_SHARED_CREDENTIALS_FILE" "$i" source_profile) - debug "${BOLD}MFA_PROFILE_NAME=${RESET}$MFA_PROFILE_NAME" - # Validate all required fields - if [[ $MFA_SERIAL_NUMBER == "" ]]; then error "Missing 'mfa_serial'" && exit 151; fi - if [[ $MFA_PROFILE_NAME == "" ]]; then error "Missing 'source_profile'" && exit 152; fi - - # ----------------------------------------------------------------------------- - # 2.2. Figure out the OTP or prompt the user - # ----------------------------------------------------------------------------- - # Loop a predefined number of times in case the OTP becomes invalid between - # the time it is generated and the time it is provided to the script - # ----------------------------------------------------------------------------- - MAX_RETRIES=3 - RETRIES_COUNT=0 - OTP_FAILED=true - MFA_DURATION=3600 - TEMP_FILE="$AWS_CACHE_DIR/$i" - debug "${BOLD}TEMP_FILE=${RESET}$TEMP_FILE" - - while [[ $OTP_FAILED == true && $RETRIES_COUNT -lt $MAX_RETRIES ]]; do - - # - # Check if cached credentials exist: look for a file that correspond to - # the current profile - # - if [[ -f "$TEMP_FILE" ]] && EXPIRATION_DATE=$(jq -r '.Credentials.Expiration' "$TEMP_FILE"); then - debug "Found cached credentials in ${BOLD}$TEMP_FILE${RESET}" - - # Get expiration date/timestamp - EXPIRATION_DATE=$(echo "$EXPIRATION_DATE" | sed -e 's/T/ /' | sed -E 's/(Z|\+[0-9]{2}:[0-9]{2})$//') - debug "${BOLD}EXPIRATION_DATE=${RESET}$EXPIRATION_DATE" - EXPIRATION_TS=$(date -d "$EXPIRATION_DATE" +"%s" || date +"%s") - debug "${BOLD}EXPIRATION_TS=${RESET}$EXPIRATION_TS" - - # Compare current timestamp (plus a margin) with the expiration timestamp - CURRENT_TS=$(date +"%s") - CURRENT_TS_PLUS_MARGIN=$(( "$CURRENT_TS" + (30 * 60) )) - debug "${BOLD}CURRENT_TS=${RESET}$CURRENT_TS" - debug "${BOLD}CURRENT_TS_PLUS_MARGIN=${RESET}$CURRENT_TS_PLUS_MARGIN" - if [[ CURRENT_TS_PLUS_MARGIN -lt $EXPIRATION_TS ]]; then - info "${BOLD}MFA:${RESET} Using cached credentials" - - # Pretend the OTP succeeded and exit the while loop - OTP_FAILED=false - break - fi - fi - - # Prompt user for MFA Token - echo -ne "${BOLD}MFA:${RESET} Please type in your OTP: " - if ! MFA_TOKEN_CODE=$(read MFA_TOKEN_CODE && echo "$MFA_TOKEN_CODE"); then - echo - error "Aborted!" - exit 156; - fi - debug "${BOLD}MFA_TOKEN_CODE=${RESET}$MFA_TOKEN_CODE" - - # ----------------------------------------------------------------------------- - # 2.3. Assume the role to generate the temporary credentials - # ----------------------------------------------------------------------------- - MFA_ROLE_SESSION_NAME="$MFA_PROFILE_NAME-temp" - if ! MFA_ASSUME_ROLE_OUTPUT=$(AWS_CONFIG_FILE="$SRC_AWS_CONFIG_FILE" && \ - AWS_SHARED_CREDENTIALS_FILE="$SRC_AWS_SHARED_CREDENTIALS_FILE" && \ - aws sts assume-role \ - --role-arn "$MFA_ROLE_ARN" \ - --serial-number "$MFA_SERIAL_NUMBER" \ - --role-session-name "$MFA_ROLE_SESSION_NAME" \ - --duration-seconds "$MFA_DURATION" \ - --token-code "$MFA_TOKEN_CODE" \ - --profile "$MFA_PROFILE_NAME" 2>&1); then - # Check if STS call failed because of invalid token or user interruption - if [[ $MFA_ASSUME_ROLE_OUTPUT == *"invalid MFA"* ]]; then - OTP_FAILED=true - info "Unable to get valid credentials. Let's try again..." - elif [[ $MFA_ASSUME_ROLE_OUTPUT == *"Invalid length for parameter TokenCode, value:"* ]]; then - OTP_FAILED=true - info "Invalid token length, it must be 6 digits long. Let's try again..." - elif [[ $MFA_ASSUME_ROLE_OUTPUT == *"AccessDenied"* ]]; then - info "Access Denied error!" - exit 161 - elif [[ $MFA_ASSUME_ROLE_OUTPUT == *"An error occurred"* ]]; then - info "An error occurred!" - exit 162 - fi - debug "${BOLD}MFA_ASSUME_ROLE_OUTPUT=${RESET}${MFA_ASSUME_ROLE_OUTPUT}" - else - OTP_FAILED=false - echo "$MFA_ASSUME_ROLE_OUTPUT" > "$TEMP_FILE" - fi - debug "${BOLD}OTP_FAILED=${RESET}$OTP_FAILED" - RETRIES_COUNT=$((RETRIES_COUNT+1)) - debug "${BOLD}RETRIES_COUNT=${RESET}$RETRIES_COUNT" - - done - - # Check if credentials were actually created - if [[ $OTP_FAILED == true ]]; then - error "Unable to get valid credentials after $MAX_RETRIES attempts" - exit 160 - fi - - # ----------------------------------------------------------------------------- - # 2.4. Generate the AWS profiles config files - # ----------------------------------------------------------------------------- - - # Parse id, secret and session from the output above - AWS_ACCESS_KEY_ID=$(jq -r .Credentials.AccessKeyId "$TEMP_FILE") - AWS_SECRET_ACCESS_KEY=$(jq -r .Credentials.SecretAccessKey "$TEMP_FILE") - AWS_SESSION_TOKEN=$(jq -r .Credentials.SessionToken "$TEMP_FILE") - debug "${BOLD}AWS_ACCESS_KEY_ID=${RESET}${AWS_ACCESS_KEY_ID:0:4}**************" - debug "${BOLD}AWS_SECRET_ACCESS_KEY=${RESET}${AWS_SECRET_ACCESS_KEY:0:4}**************" - debug "${BOLD}AWS_SESSION_TOKEN=${RESET}${AWS_SESSION_TOKEN:0:4}**************" - - # Create a profile block in the AWS credentials file using the credentials above - (AWS_CONFIG_FILE=$TF_AWS_CONFIG_FILE; \ - AWS_SHARED_CREDENTIALS_FILE=$TF_AWS_SHARED_CREDENTIALS_FILE; \ - aws configure set "profile.$i.aws_access_key_id" "$AWS_ACCESS_KEY_ID"; \ - aws configure set "profile.$i.aws_secret_access_key" "$AWS_SECRET_ACCESS_KEY"; \ - aws configure set "profile.$i.aws_session_token" "$AWS_SESSION_TOKEN"; \ - aws configure set region "$AWS_REGION"; \ - aws configure set output "$AWS_OUTPUT") - - info "${BOLD}MFA:${RESET} Credentials written succesfully!" -done - -# ----------------------------------------------------------------------------- -# 3. Pass the control back to the main process -# ----------------------------------------------------------------------------- -exec "$@" diff --git a/leverage/scripts/aws-sso/aws-sso-login.sh b/leverage/scripts/aws-sso/aws-sso-login.sh deleted file mode 100755 index 9d653330..00000000 --- a/leverage/scripts/aws-sso/aws-sso-login.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - -# ----------------------------------------------------------------------------- -# Formatting helpers -# ----------------------------------------------------------------------------- -BOLD="\033[1m" -DATE="\033[0;90m" -ERROR="\033[41;37m" -INFO="\033[0;34m" -DEBUG="\033[0;32m" -RESET="\033[0m" - -# ----------------------------------------------------------------------------- -# Helper Functions -# ----------------------------------------------------------------------------- -# Simple logging functions -function error { log "${ERROR}ERROR${RESET}\t$1" 0; } -function info { log "${INFO}INFO${RESET}\t$1" 1; } -function debug { log "${DEBUG}DEBUG${RESET}\t$1" 2; } -function log { - if [[ $SCRIPT_LOG_LEVEL -gt $2 ]]; then - printf "%b[%(%T)T]%b %b\n" "$DATE" "$(date +%s)" "$RESET" "$1" - fi -} - -# ----------------------------------------------------------------------------- -# Initialize variables -# ----------------------------------------------------------------------------- -SCRIPT_LOG_LEVEL=${SCRIPT_LOG_LEVEL:-2} -PROJECT=$(hcledit -f "$COMMON_CONFIG_FILE" attribute get project | sed 's/"//g') -SSO_PROFILE_NAME=${SSO_PROFILE_NAME:-$PROJECT-sso} -SSO_CACHE_DIR=${SSO_CACHE_DIR:-/home/leverage/tmp/$PROJECT/sso/cache} -AWS_SSO_CACHE_DIR=/home/leverage/.aws/sso/cache -SSO_TOKEN_FILE_NAME='token' -debug "SCRIPT_LOG_LEVEL=$SCRIPT_LOG_LEVEL" -debug "COMMON_CONFIG_FILE=$COMMON_CONFIG_FILE" -debug "ACCOUNT_CONFIG_FILE=$ACCOUNT_CONFIG_FILE" -debug "BACKEND_CONFIG_FILE=$BACKEND_CONFIG_FILE" -debug "SSO_PROFILE_NAME=$SSO_PROFILE_NAME" -debug "SSO_CACHE_DIR=$SSO_CACHE_DIR" -debug "SSO_TOKEN_FILE_NAME=$SSO_TOKEN_FILE_NAME" - -# Make sure cache dir exists -mkdir -p "$SSO_CACHE_DIR" - -# ----------------------------------------------------------------------------- -# Log in -# ----------------------------------------------------------------------------- -info "Logging in..." -aws sso login --profile "$SSO_PROFILE_NAME" - -# Store token in cache -debug "Caching token" -TOKEN_FILE="$SSO_CACHE_DIR/$SSO_TOKEN_FILE_NAME" -FILES=$(find "$AWS_SSO_CACHE_DIR" -maxdepth 1 -type f -name '*.json' -not -name 'botocore-client*' -exec ls {} \;) -for file in $FILES; -do - if (jq -er '.accessToken' $file >/dev/null); - then - cp $file "$TOKEN_FILE" - break - fi -done -debug "Token Expiration: $BOLD$(jq -r '.expiresAt' "$TOKEN_FILE")$RESET" - -info "${BOLD}Successfully logged in!$RESET" diff --git a/leverage/scripts/aws-sso/aws-sso-logout.sh b/leverage/scripts/aws-sso/aws-sso-logout.sh deleted file mode 100755 index 560c54d1..00000000 --- a/leverage/scripts/aws-sso/aws-sso-logout.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - -# ----------------------------------------------------------------------------- -# Formatting helpers -# ----------------------------------------------------------------------------- -BOLD="\033[1m" -DATE="\033[0;90m" -ERROR="\033[41;37m" -INFO="\033[0;34m" -DEBUG="\033[0;32m" -RESET="\033[0m" - -# ----------------------------------------------------------------------------- -# Helper Functions -# ----------------------------------------------------------------------------- -# Simple logging functions -function error { log "${ERROR}ERROR${RESET}\t$1" 0; } -function info { log "${INFO}INFO${RESET}\t$1" 1; } -function debug { log "${DEBUG}DEBUG${RESET}\t$1" 2; } -function log { - if [[ $SCRIPT_LOG_LEVEL -gt $2 ]]; then - printf "%b[%(%T)T]%b %b\n" "$DATE" "$(date +%s)" "$RESET" "$1" - fi -} - -# ----------------------------------------------------------------------------- -# Initialize variables -# ----------------------------------------------------------------------------- -SCRIPT_LOG_LEVEL=${SCRIPT_LOG_LEVEL:-2} -PROJECT=$(hcledit -f "$COMMON_CONFIG_FILE" attribute get project | sed 's/"//g') -SSO_CACHE_DIR=${SSO_CACHE_DIR:-/home/leverage/tmp/$PROJECT/sso/cache} -debug "SCRIPT_LOG_LEVEL=$SCRIPT_LOG_LEVEL" -debug "AWS_SHARED_CREDENTIALS_FILE=$AWS_SHARED_CREDENTIALS_FILE" -debug "AWS_CONFIG_FILE=$AWS_CONFIG_FILE" -debug "SSO_CACHE_DIR=$SSO_CACHE_DIR" -debug "PROJECT=$PROJECT" - -# ----------------------------------------------------------------------------- -# Log out -# ----------------------------------------------------------------------------- -aws sso logout - -# Clear sso token -debug "Removing SSO Tokens." -rm -f $SSO_CACHE_DIR/* - -# Clear AWS CLI credentials -debug "Wiping current SSO credentials." -awk '/^\[/{if($0~/profile '"$PROJECT-sso"'/ || $0 == "[default]"){found=1}else{found=""}} found' "$AWS_CONFIG_FILE" > tempconf && mv tempconf "$AWS_CONFIG_FILE" - -rm -f "$AWS_SHARED_CREDENTIALS_FILE" - -debug "All credentials wiped!" From 23f11e19a862f5361aaf05f141614ba1b4de8df9 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sun, 8 Sep 2024 16:34:50 -0300 Subject: [PATCH 12/15] fix test references --- pyproject.toml | 3 +-- tests/test_containers/test_aws.py | 2 +- tests/test_containers/test_kubectl.py | 2 +- tests/test_containers/test_terraform.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 42f677ff..b19e3fc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,7 @@ classifiers = [ packages = [ { include = "leverage" }, { include = "leverage/modules" }, - { include = "leverage/containers" }, - { include = "leverage/scripts" } + { include = "leverage/containers" } ] [tool.poetry.dependencies] diff --git a/tests/test_containers/test_aws.py b/tests/test_containers/test_aws.py index f6d1e8ba..819981a7 100644 --- a/tests/test_containers/test_aws.py +++ b/tests/test_containers/test_aws.py @@ -55,7 +55,7 @@ def test_sso_login(mocked_new_tab, aws_container, fake_os_user, propagate_logs, container_args = aws_container.client.api.create_container.call_args_list[0][1] # make sure we: point to the correct script - assert container_args["command"] == "/opt/scripts/aws-sso/aws-sso-login.sh" + assert container_args["command"] == "/home/leverage/scripts/aws-sso/aws-sso-login.sh" # the browser tab points to the correct code and the correct region assert mocked_new_tab.call_args[0][0] == "https://device.sso.us-east-1.amazonaws.com/?user_code=TEST-CODE" # and the fallback method is printed diff --git a/tests/test_containers/test_kubectl.py b/tests/test_containers/test_kubectl.py index 51895f10..a8f98c74 100644 --- a/tests/test_containers/test_kubectl.py +++ b/tests/test_containers/test_kubectl.py @@ -97,7 +97,7 @@ def test_start_shell_mfa(kubectl_container): # we want a shell, so -> /bin/bash with no entrypoint assert container_args["command"] == "/bin/bash" - assert container_args["entrypoint"] == "/opt/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " + assert container_args["entrypoint"] == "/home/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " # make sure we are pointing to the right AWS credentials: /.aws/ folder for MFA assert container_args["environment"]["AWS_CONFIG_FILE"] == "/home/leverage/.aws/test/config" diff --git a/tests/test_containers/test_terraform.py b/tests/test_containers/test_terraform.py index 1d61b658..d28e663f 100644 --- a/tests/test_containers/test_terraform.py +++ b/tests/test_containers/test_terraform.py @@ -51,7 +51,7 @@ def test_auth_method_mfa_enabled(terraform_container): terraform_container.sso_enabled = False terraform_container.mfa_enabled = True - assert terraform_container.auth_method() == "/opt/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " + assert terraform_container.auth_method() == "/home/leverage/scripts/aws-mfa/aws-mfa-entrypoint.sh -- " def test_auth_method_else(terraform_container): From 3951a46649dd82cd56dc7cb617f88ad0b3145a6b Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Mon, 9 Sep 2024 15:39:59 -0300 Subject: [PATCH 13/15] removing ipdb --- leverage/container.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/leverage/container.py b/leverage/container.py index a78d0404..0ae58bdc 100644 --- a/leverage/container.py +++ b/leverage/container.py @@ -598,9 +598,6 @@ def start(self, command, *arguments): return self._start(command, *arguments) def start_in_layer(self, command, *arguments): - import ipdb - - ipdb.set_trace() """Run a command that can only be performed in layer level.""" self.paths.check_for_layer_location() From 1b28ae38c3723e32fe385a82e78d9179579f4cf4 Mon Sep 17 00:00:00 2001 From: Francisco Rivera Date: Sat, 21 Sep 2024 08:42:08 -0300 Subject: [PATCH 14/15] merge master --- leverage/container.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/leverage/container.py b/leverage/container.py index 0ae58bdc..a8258ecb 100644 --- a/leverage/container.py +++ b/leverage/container.py @@ -92,7 +92,7 @@ def __init__(self, client, mounts: tuple = None, env_vars: dict = None): raise Exit(1) mounts = [Mount(source=source, target=target, type="bind") for source, target in mounts] if mounts else [] - self.host_config = self.client.api.create_host_config(security_opt=["label:disable"], mounts=mounts) + self.host_config = self.client.api.create_host_config(security_opt=["label=disable"], mounts=mounts) self.container_config = { "image": f"{self.image}:{self.local_image_tag}", "command": "", @@ -343,7 +343,8 @@ def get_sso_code(self, container) -> str: logs = self.docker_logs(container) if "Then enter the code:" in logs: return logs.split("Then enter the code:")[1].split("\n")[2] - + else: + logger.debug(logs) sleep(self.AWS_SSO_CODE_WAIT_SECONDS) raise ExitError(1, "Get SSO code timed-out") @@ -356,7 +357,7 @@ def get_sso_region(self): def sso_login(self) -> int: region = self.get_sso_region() - with CustomEntryPoint(self, ""): + with CustomEntryPoint(self, "sh -c"): container = self._create_container(False, command=self.AWS_SSO_LOGIN_SCRIPT) with ContainerSession(self.client, container): From 510fe04d151b4818ab90167dd5938295a63bd2c0 Mon Sep 17 00:00:00 2001 From: Angelo Fenoglio Date: Mon, 7 Oct 2024 15:19:17 -0300 Subject: [PATCH 15/15] Bump default Toolbox-version to 1.3.2-0.2.0 --- Makefile | 2 +- leverage/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a1ed27d3..2d5aa264 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: help build LEVERAGE_TESTING_IMAGE := binbash/leverage-cli-testing LEVERAGE_TESTING_TAG := 2.5.0 -LEVERAGE_IMAGE_TAG := 1.2.7-0.1.14 +LEVERAGE_IMAGE_TAG := 1.3.5-0.2.0 PYPROJECT_FILE := pyproject.toml INIT_FILE := leverage/__init__.py PLACEHOLDER := 0.0.0 diff --git a/leverage/__init__.py b/leverage/__init__.py index 24fc9b74..ec5410bf 100644 --- a/leverage/__init__.py +++ b/leverage/__init__.py @@ -4,7 +4,7 @@ # pylint: disable=wrong-import-position __version__ = "0.0.0" -__toolbox_version__ = "1.2.7-0.1.12" +__toolbox_version__ = "1.3.5-0.2.0" import sys from shutil import which