#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
import json
import os
import platform
import subprocess
import tempfile
from typing import Optional, Tuple

import boto3
import click
import requests
from botocore.exceptions import NoCredentialsError


@click.command()
@click.option(
    "--runner-version",
    default="2.275.1",
    help="Runner version to register with",
    metavar="VER",
)
@click.option("--repo", default="apache/airflow")
@click.option("--store-as", default="apache/airflow")
@click.option("--runnergroup")
@click.option("--token", help="GitHub runner registration token", required=False)
@click.option("--index", type=int, required=False)
def main(
    token, runner_version, store_as: Optional[str], repo, runnergroup: Optional[str], index: Optional[int]
):
    check_aws_config()
    dir = make_runner_dir(runner_version)

    if not token:
        token = click.prompt("GitHub runner registration token")

    if store_as is None:
        store_as = repo

    if index is None:
        index = get_next_index(store_as)
    click.echo(f"Registering as runner {index}")

    register_runner(dir.name, token, repo, runnergroup, store_as, index)


def check_aws_config():
    click.echo("Checking AWS account credentials")
    try:
        whoami = boto3.client("sts").get_caller_identity()
    except NoCredentialsError:
        click.echo("No AWS credentials found -- maybe you need to set AWS_PROFILE?", err=True)
        exit(1)

    if whoami["Account"] != "827901512104":
        click.echo("Wrong AWS account in use -- maybe you need to set AWS_PROFILE?", err=True)
        exit(1)


def make_runner_dir(version):
    """Extract the runner tar to a temporary directory"""
    dir = tempfile.TemporaryDirectory()

    tar = _get_runner_tar(version)

    subprocess.check_call(
        ["tar", "-xzf", tar],
        cwd=dir.name,
    )

    return dir


def get_next_index(repo: str) -> int:
    """Find the next available index to store the runner credentials in AWS SSM ParameterStore"""
    paginator = boto3.client("ssm").get_paginator("describe_parameters")

    path = os.path.join('/runners/', repo, '')

    pages = paginator.paginate(ParameterFilters=[{"Key": "Path", "Option": "Recursive", "Values": [path]}])

    seen = set()

    for page in pages:
        for param in page['Parameters']:
            name = param['Name']

            # '/runners/1/config' -> '1'
            index = os.path.basename(os.path.dirname(name))
            seen.add(int(index))

    if not seen:
        return 1

    # Fill in any gaps too.
    for n in range(1, max(seen) + 2):
        if n not in seen:
            return n


def register_runner(dir: str, token: str, repo: str, runnergroup: Optional[str], store_as: str, index: int):
    os.chdir(dir)

    cmd = [
        "./config.sh",
        "--unattended",
        "--url",
        f"https://github.com/{repo}",
        "--token",
        token,
        "--name",
        f"Airflow Runner {index}",
    ]

    if runnergroup:
        cmd += ['--runnergroup', runnergroup]

    res = subprocess.call(cmd)

    if res != 0:
        exit(res)
    _put_runner_creds(store_as, index)


def _put_runner_creds(repo: str, index: int):
    client = boto3.client("ssm")

    with open(".runner", encoding='utf-8-sig') as fh:
        # We want to adjust the config before storing it!
        config = json.load(fh)
        config["pullRequestSecurity"] = {}

        client.put_parameter(
            Name=f"/runners/{repo}/{index}/config",
            Type="String",
            Value=json.dumps(config, indent=2),
        )

    with open(".credentials", encoding='utf-8-sig') as fh:
        client.put_parameter(Name=f"/runners/{repo}/{index}/credentials", Type="String", Value=fh.read())

    with open(".credentials_rsaparams", encoding='utf-8-sig') as fh:
        client.put_parameter(Name=f"/runners/{repo}/{index}/rsaparams", Type="SecureString", Value=fh.read())


def _get_system_arch() -> Tuple[str, str]:
    uname = platform.uname()
    if uname.system == "Linux":
        system = "linux"
    elif uname.system == "Darwin":
        system = "osx"
    else:
        raise RuntimeError("Un-supported platform")

    if uname.machine == "x86_64":
        arch = "x64"
    else:
        raise RuntimeError("Un-supported architecture")

    return system, arch


def _get_runner_tar(version) -> str:
    system, arch = _get_system_arch()

    cache = os.path.abspath(".cache")

    try:
        os.mkdir(cache)
    except FileExistsError:
        pass

    fname = f"actions-runner-{system}-{arch}-{version}.tar.gz"
    local_file = os.path.join(cache, fname)

    if os.path.exists(local_file):
        return local_file

    url = f"https://github.com/actions/runner/releases/download/v{version}/{fname}"
    click.echo(f"Getting {url}")
    resp = requests.get(url, stream=True)
    resp.raise_for_status()
    with open(local_file, "wb") as fh, click.progressbar(length=int(resp.headers["content-length"])) as bar:
        for chunk in resp.iter_content(chunk_size=40960):
            fh.write(chunk)
            bar.update(len(chunk))
    return local_file


if __name__ == "__main__":
    main()

#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
import json
import os
import platform
import subprocess
import tempfile
from typing import Optional, Tuple

import boto3
import click
import requests
from botocore.exceptions import NoCredentialsError


