Example #1
0
def create():
    """ Create the directory structure required by the project configuration and set up each account accordingly. """

    config = load_project_config()
    if not config:
        logger.error(
            "No configuration file found for the project."
            " Make sure the project has already been initialized ([bold]leverage project init[/bold])."
        )
        return

    if (PROJECT_ROOT / "config").exists():
        logger.error("Project has already been created.")
        return

    # Make project structure
    _copy_project_template(config=config)

    # Render project
    _render_project_template(config=config)

    # Format the code correctly
    logger.info("Reformatting terraform configuration to the standard style.")
    # NOTE: This is done just for the sake of making sure the docker image is already available,
    # otherwise two animations try to stack on each other and rich does not support that.
    # TODO: Modularize docker handling as to avoid this.
    tfrun(entrypoint="/bin/sh -c",
          command="'echo \"pull image\"'",
          enable_mfa=False,
          interactive=False)
    with console.status("Formatting..."):
        tfrun(command="fmt -recursive", enable_mfa=False, interactive=False)

    logger.info("Finished setting up project.")
Example #2
0
def configure_profile(profile, values):
    """ Set profile in `config` file for AWS cli.

    Args:
        profile (str): Profile name.
        values (dict): Mapping of values to be set in the profile.
    """
    logger.info(f"\tConfiguring profile [bold]{profile}[/bold]")
    for key, value in values.items():
        awscli(f"configure set {key} {value} --profile {profile}")
Example #3
0
def configure_accounts_profiles(profile, region, organization_accounts,
                                project_accounts):
    """ Set up the required profiles for all accounts to be used with AWS cli. Backup previous profiles.

    Args:
        profile(str): Name of the profile to configure.
        region (str): Region.
        organization_accounts (dict): Name and id of all accounts in the organization.
        project_accounts (dict): Name and email of all accounts in project configuration file.
    """
    short_name, type = profile.split("-")

    mfa_serial = ""
    if PROFILES[type]["mfa"]:
        logger.info("Fetching MFA device serial.")
        mfa_serial = _get_mfa_serial(profile)
        if not mfa_serial:
            logger.error(
                "No MFA device found for user. Please set up a device before configuring the accounts profiles."
            )
            raise Exit(1)

    account_profiles = {}
    for account in project_accounts:
        account_name = account["name"]
        # DevOps roles do not have permission over management account
        if "security" in profile and account_name == "management":
            continue

        # TODO: Add remaining profiles for remaining accounts declared in code if enough information is available
        account_id = organization_accounts.get(account_name, account.get("id"))
        if account_id is None:
            continue

        # A profile identifier looks like `le-security-oaar`
        account_profiles[
            f"{short_name}-{account_name}-{PROFILES[type]['profile_role']}"] = {
                "output": "json",
                "region": region,
                "role_arn":
                f"arn:aws:iam::{account_id}:role/{PROFILES[type]['role']}",
                "source_profile": profile,
                "mfa_serial": mfa_serial
            }

    logger.info("Backing up account profiles file.")
    _backup_file("config")

    for profile_identifier, profile_values in account_profiles.items():
        configure_profile(profile_identifier, profile_values)
Example #4
0
def _render_project_template(config, source=TEMPLATE_DIR):
    # Render base and non account related templates
    template_files = list(source.glob(TEMPLATE_PATTERN))
    config_templates = list(
        (source / CONFIG_DIRECTORY).rglob(TEMPLATE_PATTERN))
    template_files.extend(config_templates)

    logger.info("Setting up common base files.")
    _render_templates(template_files=template_files,
                      config=config,
                      source=source)

    # Render each account's templates
    for account in config["organization"]["accounts"]:
        _render_account_templates(account=account,
                                  config=config,
                                  source=source)

    logger.info("Project configuration finished.")
Example #5
0
def _render_account_templates(account, config, source=TEMPLATE_DIR):
    account_name = account["name"]
    logger.info(f"Account: Setting up [bold]{account_name}[/bold].")
    account_dir = source / account_name

    layers = [CONFIG_DIRECTORY]
    for account_name, account_layers in PROJECT_STRUCTURE[account_name].items(
    ):
        layers = layers + [
            f"{account_name}/{layer}" for layer in account_layers
        ]

    for layer in layers:
        logger.info(
            f"\tLayer: Setting up [bold]{layer.split('/')[-1]}[/bold].")
        layer_dir = account_dir / layer

        layer_templates = layer_dir.glob(TEMPLATE_PATTERN)
        _render_templates(template_files=layer_templates,
                          config=config,
                          source=source)
