pg-api 0.1.0

A high-performance PostgreSQL REST API driver with rate limiting, connection pooling, and observability
#!/usr/bin/env python3
"""
Create GitLab repository via API and push the pg-api project
"""

import os
import sys
import json
import subprocess
import argparse
import requests
from typing import Optional, Dict, Any

class GitLabRepoCreator:
    """Create and configure GitLab repository for pg-api"""
    
    def __init__(self, token: str, gitlab_url: str = "https://gitlab.com"):
        self.token = token
        self.gitlab_url = gitlab_url.rstrip('/')
        self.api_url = f"{self.gitlab_url}/api/v4"
        self.headers = {
            "PRIVATE-TOKEN": token,
            "Content-Type": "application/json"
        }
    
    def get_current_user(self) -> Dict[str, Any]:
        """Get current user information"""
        response = requests.get(
            f"{self.api_url}/user",
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()
    
    def check_repo_exists(self, project_name: str, username: str) -> bool:
        """Check if repository already exists"""
        response = requests.get(
            f"{self.api_url}/projects/{username}%2F{project_name}",
            headers=self.headers
        )
        return response.status_code == 200
    
    def create_repository(self, 
                         name: str = "pg-api",
                         description: str = None,
                         visibility: str = "public",
                         initialize_readme: bool = False) -> Dict[str, Any]:
        """Create a new GitLab repository"""
        
        if description is None:
            description = "High-performance PostgreSQL REST API driver built with Rust"
        
        data = {
            "name": name,
            "description": description,
            "visibility": visibility,
            "initialize_with_readme": initialize_readme,
            "issues_enabled": True,
            "merge_requests_enabled": True,
            "wiki_enabled": True,
            "snippets_enabled": False,
            "container_registry_enabled": True,
            "packages_enabled": True,
            "auto_devops_enabled": False
        }
        
        response = requests.post(
            f"{self.api_url}/projects",
            headers=self.headers,
            json=data
        )
        
        if response.status_code == 201:
            return response.json()
        elif response.status_code == 400:
            error = response.json()
            if "has already been taken" in str(error):
                print(f"❌ Repository '{name}' already exists")
                return None
        
        response.raise_for_status()
        return response.json()
    
    def add_topics(self, project_id: int, topics: list) -> bool:
        """Add topics/tags to the repository"""
        data = {"topics": topics}
        response = requests.put(
            f"{self.api_url}/projects/{project_id}",
            headers=self.headers,
            json=data
        )
        return response.status_code == 200
    
    def create_branch_protection(self, project_id: int, branch: str = "main") -> bool:
        """Set up branch protection rules"""
        data = {
            "name": branch,
            "push_access_level": 30,  # Developer + Maintainer
            "merge_access_level": 30,  # Developer + Maintainer
            "allow_force_push": False,
            "code_owner_approval_required": False
        }
        
        response = requests.post(
            f"{self.api_url}/projects/{project_id}/protected_branches",
            headers=self.headers,
            json=data
        )
        return response.status_code in [201, 409]  # 409 if already protected
    
    def setup_ci_variables(self, project_id: int, variables: Dict[str, str]) -> bool:
        """Add CI/CD variables to the project"""
        success = True
        for key, value in variables.items():
            data = {
                "key": key,
                "value": value,
                "protected": False,
                "masked": "TOKEN" in key or "SECRET" in key or "KEY" in key
            }
            
            response = requests.post(
                f"{self.api_url}/projects/{project_id}/variables",
                headers=self.headers,
                json=data
            )
            
            if response.status_code not in [201, 409]:  # 409 if already exists
                success = False
                print(f"⚠️  Failed to add variable {key}")
        
        return success
    
    def check_ssh_key(self) -> bool:
        """Check if SSH key is configured for GitLab"""
        try:
            # Check if SSH key exists
            ssh_dir = os.path.expanduser("~/.ssh")
            has_key = any(
                os.path.exists(os.path.join(ssh_dir, key))
                for key in ["id_rsa", "id_ed25519", "id_ecdsa"]
            )
            
            if not has_key:
                return False
            
            # Test SSH connection to GitLab
            result = subprocess.run(
                ["ssh", "-T", "git@gitlab.com"],
                capture_output=True,
                text=True,
                timeout=5
            )
            
            # GitLab returns exit code 1 but with success message
            return "Welcome to GitLab" in result.stderr
        except:
            return False
    
    def push_to_gitlab(self, repo_url: str, branch: str = "main", use_ssh: bool = None) -> bool:
        """Push local repository to GitLab"""
        try:
            # Check if git is initialized
            if not os.path.exists(".git"):
                subprocess.run(["git", "init"], check=True)
                subprocess.run(["git", "add", "."], check=True)
                subprocess.run(["git", "commit", "-m", "Initial commit: pg-api PostgreSQL REST API driver"], check=True)
            
            # Check if we need to rename branch to main
            current_branch = subprocess.run(
                ["git", "branch", "--show-current"],
                capture_output=True,
                text=True,
                check=True
            ).stdout.strip()
            
            if current_branch != branch:
                subprocess.run(["git", "branch", "-M", branch], check=True)
            
            # Add GitLab remote
            remotes = subprocess.run(
                ["git", "remote"],
                capture_output=True,
                text=True
            ).stdout.strip().split('\n')
            
            if "origin" in remotes:
                subprocess.run(["git", "remote", "remove", "origin"], check=True)
            
            subprocess.run(["git", "remote", "add", "origin", repo_url], check=True)
            
            # Push to GitLab
            subprocess.run(["git", "push", "-u", "origin", branch], check=True)
            
            return True
        except subprocess.CalledProcessError as e:
            print(f"❌ Git operation failed: {e}")
            return False


def main():
    parser = argparse.ArgumentParser(description="Create pg-api repository on GitLab")
    parser.add_argument(
        "--token",
        required=True,
        help="GitLab personal access token (needs 'api' scope)"
    )
    parser.add_argument(
        "--gitlab-url",
        default="https://gitlab.com",
        help="GitLab instance URL (default: https://gitlab.com)"
    )
    parser.add_argument(
        "--name",
        default="pg-api",
        help="Repository name (default: pg-api)"
    )
    parser.add_argument(
        "--visibility",
        choices=["public", "private", "internal"],
        default="public",
        help="Repository visibility (default: public)"
    )
    parser.add_argument(
        "--push",
        action="store_true",
        help="Push local repository to GitLab after creation"
    )
    parser.add_argument(
        "--setup-ci",
        action="store_true",
        help="Set up CI/CD variables"
    )
    parser.add_argument(
        "--protect-main",
        action="store_true",
        help="Set up branch protection for main branch"
    )
    
    args = parser.parse_args()
    
    # Create GitLab repository
    creator = GitLabRepoCreator(args.token, args.gitlab_url)
    
    try:
        # Get user info
        print("🔍 Getting user information...")
        user = creator.get_current_user()
        username = user["username"]
        print(f"✅ Authenticated as: {username}")
        
        # Check if repo exists
        if creator.check_repo_exists(args.name, username):
            print(f"ℹ️  Repository '{args.name}' already exists")
            repo_url = f"{args.gitlab_url}/{username}/{args.name}.git"
        else:
            # Create repository
            print(f"📦 Creating repository '{args.name}'...")
            project = creator.create_repository(
                name=args.name,
                visibility=args.visibility
            )
            
            if project:
                print(f"✅ Repository created: {project['web_url']}")
                project_id = project["id"]
                repo_url = project["ssh_url_to_repo"]
                
                # Add topics
                print("🏷️  Adding topics...")
                topics = [
                    "postgresql", "rust", "api", "rest-api", 
                    "database", "driver", "axum", "tokio"
                ]
                if creator.add_topics(project_id, topics):
                    print("✅ Topics added")
                
                # Set up branch protection
                if args.protect_main:
                    print("🔒 Setting up branch protection...")
                    if creator.create_branch_protection(project_id):
                        print("✅ Branch protection enabled for 'main'")
                
                # Set up CI variables
                if args.setup_ci:
                    print("⚙️  Setting up CI/CD variables...")
                    ci_vars = {
                        "CARGO_HOME": "${CI_PROJECT_DIR}/.cargo",
                        "RUST_BACKTRACE": "1"
                    }
                    if creator.setup_ci_variables(project_id, ci_vars):
                        print("✅ CI/CD variables configured")
            else:
                sys.exit(1)
        
        # Push to GitLab
        if args.push:
            print(f"📤 Preparing to push to GitLab...")
            
            # Check if SSH key is available
            has_ssh = creator.check_ssh_key()
            
            if has_ssh:
                print("🔑 SSH key detected, using SSH for push...")
                push_url = repo_url  # Use SSH URL directly
            else:
                print("🔐 No SSH key found, using HTTPS with token...")
                # Use HTTPS URL with token for authentication
                https_url = repo_url.replace("git@gitlab.com:", "https://gitlab.com/").replace(".git", "")
                push_url = https_url.replace("https://", f"https://oauth2:{args.token}@") + ".git"
            
            if creator.push_to_gitlab(push_url):
                print("✅ Code pushed successfully!")
                web_url = repo_url.replace("git@gitlab.com:", "https://gitlab.com/").replace(".git", "")
                print(f"\n🎉 Repository ready at: {web_url}")
                print(f"\n📝 Next steps:")
                print(f"  1. Visit your repository: {web_url}")
                print(f"  2. Check the CI/CD pipeline: {web_url}/-/pipelines")
                print(f"  3. Configure additional settings: {web_url}/-/settings/general")
            else:
                print("⚠️  Failed to push code. You can manually push with:")
                if has_ssh:
                    print(f"  git remote add origin {repo_url}")
                    print(f"  git push -u origin main")
                else:
                    print("  # Option 1: Configure SSH key first")
                    print("  ssh-keygen -t ed25519 -C 'your-email@example.com'")
                    print("  # Add the public key to GitLab: https://gitlab.com/-/profile/keys")
                    print(f"  git remote add origin {repo_url}")
                    print(f"  git push -u origin main")
                    print("\n  # Option 2: Use HTTPS with token")
                    https_url = repo_url.replace("git@gitlab.com:", "https://gitlab.com/").replace(".git", "")
                    print(f"  git remote add origin https://oauth2:{args.token}@gitlab.com/{username}/{args.name}.git")
                    print(f"  git push -u origin main")
        else:
            # Check SSH availability for manual instructions
            has_ssh = creator.check_ssh_key()
            print(f"\n📝 To push your code manually:")
            
            if has_ssh:
                print("🔑 SSH key detected. You can push directly:")
                print(f"  git remote add origin {repo_url}")
                print(f"  git push -u origin main")
            else:
                print("⚠️  No SSH key detected. You have two options:")
                print("\n  Option 1: Configure SSH key (recommended)")
                print("  ssh-keygen -t ed25519 -C 'your-email@example.com'")
                print("  # Add the public key to: https://gitlab.com/-/profile/keys")
                print(f"  git remote add origin {repo_url}")
                print(f"  git push -u origin main")
                print("\n  Option 2: Use HTTPS with token")
                print(f"  git remote add origin https://oauth2:{args.token}@gitlab.com/{username}/{args.name}.git")
                print(f"  git push -u origin main")
        
    except requests.exceptions.RequestException as e:
        print(f"❌ API request failed: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"❌ Unexpected error: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()