apiVersion: v1
kind: ConfigMap
metadata:
  name: extension-downloader-configmap
data:
  install.py: |
    import argparse
    import json
    import os
    import shutil
    import sys
    import tempfile
    import zipfile
    from pathlib import Path
    from urllib.error import HTTPError, URLError
    from urllib.parse import quote
    from urllib.request import Request, urlopen


    # GitHub REST API version.
    GITHUB_API_VERSION = "2022-11-28"
    GITHUB_ORGANIZATION = "Keyfactor"


    def github_request(url, token, accept="application/vnd.github+json"):
        headers = {
            "Accept": accept,
            "X-GitHub-Api-Version": GITHUB_API_VERSION,
            "User-Agent": "Keyfactor-extension-downloader",
        }

        if token:
            headers["Authorization"] = f"Bearer {token}"

        return Request(url, headers=headers)


    def get_release(extension, version, token):
        encoded_version = quote(version, safe="")
        release_url = (
            f"https://api.github.com/repos/{GITHUB_ORGANIZATION}/"
            f"{extension}/releases/tags/{encoded_version}"
        )

        print(f"Fetching release: {release_url}")

        try:
            with urlopen(github_request(release_url, token)) as response:
                return json.load(response)
        except HTTPError as error:
            if error.code == 404:
                fail(
                    f"No release with tag '{version}' was found in "
                    f"{GITHUB_ORGANIZATION}/{extension}."
                )
            fail(f"GitHub returned HTTP {error.code} for {release_url}: {error.reason}")
        except URLError as error:
            fail(f"Unable to connect to GitHub: {error.reason}")
        except json.JSONDecodeError as error:
            fail(f"GitHub returned an invalid JSON response: {error}")


    def choose_asset(release, asset_name=None):
        assets = release.get("assets", [])

        if not assets:
            fail("The release contains no assets.")

        if asset_name:
            for asset in assets:
                if asset.get("name") == asset_name:
                    return asset

            fail(f"The release does not contain an asset named '{asset_name}'.")

        suffixes = (
            "_net10.0_signed.zip",
            "_net10.0.zip",
            "_net8.0_signed.zip",
            "_net8.0.zip",
        )

        for suffix in suffixes:
            matches = [
                asset
                for asset in assets
                if asset.get("name", "").lower().endswith(suffix)
            ]

            if len(matches) == 1:
                return matches[0]

            if len(matches) > 1:
                names = ", ".join(asset["name"] for asset in matches)
                fail(
                    f"Multiple assets matched '{suffix}': {names}. "
                    "Specify one with --asset-name."
                )

        fail(
            "No suitable signed or unsigned net10.0 or net8.0 ZIP asset was found."
        )


    def download_asset(asset, destination, token):
        asset_url = asset.get("url")
        asset_name = asset.get("name", "<unnamed>")

        if not asset_url:
            fail(f"The selected asset '{asset_name}' does not have a download URL.")

        print(f"Downloading asset: {asset_name}")

        try:
            with urlopen(
                github_request(asset_url, token, "application/octet-stream")
            ) as response:
                with destination.open("wb") as output:
                    shutil.copyfileobj(response, output)
        except HTTPError as error:
            fail(
                f"GitHub returned HTTP {error.code} while downloading "
                f"'{asset_name}': {error.reason}"
            )
        except URLError as error:
            fail(f"Unable to download '{asset_name}': {error.reason}")


    def get_root_directory(members):
        roots = set()

        for member in members:
            path = Path(member.filename.replace("\\", "/"))

            if not path.parts:
                continue

            roots.add(path.parts[0])

        if len(roots) != 1:
            return None

        root = next(iter(roots))

        if all(
            len(Path(member.filename.replace("\\", "/")).parts) > 1
            for member in members
            if member.filename
        ):
            return root

        return None


    def extract_archive(archive_path, output_directory):
        output_root = output_directory.resolve()

        try:
            with zipfile.ZipFile(archive_path) as archive:
                members = archive.infolist()
                root_directory = get_root_directory(members)

                entries = []

                for member in members:
                    path = Path(member.filename.replace("\\", "/"))

                    if root_directory and path.parts:
                        path = Path(*path.parts[1:])

                    if path.parts:
                        entries.append((member, path))

                directory_paths = set()

                for _, path in entries:
                    for index in range(1, len(path.parts)):
                        directory_paths.add(Path(*path.parts[:index]))

                for member, relative_path in entries:
                    destination = (output_root / relative_path).resolve()

                    try:
                        destination.relative_to(output_root)
                    except ValueError:
                        fail(
                            f"The archive contains an unsafe path: "
                            f"{member.filename}"
                        )

                    is_directory = (
                        member.is_dir()
                        or member.filename.endswith(("/", "\\"))
                        or relative_path in directory_paths
                    )

                    if is_directory:
                        destination.mkdir(parents=True, exist_ok=True)
                        continue

                    destination.parent.mkdir(parents=True, exist_ok=True)

                    with archive.open(member) as source:
                        with destination.open("wb") as target:
                            shutil.copyfileobj(source, target)

        except zipfile.BadZipFile:
            fail("The downloaded asset is not a valid ZIP archive.")


    def fail(message):
        print(f"Error: {message}", file=sys.stderr)
        sys.exit(1)


    def parse_arguments():
        parser = argparse.ArgumentParser(
            description="Download and extract a Keyfactor GitHub release asset."
        )
        parser.add_argument(
            "--extension",
            required=True,
            help="Name of the repository in the Keyfactor GitHub organization.",
        )
        parser.add_argument(
            "--version",
            required=True,
            help="Release tag to download.",
        )
        parser.add_argument(
            "--out",
            required=True,
            type=Path,
            help="Directory in which to extract the release asset.",
        )
        parser.add_argument(
            "--asset-name",
            help=(
                "Exact asset filename to download. When omitted, the script "
                "prefers a signed net10.0 ZIP asset, followed by net10.0, "
                "signed net8.0, and net8.0."
            ),
        )
        parser.add_argument(
            "--github-token-env",
            default="GITHUB_TOKEN",
            help=(
                "Environment variable containing a GitHub token. "
                "Default: GITHUB_TOKEN."
            ),
        )

        return parser.parse_args()


    def main():
        args = parse_arguments()
        args.out.mkdir(parents=True, exist_ok=True)

        token = os.getenv(args.github_token_env, "")
        release = get_release(args.extension, args.version, token)
        asset = choose_asset(release, args.asset_name)

        print(f"Selected asset: {asset['name']}")

        with tempfile.TemporaryDirectory() as temporary_directory:
            archive_path = Path(temporary_directory) / asset["name"]
            download_asset(asset, archive_path, token)

            print(f"Extracting asset to: {args.out}")
            extract_archive(archive_path, args.out)

        print(f"Extraction complete. Files are available in {args.out}.")


    if __name__ == "__main__":
        main()
