From 9d96296fddb95742a5f96066b166e2513758d24d Mon Sep 17 00:00:00 2001 From: "Trez.One" Date: Fri, 17 Oct 2025 08:52:25 -0400 Subject: [PATCH 1/2] Gitea reverse Terraform script. --- gitea/gitea-terraforming.py | 274 ++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100755 gitea/gitea-terraforming.py diff --git a/gitea/gitea-terraforming.py b/gitea/gitea-terraforming.py new file mode 100755 index 0000000..2159f37 --- /dev/null +++ b/gitea/gitea-terraforming.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +gitea-terraforming: Reverse Terraform for Gitea (OpenTofu compatible) + +Generates Terraform HCL for: +- Users +- Organizations +- Repositories (user & org) +- Branch protections + +Output files are automatically split per resource type: +gitea-.tf + +Supports import generation: +- Modern import blocks (--modern-import-block) +- Shell script terraform import + +Usage example: + python gitea_terraforming.py --api https://gitea.example.com --token --out-dir ./gitea_tf +""" + +import argparse, os, sys, time, json, re +from typing import Any, Dict, List, Optional +from datetime import datetime +import requests + +def slugify(s: str) -> str: + s = re.sub(r'[^0-9a-zA-Z_-]', '_', s) + return re.sub('_+', '_', s).strip('_').lower() + +class GiteaClient: + def __init__(self, api_base: str, token: str, verify: bool = True): + self.base = api_base.rstrip('/') + self.s = requests.Session() + self.s.headers.update({ + 'Authorization': f'token {token}', + 'Accept': 'application/json', + 'User-Agent': 'gitea-terraforming/0.1' + }) + self.verify = verify + + def _get(self, path: str, params: Optional[dict] = None): + url = f"{self.base}{path}" + out = [] + page = 1 + while True: + qp = params.copy() if params else {} + qp.update({'page': page, 'limit': 100}) + resp = self.s.get(url, params=qp, verify=self.verify, timeout=30) + if resp.status_code == 404: + return [] + if resp.status_code == 429: + retry = int(resp.headers.get('Retry-After', '5')) + time.sleep(retry) + continue + resp.raise_for_status() + data = resp.json() + if isinstance(data, list): + out.extend(data) + if len(data) < 100: + break + page += 1 + else: + return data + return out + + def list_orgs(self) -> List[dict]: + return self._get("/api/v1/orgs") + + def list_users(self) -> List[dict]: + return self._get("/api/v1/admin/users") + + def list_user_repos(self, user: str) -> List[dict]: + return self._get(f"/api/v1/users/{user}/repos") + + def list_org_repos(self, org: str) -> List[dict]: + return self._get(f"/api/v1/orgs/{org}/repos") + + def list_branch_protections(self, owner: str, repo: str) -> List[dict]: + return self._get(f"/api/v1/repos/{owner}/{repo}/branch_protections") + +def hcl_block(resource_type: str, name: str, attrs: dict, comment: Optional[str] = None) -> str: + lines = [] + if comment: + lines.append(f"# {comment}") + lines.append(f'resource "{resource_type}" "{name}" ' + "{") + for k, v in attrs.items(): + if v is None: + continue + if isinstance(v, bool): + lines.append(f" {k} = {str(v).lower()}") + elif isinstance(v, (int, float)): + lines.append(f" {k} = {v}") + elif isinstance(v, str): + safe = v.replace('"', '\\"') + lines.append(f' {k} = "{safe}"') + elif isinstance(v, list): + joined = ", ".join([f'"{x}"' for x in v]) + lines.append(f" {k} = [{joined}]") + else: + lines.append(f' # {k} = {json.dumps(v)}') + lines.append("}\n") + return "\n".join(lines) + +def modern_import_block(to: str, ident: str) -> str: + return f'import {{\n to = {to}\n id = "{ident}"\n}}\n' + +def generate(api: str, token: str, out_dir: str, modern: bool = False, dry: bool = False): + client = GiteaClient(api, token) + os.makedirs(out_dir, exist_ok=True) + imports = [] + files: dict = {} + + orgs = client.list_orgs() + org_buf = [] + for o in orgs: + uname = o.get("username") or o.get("user_name") or o.get("name") + rname = f"org_{slugify(uname)}" + attrs = {"name": uname} + org_buf.append(hcl_block("gitea_organization", rname, attrs, comment=f"source id={o.get('id')}")) + imports.append((f"gitea_organization.{rname}", uname)) + if org_buf: + files["orgs"] = "\n".join(org_buf) + + users = [] + try: + users = client.list_users() + except Exception as e: + print(f"Warning: cannot list users: {e}", file=sys.stderr) + user_buf = [] + for u in users: + uname = u.get("login") or u.get("username") + if not uname: + continue + rname = f"user_{slugify(uname)}" + attrs = { + "username": uname, + "email": u.get("email"), + "full_name": u.get("full_name"), + "is_admin": u.get("is_admin") + } + user_buf.append(hcl_block("gitea_user", rname, attrs, comment=f"source id={u.get('id')}")) + imports.append((f"gitea_user.{rname}", uname)) + if user_buf: + files["users"] = "\n".join(user_buf) + + repo_buf = [] + for u in users: + uname = u.get("login") or u.get("username") + if not uname: + continue + try: + repos = client.list_user_repos(uname) + except Exception: + repos = [] + for r in repos: + rname = f"repo_{slugify(uname)}_{slugify(r['name'])}" + attrs = { + "owner": uname, + "name": r["name"], + "private": r.get("private", False), + "description": r.get("description") + } + repo_buf.append(hcl_block("gitea_repository", rname, attrs, comment=f"source id={r.get('id')}")) + imports.append((f"gitea_repository.{rname}", f"{uname}/{r['name']}")) + for o in orgs: + uname = o.get("username") or o.get("user_name") or o.get("name") + if not uname: + continue + try: + repos = client.list_org_repos(uname) + except Exception: + repos = [] + for r in repos: + rname = f"repo_{slugify(uname)}_{slugify(r['name'])}" + attrs = { + "owner": uname, + "name": r["name"], + "private": r.get("private", False), + "description": r.get("description") + } + repo_buf.append(hcl_block("gitea_repository", rname, attrs, comment=f"source id={r.get('id')}")) + imports.append((f"gitea_repository.{rname}", f"{uname}/{r['name']}")) + if repo_buf: + files["repos"] = "\n".join(repo_buf) + + bp_buf = [] + for to, ident in imports: + if not to.startswith("gitea_repository."): + continue + owner, repo = ident.split("/", 1) + try: + bps = client.list_branch_protections(owner, repo) + except Exception: + bps = [] + for bp in bps: + branch_name = bp.get("branch_name") or bp.get("branch") or bp.get("name") + if branch_name is None: + continue + bn = f"branch_protection_{slugify(owner)}_{slugify(repo)}_{slugify(branch_name)}" + attrs = { + "repository": repo, + "owner": owner, + "branch": branch_name, + "enable_status_check": bp.get("enable_status_check", False), + "required_approvals": bp.get("required_approvals", 0), + "enable_merge_whitelist": bp.get("enable_merge_whitelist", False), + } + bp_buf.append(hcl_block("gitea_branch_protection", bn, attrs, comment=f"protect branch {branch_name}")) + imports.append((f"gitea_branch_protection.{bn}", f"{owner}/{repo}/{branch_name}")) + if bp_buf: + files["branches"] = "\n".join(bp_buf) + + for rtype, content in files.items(): + fname = f"gitea-{rtype}.tf" + fpath = os.path.join(out_dir, fname) + if dry: + print(f"--- {fpath} ---") + print(content) + else: + with open(fpath, "w", encoding="utf-8") as f: + f.write(f"// Generated by gitea-terraforming (OpenTofu compatible)\n\n") + f.write(content) + print(f"Wrote {fname}", file=sys.stderr) + + if modern: + imps = "\n".join([modern_import_block(to, ident) for to, ident in imports]) + imppath = os.path.join(out_dir, "imports.tf") + if dry: + print("--- imports.tf ---") + print(imps) + else: + with open(imppath, "w", encoding="utf-8") as f: + f.write("// Import blocks\n\n") + f.write(imps) + print(f"Wrote imports.tf", file=sys.stderr) + else: + lines = ["#!/usr/bin/env bash", "set -euo pipefail"] + lines += [f'terraform import {to} "{ident}"' for to, ident in imports] + imppath = os.path.join(out_dir, "terraform_imports.sh") + if dry: + print("--- terraform_imports.sh ---") + print("\n".join(lines)) + else: + with open(imppath, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + os.chmod(imppath, 0o755) + print(f"Wrote terraform_imports.sh", file=sys.stderr) + +def main(): + parser = argparse.ArgumentParser( + description="gitea-terraforming: Reverse Terraform for Gitea (OpenTofu compatible)\n\n" + "Generates Terraform HCL for users, organizations, repositories, and branch protections.\n" + "Output files are split per resource type and timestamped.\n\n" + "Example usage:\n" + " python gitea_terraforming.py --api https://gitea.example.com --token --out-dir ./gitea_tf\n", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument("--api", required=True, help="Gitea API URL (e.g., https://gitea.example.com)") + parser.add_argument("--token", default=os.environ.get("GITEA_TOKEN"), + help="Gitea admin token (or set GITEA_TOKEN environment variable)") + parser.add_argument("--out-dir", default="./gitea_tf", help="Directory to write Terraform files") + parser.add_argument("--modern-import-block", action="store_true", + help="Generate modern OpenTofu import blocks (imports.tf) instead of shell script") + parser.add_argument("--dry-run", action="store_true", help="Print output instead of writing files") + args = parser.parse_args() + + if not args.token: + parser.error("Missing Gitea token: provide --token or set GITEA_TOKEN environment variable") + + generate(args.api, args.token, args.out_dir, modern=args.modern_import_block, dry=args.dry_run) + +if __name__ == "__main__": + main() -- 2.52.0 From cbb66ed806819555e102288c3d8f408f1caac0a9 Mon Sep 17 00:00:00 2001 From: "Trez.One" Date: Fri, 17 Oct 2025 08:52:48 -0400 Subject: [PATCH 2/2] Resources and .env example. --- gitea/env.example | 1 + gitea/gitea-orgs.tf | 6 +++ gitea/gitea-repos.tf | 121 +++++++++++++++++++++++++++++++++++++++++++ gitea/gitea-users.tf | 33 ++++++++++++ gitea/imports.tf | 101 ++++++++++++++++++++++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 gitea/env.example create mode 100644 gitea/gitea-orgs.tf create mode 100644 gitea/gitea-repos.tf create mode 100644 gitea/gitea-users.tf create mode 100644 gitea/imports.tf diff --git a/gitea/env.example b/gitea/env.example new file mode 100644 index 0000000..9db5d83 --- /dev/null +++ b/gitea/env.example @@ -0,0 +1 @@ +GITEA_TOKEN="" diff --git a/gitea/gitea-orgs.tf b/gitea/gitea-orgs.tf new file mode 100644 index 0000000..c080dce --- /dev/null +++ b/gitea/gitea-orgs.tf @@ -0,0 +1,6 @@ +// Generated by gitea-terraforming (OpenTofu compatible) + +# source id=52 +resource "gitea_organization" "org_trez" { + name = "Trez" +} diff --git a/gitea/gitea-repos.tf b/gitea/gitea-repos.tf new file mode 100644 index 0000000..51b0cdc --- /dev/null +++ b/gitea/gitea-repos.tf @@ -0,0 +1,121 @@ +// Generated by gitea-terraforming (OpenTofu compatible) + +# source id=5 +resource "gitea_repository" "repo_trez_rinoa-docker" { + owner = "Trez" + name = "rinoa-docker" + private = false + description = "" +} + +# source id=9 +resource "gitea_repository" "repo_trez_meraki-naemon" { + owner = "Trez" + name = "meraki-naemon" + private = false + description = "" +} + +# source id=13 +resource "gitea_repository" "repo_trez_benedikta-ovos" { + owner = "Trez" + name = "benedikta-ovos" + private = false + description = "" +} + +# source id=16 +resource "gitea_repository" "repo_trez_rikku-home-assistant" { + owner = "Trez" + name = "rikku-home-assistant" + private = false + description = "" +} + +# source id=17 +resource "gitea_repository" "repo_trez_tar-valon-terraform" { + owner = "Trez" + name = "tar-valon-terraform" + private = true + description = "" +} + +# source id=18 +resource "gitea_repository" "repo_trez_hugo_it-services" { + owner = "Trez" + name = "hugo_it-services" + private = false + description = "" +} + +# source id=19 +resource "gitea_repository" "repo_trez_docker-mods-uptime-kuma-timeout-fix" { + owner = "Trez" + name = "docker-mods-uptime-kuma-timeout-fix" + private = false + description = "Documentation and Examples of base container modifications" +} + +# source id=21 +resource "gitea_repository" "repo_trez_tar-valon-ansible" { + owner = "Trez" + name = "tar-valon-ansible" + private = false + description = "" +} + +# source id=22 +resource "gitea_repository" "repo_trez_congo-hindi-gujarati" { + owner = "Trez" + name = "congo-hindi-gujarati" + private = false + description = "A powerful, lightweight theme for Hugo built with Tailwind CSS." +} + +# source id=26 +resource "gitea_repository" "repo_trez_action-home-assistant" { + owner = "Trez" + name = "action-home-assistant" + private = false + description = "🚀 Frenck's GitHub Action for running a Home Assistant Core configuration check" +} + +# source id=27 +resource "gitea_repository" "repo_trez_renovate-config" { + owner = "Trez" + name = "renovate-config" + private = false + description = "" +} + +# source id=31 +resource "gitea_repository" "repo_trez_hc-vault-env" { + owner = "Trez" + name = "hc-vault-env" + private = false + description = "" +} + +# source id=32 +resource "gitea_repository" "repo_trez_docker-select-image-pull" { + owner = "Trez" + name = "docker-select-image-pull" + private = false + description = "" +} + +# source id=33 +resource "gitea_repository" "repo_trez_gitea-auto-pr" { + owner = "Trez" + name = "gitea-auto-pr" + private = false + description = "" +} + +# source id=34 +resource "gitea_repository" "repo_trez_ultima-ai" { + owner = "Trez" + name = "ultima-ai" + private = true + description = "" +} diff --git a/gitea/gitea-users.tf b/gitea/gitea-users.tf new file mode 100644 index 0000000..94be204 --- /dev/null +++ b/gitea/gitea-users.tf @@ -0,0 +1,33 @@ +// Generated by gitea-terraforming (OpenTofu compatible) + +# source id=3 +resource "gitea_user" "user_gitea-sonarqube-bot" { + username = "gitea-sonarqube-bot" + email = "trezone@vivaldi.net" + full_name = "" + is_admin = false +} + +# source id=51 +resource "gitea_user" "user_renovate-bot" { + username = "renovate-bot" + email = "charish2k1@gmail.com" + full_name = "" + is_admin = false +} + +# source id=1 +resource "gitea_user" "user_root" { + username = "root" + email = "noreply@trez.wtf" + full_name = "" + is_admin = true +} + +# source id=2 +resource "gitea_user" "user_trez_one" { + username = "Trez.One" + email = "charish.patel@trez.wtf" + full_name = "" + is_admin = false +} diff --git a/gitea/imports.tf b/gitea/imports.tf new file mode 100644 index 0000000..62be673 --- /dev/null +++ b/gitea/imports.tf @@ -0,0 +1,101 @@ +// Import blocks + +import { + to = gitea_organization.org_trez + id = "Trez" +} + +import { + to = gitea_user.user_gitea-sonarqube-bot + id = "gitea-sonarqube-bot" +} + +import { + to = gitea_user.user_renovate-bot + id = "renovate-bot" +} + +import { + to = gitea_user.user_root + id = "root" +} + +import { + to = gitea_user.user_trez_one + id = "Trez.One" +} + +import { + to = gitea_repository.repo_trez_rinoa-docker + id = "Trez/rinoa-docker" +} + +import { + to = gitea_repository.repo_trez_meraki-naemon + id = "Trez/meraki-naemon" +} + +import { + to = gitea_repository.repo_trez_benedikta-ovos + id = "Trez/benedikta-ovos" +} + +import { + to = gitea_repository.repo_trez_rikku-home-assistant + id = "Trez/rikku-home-assistant" +} + +import { + to = gitea_repository.repo_trez_tar-valon-terraform + id = "Trez/tar-valon-terraform" +} + +import { + to = gitea_repository.repo_trez_hugo_it-services + id = "Trez/hugo_it-services" +} + +import { + to = gitea_repository.repo_trez_docker-mods-uptime-kuma-timeout-fix + id = "Trez/docker-mods-uptime-kuma-timeout-fix" +} + +import { + to = gitea_repository.repo_trez_tar-valon-ansible + id = "Trez/tar-valon-ansible" +} + +import { + to = gitea_repository.repo_trez_congo-hindi-gujarati + id = "Trez/congo-hindi-gujarati" +} + +import { + to = gitea_repository.repo_trez_action-home-assistant + id = "Trez/action-home-assistant" +} + +import { + to = gitea_repository.repo_trez_renovate-config + id = "Trez/renovate-config" +} + +import { + to = gitea_repository.repo_trez_hc-vault-env + id = "Trez/hc-vault-env" +} + +import { + to = gitea_repository.repo_trez_docker-select-image-pull + id = "Trez/docker-select-image-pull" +} + +import { + to = gitea_repository.repo_trez_gitea-auto-pr + id = "Trez/gitea-auto-pr" +} + +import { + to = gitea_repository.repo_trez_ultima-ai + id = "Trez/ultima-ai" +} -- 2.52.0