Example #6
0
def ensure_image(docker_client, image, tag):
    """ Check if the required image exists, if not, pull it from the registry.

    Args:
        docker_client (docker.DockerClient): Point of communication with Docker.
        image (str): Name of the required image.
        tag (str): Tag of the required image.
    """
    found_image = docker_client.api.images(f"{image}:{tag}")

    if found_image:
        return

    logger.info("Required docker image not found.")

    stream = docker_client.api.pull(repository=image,
                                    tag=tag,
                                    stream=True,
                                    decode=True)
    logger.info(next(stream)["status"])

    imageinfo = []
    with console.status("Pulling image..."):
        for status in stream:
            status = status["status"]
            if status.startswith("Digest") or status.startswith("Status"):
                imageinfo.append(status)

    for info in imageinfo:
        logger.info(info)
Example #7
0
def _load_configs_for_credentials():
    """ Load all required values to configure credentials.

    Raises:
        Exit: If no project has been already initialized in the system.

    Returns:
        dict: Values needed to configure a credential and update the files accordingly.
    """
    logger.info("Loading configuration file.")
    project_config = load_project_config()

    logger.info("Loading project environment configuration file.")
    env_config = load_env_config()

    terraform_config = {}
    if PROJECT_COMMON_TFVARS.exists():
        logger.info("Loading Terraform common configuration.")
        terraform_config = hcl2.loads(PROJECT_COMMON_TFVARS.read_text())

    config_values = {}
    config_values["short_name"] = (project_config.get("short_name")
                                   or env_config.get("PROJECT")
                                   or terraform_config.get("project")
                                   or _ask_for_short_name())
    config_values["project_name"] = (project_config.get("project_name")
                                     or terraform_config.get("project_long"))

    config_values["primary_region"] = (project_config.get("primary_region") or
                                       terraform_config.get("region_primary")
                                       or _ask_for_region())
    config_values["secondary_region"] = terraform_config.get(
        "region_secondary")

    config_values["organization"] = {"accounts": []}
    # Accounts defined in Terraform code take priority
    terraform_accounts = terraform_config.get("accounts", {})
    if terraform_accounts:
        config_values["organization"]["accounts"].extend([{
            "name":
            account_name,
            "email":
            account_info.get("email"),
            "id":
            account_info.get("id")
        } for account_name, account_info in terraform_accounts.items()])
    # Add accounts not found in terraform code
    project_accounts = [
        account for account in project_config.get("organization", {}).get(
            "accounts", []) if account.get("name") not in terraform_accounts
    ]
    if project_accounts:
        config_values["organization"]["accounts"].extend(project_accounts)

    config_values["mfa_enabled"] = env_config.get("MFA_ENABLED", "false")
    config_values["terraform_image_tag"] = env_config.get(
        "TERRAFORM_IMAGE_TAG", "1.0.9")

    return config_values
Example #8
0
def _copy_project_template(config):
    """ Copy all files and directories from the Leverage project template to the project directory.
    It excludes al jinja templates as those will be rendered directly to their final location.

    Args:
        config (dict): Project configuration.
    """
    logger.info("Creating project directory structure.")

    # Copy .gitignore file
    copy2(src=TEMPLATE_DIR / ".gitignore", dst=PROJECT_ROOT / ".gitignore")

    # Root config directory
    copytree(src=TEMPLATE_DIR / CONFIG_DIRECTORY,
             dst=PROJECT_ROOT / CONFIG_DIRECTORY,
             ignore=IGNORE_PATTERNS)

    # Accounts
    for account in PROJECT_STRUCTURE:
        _copy_account(account=account, primary_region=config["primary_region"])

    logger.info("Finished creating directory structure.")
Example #9
0
def configure_credentials(profile, file=None, make_backup=False):
    """ Set credentials in `credentials` file for AWS cli. Make backup if required.

    Args:
        profile (str): Name of the profile to configure.
        file (Path, optional): Credentials file. Defaults to None.
        make_backup (bool, optional): Whether to make a backup of the credentials file. Defaults to False.
    """
    file = file or _ask_for_credentials_location()

    if file is not None and file != "manual":
        key_id, secret_key = _extract_credentials(file)

    else:
        key_id, secret_key = _ask_for_credentials()

    if make_backup:
        logger.info("Backing up credentials file.")
        _backup_file("credentials")

    values = {"aws_access_key_id": key_id, "aws_secret_access_key": secret_key}

    for key, value in values.items():
        awscli(f"configure set {key} {value} --profile {profile}")