@click.command()
@click.option(
    "--runner-version",
    default="2.275.1",
    help="Runner version to register with",
    metavar="VER",
)
@click.option("--repo", default="apache/airflow")
@click.option("--store-as", default="apache/airflow")
@click.option("--runnergroup")
@click.option("--token", help="GitHub runner registration token", required=False)
@click.option("--index", type=int, required=False)
def main(
    token, runner_version, store_as: Optional[str], repo, runnergroup: Optional[str], index: Optional[int]
):
    check_aws_config()
    dir = make_runner_dir(runner_version)

    if not token:
        token = click.prompt("GitHub runner registration token")

    if store_as is None:
        store_as = repo

    if index is None:
        index = get_next_index(store_as)
    click.echo(f"Registering as runner {index}")

    register_runner(dir.name, token, repo, runnergroup, store_as, index)


def check_aws_config():
    click.echo("Checking AWS account credentials")
    try:
        whoami = boto3.client("sts").get_caller_identity()
    except NoCredentialsError:
        click.echo("No AWS credentials found -- maybe you need to set AWS_PROFILE?", err=True)
        exit(1)

    if whoami["Account"] != "827901512104":
        click.echo("Wrong AWS account in use -- maybe you need to set AWS_PROFILE?", err=True)
        exit(1)


def make_runner_dir(version):
    """Extract the runner tar to a temporary directory"""
    dir = tempfile.TemporaryDirectory()

    tar = _get_runner_tar(version)

    subprocess.check_call(
        ["tar", "-xzf", tar],
        cwd=dir.name,
    )

    return dir


def get_next_index(repo: str) -> int:
    """Find the next available index to store the runner credentials in AWS SSM ParameterStore"""
    paginator = boto3.client("ssm").get_paginator("describe_parameters")

    path = os.path.join('/runners/', repo, '')

    pages = paginator.paginate(ParameterFilters=[{"Key": "Path", "Option": "Recursive", "Values": [path]}])

    seen = set()

    for page in pages:
        for param in page['Parameters']:
            name = param['Name']

            # '/runners/1/config' -> '1'
            index = os.path.basename(os.path.dirname(name))
            seen.add(int(index))

    if not seen:
        return 1

    # Fill in any gaps too.
    for n in range(1, max(seen) + 2):
        if n not in seen:
            return n


def register_runner(dir: str, token: str, repo: str, runnergroup: Optional[str], store_as: str, index: int):
    os.chdir(dir)

    cmd = [
        "./config.sh",
        "--unattended",
        "--url",
        f"https://github.com/{repo}",
        "--token",
        token,
        "--name",
        f"Airflow Runner {index}",
    ]

    if runnergroup:
        cmd += ['--runnergroup', runnergroup]

    res = subprocess.call(cmd)

    if res != 0:
        exit(res)
    _put_runner_creds(store_as, index)


def _put_runner_creds(repo: str, index: int):
    client = boto3.client("ssm")

    with open(".runner", encoding='utf-8-sig') as fh:
        # We want to adjust the config before storing it!
        config = json.load(fh)
        config["pullRequestSecurity"] = {}

        client.put_parameter(
            Name=f"/runners/{repo}/{index}/config",
            Type="String",
            Value=json.dumps(config, indent=2),
        )

    with open(".credentials", encoding='utf-8-sig') as fh:
        client.put_parameter(Name=f"/runners/{repo}/{index}/credentials", Type="String", Value=fh.read())

    with open(".credentials_rsaparams", encoding='utf-8-sig') as fh:
        client.put_parameter(Name=f"/runners/{repo}/{index}/rsaparams", Type="SecureString", Value=fh.read())


def _get_system_arch() -> Tuple[str, str]:
    uname = platform.uname()
    if uname.system == "Linux":
        system = "linux"
    elif uname.system == "Darwin":
        system = "osx"
    else:
        raise RuntimeError("Un-supported platform")

    if uname.machine == "x86_64":
        arch = "x64"
    else:
        raise RuntimeError("Un-supported architecture")

    return system, arch


def _get_runner_tar(version) -> str:
    system, arch = _get_system_arch()

    cache = os.path.abspath(".cache")

    try:
        os.mkdir(cache)
    except FileExistsError:
        pass

    fname = f"actions-runner-{system}-{arch}-{version}.tar.gz"
    local_file = os.path.join(cache, fname)

    if os.path.exists(local_file):
        return local_file

    url = f"https://github.com/actions/runner/releases/download/v{version}/{fname}"
    click.echo(f"Getting {url}")
    resp = requests.get(url, stream=True)
    resp.raise_for_status()
    with open(local_file, "wb") as fh, click.progressbar(length=int(resp.headers["content-length"])) as bar:
        for chunk in resp.iter_content(chunk_size=40960):
            fh.write(chunk)
            bar.update(len(chunk))
    return local_file


if __name__ == "__main__":
    main()

#!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements.  See the NOTICE file # distributed with this work for additional information # regarding copyright ownership.  The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License.  You may obtain a copy of the License at # #   http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied.  See the License for the # specific language governing permissions and limitations # under the License. Extract the runner tar to a temporary directory Find the next available index to store the runner credentials in AWS SSM ParameterStore # '/runners/1/config' -> '1' # Fill in any gaps too. # We want to adjust the config before storing it!