<reponame>shyovn/consoleme<gh_stars>1000+
import io
import json
import time

from ruamel.yaml.comments import CommentedSeq

from consoleme.config import config
from consoleme.lib.aws import minimize_iam_policy_statements
from consoleme.lib.plugins import get_plugin_by_name
from consoleme.lib.scm.git import Repository
from consoleme.lib.scm.git.bitbucket import BitBucket
from consoleme.lib.yaml import yaml
from consoleme.models import (
    ChangeModelArray,
    ExtendedRequestModel,
    GenericFileChangeModel,
    PolicyModel,
    RequestCreationModel,
    RequestStatus,
    UserModel,
)

auth = get_plugin_by_name(config.get("plugins.auth", "default_auth"))()
log = config.get_logger()


async def generate_honeybee_request_from_change_model_array(
    request_creation: RequestCreationModel, user: str, extended_request_uuid: str
) -> ExtendedRequestModel:
    repositories_for_request = {}
    primary_principal = None
    t = int(time.time())
    generated_branch_name = f"{user}-{t}"
    policy_name = config.get(
        "generate_honeybee_request_from_change_model_array.policy_name",
        "self_service_generated",
    )
    repo_config = None

    # Checkout Git Repo and generate a branch name for the user's change
    for change in request_creation.changes.changes:
        if primary_principal and change.principal != primary_principal:
            raise Exception("Changes must all affect the same principal")
        primary_principal = change.principal
        discovered_repository_for_change = False
        if repositories_for_request.get(change.principal.repository_name):
            continue
        # Find repo
        for r in config.get("cache_resource_templates.repositories", []):
            if r["name"] == change.principal.repository_name:
                repo_config = r
                repo = Repository(
                    r["repo_url"], r["name"], r["authentication_settings"]["email"]
                )
                await repo.clone(depth=1)
                git_client = repo.git
                git_client.reset()
                git_client.checkout(b=generated_branch_name)
                repositories_for_request[change.principal.repository_name] = {
                    "main_branch_name": r["main_branch_name"],
                    "repo": repo,
                    "git_client": git_client,
                    "config": r,
                }
                discovered_repository_for_change = True
                break
        if not discovered_repository_for_change:
            raise Exception(
                "No matching repository found for change in ConsoleMe's configuration"
            )
    request_changes = ChangeModelArray(changes=[])
    affected_templates = []
    for change in request_creation.changes.changes:
        git_client = repositories_for_request[change.principal.repository_name][
            "git_client"
        ]
        repo = repositories_for_request[change.principal.repository_name]["repo"].repo
        main_branch_name = repositories_for_request[change.principal.repository_name][
            "main_branch_name"
        ]
        git_client.checkout(
            f"origin/{main_branch_name}", change.principal.resource_identifier
        )
        change_file_path = f"{repo.working_dir}/{change.principal.resource_identifier}"
        with open(change_file_path, "r") as f:
            yaml_content = yaml.load(f)

        # Original
        buf = io.BytesIO()
        yaml.dump(yaml_content, buf)
        original_text = buf.getvalue()
        successfully_merged_statement = False
        if not yaml_content.get("Policies"):
            yaml_content["Policies"] = []
        if isinstance(yaml_content["Policies"], dict):
            yaml_content["Policies"] = [yaml_content["Policies"]]

        # The PolicyModel is a representation of a single (usually inline) policy that a user has requested be merged
        # into a given template. If the policy is provided as a string, it's the contents of the full file (which
        # should include the user's requested change)
        if isinstance(change.policy, PolicyModel):
            if isinstance(change.policy.policy_document["Statement"], str):
                change.policy.policy_document["Statement"] = [
                    change.policy.policy_document["Statement"]
                ]
            for i in range(len(yaml_content.get("Policies", []))):
                policy = yaml_content["Policies"][i]
                if policy.get("PolicyName") != policy_name:
                    continue
                if policy.get("IncludeAccounts") or policy.get("ExcludeAccounts"):
                    raise ValueError(
                        f"The {policy_name} policy has IncludeAccounts or ExcludeAccounts set"
                    )
                successfully_merged_statement = True

                policy["Statement"].extend(
                    CommentedSeq(change.policy.policy_document["Statement"])
                )
                yaml_content["Policies"][i][
                    "Statement"
                ] = await minimize_iam_policy_statements(
                    json.loads(json.dumps(policy["Statement"]))
                )
            if not successfully_merged_statement:
                yaml_content["Policies"].append(
                    {
                        "PolicyName": policy_name,
                        "Statement": change.policy.policy_document["Statement"],
                    }
                )
            with open(change_file_path, "w") as f:
                yaml.dump(yaml_content, f)
            # New
            buf = io.BytesIO()
            yaml.dump(yaml_content, buf)
            updated_text = buf.getvalue()

        elif isinstance(change.policy, str):
            # If the change is provided as a string, it represents the full change
            updated_text = change.policy
            with open(change_file_path, "w") as f:
                f.write(updated_text)
        else:
            raise Exception(
                "Unable to parse change from Honeybee templated role change request"
            )

        request_changes.changes.append(
            GenericFileChangeModel(
                principal=primary_principal,
                action="attach",
                change_type="generic_file",
                policy=updated_text,
                old_policy=original_text,
                encoding="yaml",
            )
        )
        git_client.add(change.principal.resource_identifier)
        affected_templates.append(change.principal.resource_identifier)

    pull_request_url = ""
    if not request_creation.dry_run:
        commit_title = f"ConsoleMe Generated PR for {', '.join(affected_templates)}"
        commit_message = (
            f"This request was made through ConsoleMe Self Service\n\nUser: {user}\n\n"
            f"Justification: {request_creation.justification}"
        )

        git_client.commit(m=commit_message)
        git_client.push(u=["origin", generated_branch_name])
        if repo_config["code_repository_provider"] == "bitbucket":
            bitbucket = BitBucket(
                repo_config["code_repository_config"]["url"],
                config.get(
                    repo_config["code_repository_config"]["username_config_key"]
                ),
                config.get(
                    repo_config["code_repository_config"]["password_config_key"]
                ),
            )
            pull_request_url = await bitbucket.create_pull_request(
                repo_config["project_key"],
                repo_config["name"],
                repo_config["project_key"],
                repo_config["name"],
                generated_branch_name,
                repo_config["main_branch_name"],
                commit_title,
                commit_message,
            )
        else:
            raise Exception(
                f"Unsupported `code_repository_provider` specified in configuration: {repo_config}"
            )

    for repo_name, repo_details in repositories_for_request.items():
        await repo_details["repo"].cleanup()

    if not pull_request_url and not request_creation.dry_run:
        raise Exception("Unable to generate pull request URL")

    return ExtendedRequestModel(
        id=extended_request_uuid,
        request_url=pull_request_url,
        principal=primary_principal,
        timestamp=int(time.time()),
        requester_email=user,
        approvers=[],
        request_status=RequestStatus.pending,
        changes=request_changes,
        requester_info=UserModel(
            email=user,
            extended_info=await auth.get_user_info(user),
            details_url=config.config_plugin().get_employee_info_url(user),
            photo_url=config.config_plugin().get_employee_photo_url(user),
        ),
        comments=[],
        cross_account=False,
    )