Example #10
0
def configure(type, credentials_file, overwrite_existing_credentials,
              skip_access_keys_setup, skip_assumable_roles_setup):
    """ Configure credentials for the project.

    It can handle the credentials required for the initial deployment of the project (BOOTSTRAP),
    a management user (MANAGEMENT) or a devops/secops user (SECURITY).
    """
    if skip_access_keys_setup and skip_assumable_roles_setup:
        logger.info("Nothing to do. Exiting.")
        return

    config_values = _load_configs_for_credentials()

    # Environment configuration variables are needed for the Leverage docker container
    if not PROJECT_ENV_CONFIG.exists():
        PROJECT_ENV_CONFIG.write_text(f"PROJECT={config_values['short_name']}")

    type = type.lower()
    short_name = config_values.get("short_name")
    profile = f"{short_name}-{type}"

    already_configured = _profile_is_configured(profile=profile)
    if already_configured and not (skip_access_keys_setup
                                   or overwrite_existing_credentials):
        title_extra = "" if skip_assumable_roles_setup else " Continue on with assumable roles setup."

        overwrite_existing_credentials = _ask_for_credentials_overwrite(
            profile=profile,
            skip_option_title=f"Skip credentials configuration.{title_extra}",
            overwrite_option_title=
            "Overwrite current credentials. Backups will be made.")

    do_configure_credentials = False if skip_access_keys_setup else overwrite_existing_credentials or not already_configured

    if do_configure_credentials:
        logger.info(f"Configuring [bold]{type}[/bold] credentials.")
        configure_credentials(profile,
                              credentials_file,
                              make_backup=already_configured)
        logger.info(
            f"[bold]{type.capitalize()} credentials configured in:[/bold]"
            f" {(AWSCLI_CONFIG_DIR / short_name / 'credentials').as_posix()}")

        if not _credentials_are_valid(profile):
            logger.error(
                f"Invalid {profile} credentials. Please check the given keys.")
            return

    accounts = config_values.get("organization", {}).get("accounts", False)
    # First time configuring bootstrap credentials
    if type == "bootstrap" and not already_configured:
        management_account = next(
            (account
             for account in accounts if account["name"] == "management"), None)

        if management_account:
            logger.info("Fetching management account id.")
            management_account_id = _get_management_account_id(profile=profile)
            management_account["id"] = management_account_id

            project_config_file = load_project_config()
            if project_config_file and "accounts" in project_config_file.get(
                    "organization", {}):
                project_config_file["organization"]["accounts"] = accounts

                logger.info("Updating project configuration file.")
                YAML().dump(data=project_config_file, stream=PROJECT_CONFIG)

        skip_assumable_roles_setup = True

    profile_for_organization = profile
    # Security credentials don't have permission to access organization information
    if type == "security":
        for type_with_permission in ("management", "bootstrap"):
            profile_to_check = f"{short_name}-{type_with_permission}"

            if _profile_is_configured(profile_to_check):
                profile_for_organization = profile_to_check
                break

    if skip_assumable_roles_setup:
        logger.info("Skipping assumable roles configuration.")

    else:
        logger.info("Attempting to fetch organization accounts.")
        organization_accounts = _get_organization_accounts(
            profile_for_organization, config_values.get("project_name"))
        logger.debug(f"Organization Accounts fetched: {organization_accounts}")

        if organization_accounts or accounts:
            logger.info("Configuring assumable roles.")

            configure_accounts_profiles(profile,
                                        config_values["primary_region"],
                                        organization_accounts, accounts)
            logger.info(
                f"[bold]Account profiles configured in:[/bold]"
                f" {(AWSCLI_CONFIG_DIR / short_name / 'config').as_posix()}")

            for account in accounts:
                try:  # Some account may not already be created
                    account["id"] = organization_accounts[account["name"]]
                except KeyError:
                    continue

        else:
            logger.info(
                "No organization has been created yet or no accounts were configured.\n"
                "Skipping assumable roles configuration.")

    _update_account_ids(config=config_values)
Example #11
0
def init():
    """ Initializes and gets all the required resources to be able to create a new Leverage project. """

    # Application's directory
    if not LEVERAGE_DIR.exists():
        logger.info(
            "No [bold]Leverage[/bold] config directory found in user's home. Creating."
        )
        LEVERAGE_DIR.mkdir()

    # Leverage project templates
    if not TEMPLATES_REPO_DIR.exists():
        TEMPLATES_REPO_DIR.mkdir(parents=True)

    if not (TEMPLATES_REPO_DIR / ".git").exists():
        logger.info("No project template found. Cloning template.")
        git(f"clone {LEVERAGE_TEMPLATE_REPO} {TEMPLATES_REPO_DIR.as_posix()}")
        logger.info("Finished cloning template.")

    else:
        logger.info("Project template found. Updating.")
        git(f"-C {TEMPLATES_REPO_DIR.as_posix()} checkout master")
        git(f"-C {TEMPLATES_REPO_DIR.as_posix()} pull")
        logger.info("Finished updating template.")

    # Leverage projects are git repositories too
    logger.info("Initializing git repository in project directory.")
    git("init")

    # Project configuration file
    if not PROJECT_CONFIG.exists():
        logger.info(
            f"No project configuration file found. Dropping configuration template [bold]{PROJECT_CONFIG_FILE}[/bold]."
        )
        copy2(src=CONFIG_FILE_TEMPLATE, dst=PROJECT_CONFIG_FILE)

    else:
        logger.warning(
            f"Project configuration file [bold]{PROJECT_CONFIG_FILE}[/bold] already exists in directory."
        )

    logger.info("Project initialization finished.")