From 685e6f44f7c2e317b49e5867971d1785d242cb51 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sun, 17 Sep 2023 01:59:08 +0530 Subject: [PATCH] feat: deploy and configure forgejo instance for testing --- Makefile | 9 ++ docker-compose-dev-deps.yml | 15 +++ integration/__init__.py | 0 integration/__main__.py | 13 ++ integration/ci.sh | 35 ++++++ integration/cli.py | 155 +++++++++++++++++++++++ integration/csrf.py | 38 ++++++ integration/forgejo.py | 244 ++++++++++++++++++++++++++++++++++++ integration/lib.sh | 113 +++++++++++++++++ integration/tests.sh | 17 +++ 10 files changed, 639 insertions(+) create mode 100644 Makefile create mode 100644 docker-compose-dev-deps.yml create mode 100644 integration/__init__.py create mode 100644 integration/__main__.py create mode 100755 integration/ci.sh create mode 100644 integration/cli.py create mode 100644 integration/csrf.py create mode 100755 integration/forgejo.py create mode 100755 integration/lib.sh create mode 100755 integration/tests.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fbc89ad --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +env.up: + docker-compose -f ./docker-compose-dev-deps.yml up -d + +env.down: + docker-compose -f ./docker-compose-dev-deps.yml down --remove-orphans -v + +test: + ./integration/tests.sh + pnpm run test diff --git a/docker-compose-dev-deps.yml b/docker-compose-dev-deps.yml new file mode 100644 index 0000000..9826006 --- /dev/null +++ b/docker-compose-dev-deps.yml @@ -0,0 +1,15 @@ +version: "3" + + +services: + forgejo: + image: codeberg.org/forgejo/forgejo:1.18.0-1 + container_name: hostea-dash-forgejo + network_mode: host + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro diff --git a/integration/__init__.py b/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration/__main__.py b/integration/__main__.py new file mode 100644 index 0000000..23bf4ea --- /dev/null +++ b/integration/__main__.py @@ -0,0 +1,13 @@ +import argparse + +from .cli import Cli + + +def admin(args): + print(args) + + +if __name__ == "__main__": + cli = Cli() + opts = cli.parse() + opts.func(opts, c=cli.c) diff --git a/integration/ci.sh b/integration/ci.sh new file mode 100755 index 0000000..0552978 --- /dev/null +++ b/integration/ci.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -Exeuo pipefail + +source integration/lib.sh + +init() { +# if is_ci +# then +# echo "[*] CI environment detected" +# else + docker_compose_up + setup_env + sleep 5 +# wait_for_env +# fi + forgejo_root + init_users_repo + fleet_repo_init +} + +teardown() { + if ! is_ci + then + docker_compose_down + teardown_env + sed -i /localhost.*/d ~/.ssh/known_hosts + fi +} + +new_fleet_repo() { + new_fleet_repo_init $2 +} + +$1 $@ diff --git a/integration/cli.py b/integration/cli.py new file mode 100644 index 0000000..9062fb0 --- /dev/null +++ b/integration/cli.py @@ -0,0 +1,155 @@ +import argparse + +from requests import Session + + +def forgejo_from_args(args, c: Session): + from .forgejo import Forgejo + + return Forgejo( + host=args.host, + username=args.username, + password=args.password, + email=args.email, + c=c, + ) + + +class Forgejo: + def __init__(self, parser, c: Session): + self.c = c + self.parser = parser + self.subparser = self.parser.add_subparsers() + self.install() + self.register() + self.login() + self.create_repository() + self.create_issue() + + def __add_credentials_parser(self, parser): + group = parser.add_argument_group("credentials", "User credentials") + group.add_argument("username", type=str, help="Forgejo user's username") + group.add_argument("password", type=str, help="Forgejo user's password") + group.add_argument("email", type=str, help="Forgejo user's email") + group.add_argument("host", type=str, help="URI at which Forgejo is running") + + def install(self): + def run(args, c: Session): + forgejo = forgejo_from_args(args, c=c) + forgejo.install() + + self.install_parser = self.subparser.add_parser( + name="install", description="Install Forgejo", help="Install Forgejo" + ) + self.__add_credentials_parser(self.install_parser) + self.install_parser.set_defaults(func=run) + + def register(self): + def run(args, c: Session): + forgejo = forgejo_from_args(args, c=c) + forgejo.register() + + self.register_parser = self.subparser.add_parser( + name="register", + description="Forgejo user registration", + help="Register a user on Forgejo", + ) + self.__add_credentials_parser(self.register_parser) + self.register_parser.set_defaults(func=run) + + def login(self): + def run(args, c: Session): + forgejo = forgejo_from_args(args, c=c) + forgejo.login() + + self.login_parser = self.subparser.add_parser( + name="login", description="Forgejo user login", help="Login on Forgejo" + ) + self.__add_credentials_parser(self.login_parser) + self.login_parser.set_defaults(func=run) + + def create_repository(self): + def run(args, c: Session): + forgejo = forgejo_from_args(args, c=c) + forgejo.login() + forgejo.create_repository(name=args.repo_name) + + self.create_repository_parser = self.subparser.add_parser( + name="create_repo", + description="Create repository on Forgejo", + help="Create repository on Forgejo", + ) + self.__add_credentials_parser(self.create_repository_parser) + self.create_repository_parser.set_defaults(func=run) + self.create_repository_parser.add_argument( + "repo_name", type=str, help="Name of the repository to be created" + ) + + def create_issue(self): + def run(args, c: Session): + forgejo = forgejo_from_args(args, c=c) + forgejo.login() + forgejo.create_issue(owner=args.owner, repo=args.repo) + + self.create_issue_parser = self.subparser.add_parser( + name="create_issue", + description="Create issue on a repository owned by someone on Forgejo", + help="Create issue on Forgejo", + ) + self.__add_credentials_parser(self.create_issue_parser) + self.create_issue_parser.set_defaults(func=run) + self.create_issue_parser.add_argument( + "owner", type=str, help="Owner of the repo" + ) + + self.create_issue_parser.add_argument( + "repo", type=str, help="Name of the repository" + ) + + + +class Cli: + def __init__(self): + c = Session() + self.c = c + self.parser = argparse.ArgumentParser( + description="Install and Bootstrap Forgejo and Hostea Dashboard" + ) + self.subparser = self.parser.add_subparsers() + self.check_env() + self.forgejo() + + def __add_credentials_parser(self, parser): + group = parser.add_argument_group("credentials", "User credentials") + group.add_argument("username", type=str, help="Forgejo user's username") + group.add_argument("password", type=str, help="Forgejo user's password") + group.add_argument("email", type=str, help="Forgejo user's email") + + def check_env(self): + def run(args, c: Session): + from .forgejo import Forgejo + + Forgejo.check_online(host=args.forgejo_host) + + self.check_env_parser = self.subparser.add_parser( + name="check_env", + description="Check and block until environment is ready", + help="Check and block until environment is ready", + ) + + self.check_env_parser.add_argument( + "forgejo_host", type=str, help="URI at which Forgejo is running" + ) + + self.check_env_parser.set_defaults(func=run) + + def forgejo(self): + self.forgejo = self.subparser.add_parser( + name="forgejo", + description="Forgejo", + help="Forgejo-related functionality", + ) + Forgejo(parser=self.forgejo, c=self.c) + + def parse(self): + return self.parser.parse_args() diff --git a/integration/csrf.py b/integration/csrf.py new file mode 100644 index 0000000..5c298bf --- /dev/null +++ b/integration/csrf.py @@ -0,0 +1,38 @@ +from html.parser import HTMLParser + + +class ParseCSRF(HTMLParser): + token: str = None + + def __init__(self, name): + HTMLParser.__init__(self) + self.name = name + + # @classmethod + # def dashboard_parser(cls) -> "ParseCSRF": + # return cls(name="csrfmiddlewaretoken") + # + # @classmethod + # def forgejo_parser(cls) -> "ParseCSRF": + # return cls(name="_csrf") + # + def handle_starttag(self, tag: str, attrs: (str, str)): + if self.token: + return + + if tag != "input": + return + + token = None + for index, (k, v) in enumerate(attrs): + if k == "value": + token = v + + if all([k == "name", v == self.name]): + if token: + self.token = token + return + for inner_index, (nk, nv) in enumerate(attrs, start=index): + if nk == "value": + self.token = nv + return diff --git a/integration/forgejo.py b/integration/forgejo.py new file mode 100755 index 0000000..1888dcb --- /dev/null +++ b/integration/forgejo.py @@ -0,0 +1,244 @@ +import os +import random +from urllib.parse import urlunparse, urlparse +from html.parser import HTMLParser +from time import sleep + +from requests import Session +from requests.auth import HTTPBasicAuth +import requests + +from .csrf import ParseCSRF + +# FORGEJO_USER = "root" +# FORGEJO_EMAIL = "root@example.com" +# FORGEJO_PASSWORD = "foobarpassword" +# HOST = "http://localhost:8080" +# +# REPOS = [] + + +class Forgejo: + def __init__(self, host: str, username: str, password: str, email: str, c: Session): + self.host = host + self.username = username + self.password = password + self.email = email + self.c = c + self.__csrf_key = "_csrf" + self.__logged_in = False + + def get_uri(self, path: str): + parsed = urlparse(self.host) + return urlunparse((parsed.scheme, parsed.netloc, path, "", "", "")) + + def get_api_uri(self, path: str): + parsed = urlparse(self.host) + return urlunparse( + ( + parsed.scheme, + f"{self.username}:{self.password}@{parsed.netloc}", + path, + "", + "", + "", + ) + ) + + @staticmethod + def check_online(host: str): + """ + Check if Forgejo instance is online + """ + count = 0 + parsed = urlparse(host) + url = urlunparse((parsed.scheme, parsed.netloc, "api/v1/nodeinfo", "", "", "")) + + while True: + try: + res = requests.get(url, allow_redirects=False) + if any([res.status_code == 302, res.status_code == 200]): + break + except: + sleep(2) + print(f"Retrying {count} time") + count += 1 + continue + + def install(self): + """ + Install Forgejo, first form that a user sees when a new instance is + deployed + """ + # cwd = os.environ.get("PWD") + # user = os.environ.get("USER") + payload = { + "db_type": "sqlite3", + "db_host": "localhost:3306", + "db_user": "root", + "db_passwd": "", + "db_name": "forgejo", + "ssl_mode": "disable", + "db_schema": "", + "charset": "utf8", + "db_path": "/data/gitea/gitea.db", + "app_name": "Forgejo:+Beyond+Coding+We+Forge", + "repo_root_path": "/data/git/repositories", + "lfs_root_path": "/data/git/lfs", + "run_user": "git", + "domain": "localhost", + "ssh_port": "22", + "http_port": "3000", + "app_url": "http://localhost:3000/", + "log_root_path": "/data/gitea/log", + "smtp_host": "", + "smtp_from": "", + "smtp_user": "", + "smtp_passwd": "", + "enable_federated_avatar": "on", + "enable_open_id_sign_in": "on", + "enable_open_id_sign_up": "on", + "default_allow_create_organization": "on", + "default_enable_timetracking": "on", + "no_reply_address": "noreply.localhost", + "password_algorithm": "pbkdf2", + "admin_name": "", + "admin_passwd": "", + "admin_confirm_passwd": "", + "admin_email": "", + } + + resp = self.c.post(self.get_uri(""), data=payload) + sleep(10) + + def get_csrf_token(self, url: str) -> str: + """ + Get CSRF token at a URI + """ + resp = self.c.get(url, allow_redirects=False) + if resp.status_code != 200 and resp.status_code != 302: + print(resp.status_code, resp.text) + raise Exception(f"Can't get csrf token: {resp.status_code}") + parser = ParseCSRF(name=self.__csrf_key) + parser.feed(resp.text) + csrf = parser.token + return csrf + + def register(self): + """ + Register User + """ + url = self.get_uri("/user/sign_up") + csrf = self.get_csrf_token(url) + payload = { + "_csrf": csrf, + "user_name": self.username, + "password": self.password, + "retype": self.password, + "email": self.email, + } + self.c.post(url, data=payload, allow_redirects=False) + + def login(self): + """ + Login, must be called at least once before performing authenticated + operations + """ + if self.__logged_in: + return + url = self.get_uri("/user/login") + csrf = self.get_csrf_token(url) + payload = { + "_csrf": csrf, + "user_name": self.username, + "password": self.password, + "remember": "on", + } + resp = self.c.post(url, data=payload, allow_redirects=False) + if any( + [resp.status_code == 302, resp.status_code == 200, resp.status_code == 303] + ): + print("User logged in") + self.__logged_in = True + return + + raise Exception( + f"[ERROR] Authentication failed. status code {resp.status_code}" + ) + + def create_repository(self, name: str): + """ + Create repository + """ + self.login() + + def get_repository_payload(csrf: str, name: str, user_id: str): + data = { + "_csrf": csrf, + "uid": user_id, + "repo_name": name, + "description": f"this repository is named {name}", + "repo_template": "", + "issue_labels": "", + "gitignores": "", + "license": "", + "readme": "Default", + "default_branch": "master", + "trust_model": "default", + } + return data + + url = self.get_uri("/repo/create") + user_id = self.c.get(self.get_api_uri("/api/v1/user")).json()["id"] + + csrf = self.get_csrf_token(url) + data = get_repository_payload(csrf, name, user_id=user_id) + + resp = self.c.post(url, data=data, allow_redirects=False) + print(f"Created repository {name}") + if ( + resp.status_code != 302 + and resp.status_code != 200 + and resp.status_code != 303 + ): + raise Exception( + f"Error while creating repository: {name} {resp.status_code}" + ) + + + def create_issue(self, owner: str, repo: str): + """ + Create issue + """ + self.login() + + def create_issue_payload(csrf: str, title: str, body: str): + data = { + "_csrf": csrf, + "title": title, + "content": body, + "search": "", + "label_ids": "", + "milestone_id": "", + "project_id": "", + "assignee_ids": "", + "redirect_after_creation": "", + } + + + return data + + url = self.get_uri(f"/{owner}/{repo}/issues/new") + csrf = self.get_csrf_token(url) + data = create_issue_payload(csrf, "my issue", "my body") + + resp = self.c.post(url, data=data, allow_redirects=False) + print(f"Created issue") + if ( + resp.status_code != 302 + and resp.status_code != 200 + and resp.status_code != 303 + ): + raise Exception( + f"Error while creating issue: {resp.status_code}" + ) diff --git a/integration/lib.sh b/integration/lib.sh new file mode 100755 index 0000000..e2affcb --- /dev/null +++ b/integration/lib.sh @@ -0,0 +1,113 @@ +is_ci(){ + if [ -z ${CI+x} ]; + then + return 1 + else + return 0 + fi +} + +if is_ci +then + FORGEJO_URL="http://forgejo:3000" + FORGEJO_SSH_URL="ssh://git@forgejo:22" +else + FORGEJO_URL="http://localhost:3000" + FORGEJO_SSH_URL="ssh://git@localhost:22" +fi + +readonly FORGEJO_ROOT_USERNAME=root +readonly FORGEJO_ROOT_EMAIL="$FORGEJO_ROOT_USERNAME@example.org" +readonly FORGEJO_ROOT_PASSOWRD=supercomplicatedpassword + +readonly FORGEJO_USER1_USERNAME=owner_user +readonly FORGEJO_USER1_PASSWORD=supercomplicatedpassword +readonly FORGEJO_USER1_EMAIL="$FORGEJO_USER1_USERNAME@example.org" +readonly FORGEJO_USER1_SUPPORT_REPO="support" + +readonly FORGEJO_TESTUSER_USERNAME=test_user +readonly FORGEJO_TESTUSER_PASSWORD=supercomplicatedpassword +readonly FORGEJO_TESTUSER_EMAIL="$FORGEJO_TESTUSER_USERNAME@example.org" + + +wait_for_env() { + python -m integration \ + check_env $FORGEJO_URL +} + +# register root user on Forgejo to simulate Hoste admin and integrate SSO +forgejo_root(){ + python -m integration \ + forgejo install \ + $FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \ + $FORGEJO_ROOT_EMAIL \ + $FORGEJO_URL + python -m integration \ + forgejo register \ + $FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \ + $FORGEJO_ROOT_EMAIL \ + $FORGEJO_URL + python -m integration \ + forgejo login \ + $FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \ + $FORGEJO_ROOT_EMAIL \ + $FORGEJO_URL +} + + +# register user "Hostea" on Forgejo and create support repository +init_users_repo() { + python -m integration \ + forgejo register \ + $FORGEJO_USER1_USERNAME $FORGEJO_USER1_PASSWORD \ + $FORGEJO_USER1_EMAIL \ + $FORGEJO_URL + python -m integration \ + forgejo login \ + $FORGEJO_USER1_USERNAME $FORGEJO_USER1_PASSWORD \ + $FORGEJO_USER1_EMAIL \ + $FORGEJO_URL + python -m integration \ + forgejo create_repo \ + $FORGEJO_USER1_USERNAME $FORGEJO_USER1_PASSWORD \ + $FORGEJO_USER1_EMAIL \ + $FORGEJO_URL \ + $FORGEJO_USER1_SUPPORT_REPO + + python -m integration \ + forgejo register \ + $FORGEJO_TESTUSER_USERNAME $FORGEJO_TESTUSER_PASSWORD \ + $FORGEJO_TESTUSER_EMAIL \ + $FORGEJO_URL + + python -m integration \ + forgejo create_issue \ + $FORGEJO_TESTUSER_USERNAME $FORGEJO_TESTUSER_PASSWORD \ + $FORGEJO_USER1_EMAIL \ + $FORGEJO_URL \ + $FORGEJO_USER1_USERNAME \ + $FORGEJO_USER1_SUPPORT_REPO +} + +setup_env() { +# mkdir tmp/ || true +# nohup python manage.py runserver > /dev/null 2>&1 & +# SERVER_PID=$! +# echo $SERVER_PID > $SERVER_PID_FILE + echo "TODO" +} + +teardown_env() { + echo "TODO" + #kill $(cat $SERVER_PID_FILE) +} + +docker_compose_up() { + echo "[*] Starting Forgejo" + docker-compose -f docker-compose-dev-deps.yml up +} + +docker_compose_down() { + docker-compose -f docker-compose-dev-deps.yml down + docker-compose -f docker-compose-dev-deps.yml down --remove-orphans +} diff --git a/integration/tests.sh b/integration/tests.sh new file mode 100755 index 0000000..594ab4d --- /dev/null +++ b/integration/tests.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -Exeuo pipefail + +source integration/lib.sh + +main() { + teardown_env || true + setup_env + wait_for_env + forgejo_root + init_users_repo + teardown_env + echo "All Good! :)" +} + +main