diff --git a/.github/workflows/fly.yml b/.github/workflows/fly.yml new file mode 100644 index 0000000..5a5f96e --- /dev/null +++ b/.github/workflows/fly.yml @@ -0,0 +1,93 @@ +# ABOUTME: GitHub Actions workflow for automated Fly.io deployment +# ABOUTME: Follows Fly.io best practices with simple flyctl deploy commands + +name: Deploy to Fly.io + +on: + push: + branches: [main] + +env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Perl + uses: shogo82148/actions-setup-perl@v1 + with: + perl-version: '5.38' + + - name: Install Test Dependencies + run: cpanm --quiet --notest Test2::V0 + + - name: Run Infrastructure Tests + run: prove -v t/ + + setup-infrastructure: + name: Setup Infrastructure + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Setup Perl + uses: shogo82148/actions-setup-perl@v1 + with: + perl-version: '5.38' + + - name: Create PostgreSQL Database + run: | + # Check if magnet-postgres already exists + if ! flyctl apps list | grep -q "magnet-postgres"; then + echo "Creating PostgreSQL database..." + flyctl postgres create --name magnet-postgres --region ord --initial-cluster-size 1 + else + echo "PostgreSQL database already exists" + fi + + - name: Create Volumes + run: perl scripts/create-volumes.pl --production + + - name: Setup Deploy Tokens + run: perl scripts/setup-deploy-tokens.pl + + deploy: + name: Deploy Applications + runs-on: ubuntu-latest + needs: setup-infrastructure + strategy: + matrix: + include: + - app: magnet-9rl + config: servers/magnet-9rl/fly.toml + region: ord + - app: magnet-1eu + config: servers/magnet-1eu/fly.toml + region: ams + - app: magnet-atheme + config: servers/magnet-atheme/fly.toml + region: ord + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Attach PostgreSQL to magnet-atheme + if: matrix.app == 'magnet-atheme' + run: | + # Check if already attached + if ! flyctl postgres attach --app magnet-atheme magnet-postgres --dry-run 2>/dev/null; then + echo "Attaching PostgreSQL to magnet-atheme..." + flyctl postgres attach --app magnet-atheme magnet-postgres + else + echo "PostgreSQL already attached to magnet-atheme" + fi + + - name: Deploy ${{ matrix.app }} + run: flyctl deploy --config ${{ matrix.config }} --app ${{ matrix.app }} --remote-only \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc0401a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# PVX local modules +local/ + +# Claude Code private files +.claude/ +CLAUDE.md + +# Temporary files +*.tmp +*.bak + +# OS files +.DS_Store +Thumbs.db + +# Editor files +*~ +.*.swp +.*.swo + +# Log files +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d30b3f --- /dev/null +++ b/README.md @@ -0,0 +1,263 @@ +# Magnet IRC Network + +A modern, distributed IRC network infrastructure built for irc.perl.org with +multi-region deployment. + +## Overview + +The Magnet IRC Network is IRC infrastructure that provides reliable, secure, +and performant IRC services across multiple geographic regions. Built using +Solanum IRCd and Atheme services, it leverages Fly.io's global infrastructure +and Tailscale's mesh networking for secure inter-server communication. + +### Key Features + +- **Multi-Region Deployment**: US (Chicago) and EU (Amsterdam) regions for + optimal global performance +- **Security-First Design**: Tailscale mesh networking, ephemeral + authentication keys, auto-generated passwords +- **High Availability**: Geographic redundancy with automatic failover + capabilities +- **Modern Infrastructure**: Container-based deployment with proper health + checks and monitoring + +## Architecture + +``` +┌─────────────────┐ Tailscale ┌─────────────────┐ +│ magnet-9RL │◄─────────────────►│ magnet-1EU │ +│ (US Hub/IRC) │ Private Mesh │ (EU IRC) │ +│ SID: 9RL │ │ SID: 1EU │ +│ OpenSSL+EPYC │ │ OpenSSL+EPYC │ +└─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ +┌─────────────────┐ Tailscale ┌─────────────────┐ +│ magnet-atheme │◄─────────────────►│ magnet-postgres │ +│ (US Services) │ Private Mesh │ (Fly MPG) │ +│ OpenSSL+EPYC │ │ │ +└─────────────────┘ └─────────────────┘ +``` + +### Components + +1. **magnet-9RL** - Primary IRC server (US/Chicago) + - Solanum IRCd with OpenSSL optimizations + - Hub server for network coordination + - SSL/TLS client connections on port 6697 + +2. **magnet-1EU** - Secondary IRC server (EU/Amsterdam) + - Solanum IRCd with OpenSSL optimizations + - Linked to US hub for global federation + - Regional optimization for European users + +3. **magnet-atheme** - IRC Services (US/Chicago) + - User registration and authentication (NickServ) + - Channel management services (ChanServ) + - Persistent data storage via PostgreSQL + +4. **magnet-postgres** - Database (US/Chicago) + - PostgreSQL database for services persistence + - User accounts, channel registrations, configurations + - Automated backups and high availability + +## Getting Started + +### Prerequisites + +- Access to the perl-irc Github organization +- [Fly.io CLI](https://fly.io/docs/hands-on/install-flyctl/) installed and authenticated +- Access to the `magnet-irc` Fly.io organization +- Tailscale account with access to the `perl-irc` organization +- Basic familiarity with IRC network administration + +## Deployment + +### Development Deployment + +For testing and development purposes, use development-specific app names to avoid +conflicts with production: + +```bash +# Create development apps with -dev suffix +fly apps create magnet-hub-dev --org magnet-irc +fly apps create magnet-atheme-dev --org magnet-irc + +# Set up Tailscale authentication for dev +fly secrets set TAILSCALE_AUTHKEY=tskey-auth-xxxxx --app magnet-9rl-dev + +# Deploy base infrastructure (development) +fly deploy --app magnet-hub-dev +fly deploy --app magnet-atheme-dev + +# Validate mesh connectivity +fly ssh console --app magnet-hub-dev +tailscale status +``` + +**Important**: Always use the `-dev` suffix for development deployments to prevent +conflicts with production infrastructure. + +### Production Deployment + +Follow the systematic approach outlined in `github-issues.md`: + +1. **Start with Issue #1** - Implement base infrastructure with proper testing +2. **Follow TDD methodology** - Write failing tests, implement minimal code to pass +3. **Validate each step** - Ensure all tests pass before proceeding +4. **Build incrementally** - Each issue adds functionality while maintaining stability + +## Configuration + +### Key Environment Variables + +- `SERVER_NAME` - Unique server identifier (magnet-9RL, magnet-1EU) +- `SERVER_SID` - Three-character server ID for IRC protocol +- `SERVER_DESCRIPTION` - Human-readable server description +- `TAILSCALE_AUTHKEY` - Ephemeral auth key for mesh networking +- `SERVICES_PASSWORD` - Authentication between IRC server and services +- `LINK_PASSWORD_9RL_1EU` - Authentication between linked IRC servers + +### Configuration Templates + +The project uses environment variable substitution in configuration templates: + +- `ircd.conf.template` - Solanum server configuration +- `atheme.conf.template` - Atheme services configuration +- Startup scripts handle dynamic password generation and Tailscale initialization + +## Security + +### Security Features + +- **Ephemeral Tailscale Keys** - Devices automatically cleaned up on container termination +- **Auto-Generated Passwords** - 24-32 character secure passwords for all inter-service communication +- **SSL/TLS Everywhere** - All client and server-to-server communications encrypted +- **Private Mesh Networking** - Inter-server communication isolated via Tailscale +- **AMD EPYC Optimizations** - Hardware-accelerated cryptography with OpenSSL + +### Security Best Practices + +- No passwords stored in plain text or logs +- Secure credential distribution via Fly.io secrets +- Network isolation from public internet for internal communication +- Regular password rotation capabilities +- Comprehensive security audit coverage in test suite + +## Performance + +### Optimization Features + +- **OpenSSL with AES-NI** acceleration on AMD EPYC processors +- **Multi-core compilation** during Docker builds +- **Optimized connection classes** for different user types and regions +- **Efficient resource allocation** (1-2GB RAM, 1-2 vCPUs per service) +- **Geographic distribution** for optimal user experience + +### Performance Monitoring + +The project includes comprehensive performance testing: +- Response time measurement and SLA establishment +- Throughput testing under load +- Resource utilization monitoring +- Capacity planning metrics +- Performance regression detection + +## Troubleshooting + +### Common Operations + +```bash +# Check application status +fly status --app magnet-9rl + +# View logs +fly logs --app magnet-9rl + +# SSH into container +fly ssh console --app magnet-9rl + +# Check Tailscale mesh status +tailscale status + +# Monitor SSL connections +netstat -an | grep :6697 + +# Test OpenSSL performance +openssl speed aes-256-cbc + +# Verify AMD EPYC features +cat /proc/cpuinfo | grep flags +``` + +### Health Checks + +All components include comprehensive health checks: +- Tailscale mesh connectivity +- IRC server responsiveness +- Services authentication status +- Database connectivity +- SSL certificate validity + +## Development + +### Contributing + +1. **Use GitHub Issues** - Follow the systematic 15-issue implementation plan +2. **Maintain Documentation** - Update relevant documentation with changes +3. **Test Thoroughly** - Ensure all tests pass before submitting changes +4. **Security Review** - Consider security implications of all changes + +### Testing + +The project emphasizes comprehensive testing: +- **Unit Tests** - Component-level functionality validation +- **Integration Tests** - Inter-component communication testing +- **End-to-End Tests** - Complete IRC network functionality +- **Load Tests** - Performance and stability under realistic usage +- **Security Tests** - Vulnerability and penetration testing + +### Code Style + +- Simple, clean, maintainable solutions preferred +- Match existing code style and formatting +- Preserve comments and documentation +- Use descriptive, evergreen naming conventions +- No mock implementations - always use real data and APIs + +## Documentation + +### Key Files + +- **`README.md`** - This comprehensive project overview +- **`LICENSE`** - MIT License for the project + +### Additional Resources + +- [Fly.io Documentation](https://fly.io/docs/) +- [Tailscale Documentation](https://tailscale.com/kb/) +- [Solanum IRCd Documentation](https://github.com/solanum-ircd/solanum) +- [Atheme Services Documentation](https://github.com/atheme/atheme) + +## License + +This project is licensed under the MIT License - see the +[LICENSE](/Users/perigrin/dev/magnet/LICENSE) file for details. + +## Organizations + +- **Fly.io Organization**: `magnet-irc` +- **Tailscale Organization**: `perl-irc` +- **Github Organization**: `perl-irc` + +## Support + +For issues, questions, or contributions: +1. Submit issues following the established format +2. Ensure all tests pass before requesting reviews + +--- + +**Note**: This infrastructure is designed for production IRC network operation. +Follow all security best practices and test thoroughly in development +environments before production deployment. diff --git a/docs/deployment-prerequisites.md b/docs/deployment-prerequisites.md new file mode 100644 index 0000000..55a870a --- /dev/null +++ b/docs/deployment-prerequisites.md @@ -0,0 +1,365 @@ +# Deployment Prerequisites + +This document outlines the requirements and setup procedures for deploying the Magnet IRC Network infrastructure on Fly.io. + +## Fly.io CLI Requirements + +### Installation + +Install the Fly.io CLI following the official instructions: + +```bash +# macOS +brew install flyctl + +# Linux/WSL +curl -L https://fly.io/install.sh | sh + +# Windows PowerShell +iwr https://fly.io/install.ps1 -useb | iex +``` + +### Verification + +Verify your installation: + +```bash +fly version +``` + +Expected output should show a version number (e.g., `flyctl v0.1.xxx`). + +## Authentication Setup + +### Initial Authentication + +1. Create a Fly.io account at https://fly.io if you don't have one +2. Authenticate your CLI: + +```bash +fly auth login +``` + +3. Verify authentication: + +```bash +fly auth whoami +``` + +### Organization Setup (Optional) + +If deploying for an organization: + +```bash +# List available organizations +fly orgs list + +# Switch to organization context +fly auth login --org +``` + +## Required Environment Variables + +The following secrets must be configured for each application: + +### Tailscale Integration + +```bash +# Generate ephemeral auth keys from Tailscale admin console +fly secrets set TAILSCALE_AUTHKEY=tskey-auth-PLACEHOLDER --app magnet-9rl +fly secrets set TAILSCALE_AUTHKEY=tskey-auth-PLACEHOLDER --app magnet-1eu +fly secrets set TAILSCALE_AUTHKEY=tskey-auth-PLACEHOLDER --app magnet-atheme +``` + +### IRC Server Passwords + +```bash +# Generate secure passwords (24-32 characters recommended) +fly secrets set SERVICES_PASSWORD=$(openssl rand -base64 24) --app magnet-9rl +fly secrets set SERVICES_PASSWORD=$(openssl rand -base64 24) --app magnet-1eu +fly secrets set SERVICES_PASSWORD=$(openssl rand -base64 24) --app magnet-atheme + +# Link passwords between IRC servers +LINK_PASS=$(openssl rand -base64 32) +fly secrets set LINK_PASSWORD_9RL_1EU="$LINK_PASS" --app magnet-9rl +fly secrets set LINK_PASSWORD_9RL_1EU="$LINK_PASS" --app magnet-1eu +``` + +### Operator Passwords + +```bash +fly secrets set OPER_PASSWORD=$(openssl rand -base64 24) --app magnet-9rl +fly secrets set OPER_PASSWORD=$(openssl rand -base64 24) --app magnet-1eu +``` + +## App Creation + +Create the Fly.io applications before deploying: + +```bash +# Create apps in respective regions +fly apps create magnet-9rl --org +fly apps create magnet-1eu --org +fly apps create magnet-atheme --org +``` + +## Volume Provisioning + +Use the provided script to create persistent volumes: + +```bash +# Preview what will be created +scripts/create-volumes.pl --dry-run + +# Create volumes +scripts/create-volumes.pl +``` + +Manual volume creation (if script fails): + +```bash +fly volumes create magnet_9rl_data --region ord --size 3 --app magnet-9rl +fly volumes create magnet_1eu_data --region ams --size 3 --app magnet-1eu +fly volumes create magnet_atheme_data --region ord --size 3 --app magnet-atheme +``` + +## Database Setup + +Create and attach PostgreSQL database for Atheme services: + +```bash +# Create PostgreSQL cluster +fly postgres create --name magnet-postgres --region ord --vm-size shared-cpu-1x --volume-size 10 + +# Attach to Atheme services +fly postgres attach --app magnet-atheme magnet-postgres +``` + +## Deployment Process + +### Automated Deployment (Recommended) + +The project includes GitHub Actions workflow for automated deployment following Fly.io best practices: + +1. **Setup Deploy Token** (one-time setup): + +```bash +# Generate deploy token for each app +fly tokens create deploy --app magnet-9rl +fly tokens create deploy --app magnet-1eu +fly tokens create deploy --app magnet-atheme + +# Add to GitHub repository secrets as FLY_API_TOKEN +# Go to GitHub repo → Settings → Secrets → Actions +# Create new secret: FLY_API_TOKEN = +``` + +2. **Automatic Deployment**: + - Push to `main` branch triggers automatic deployment + - Workflow runs infrastructure tests first + - Deploys all applications with remote builders + - Provisions volumes automatically + - Generates deployment reports + +3. **Manual Deployment Trigger**: + - Go to GitHub → Actions → "Deploy to Fly.io" + - Click "Run workflow" for manual deployment + +### Development Environment Setup + +For local development, use per-user development environments: + +```bash +# Set up your personal dev environment (one-time) +scripts/setup-dev-env.pl + +# Deploy to your dev environment +scripts/deploy-dev.pl + +# List your dev apps +scripts/deploy-dev.pl --list + +# Preview deployment changes +scripts/deploy-dev.pl --dry-run + +# Clean up dev environment when done +scripts/cleanup-dev-env.pl +``` + +**Important**: Development environments are single-region (ord) and completely isolated from production. + +## Development vs Production Separation + +### Development Environments +- **Purpose**: Individual developer testing and experimentation +- **Naming**: `magnet-hub-`, `magnet-services-` +- **Region**: Single region (ord) for simplicity +- **Management**: Local scripts (`scripts/setup-dev-env.pl`, `scripts/deploy-dev.pl`) +- **Data**: Ephemeral, safe to destroy and recreate + +### Production Environment +- **Purpose**: Live IRC network serving users +- **Naming**: `magnet-9rl`, `magnet-1eu`, `magnet-atheme` +- **Regions**: Multi-region (ord, ams) for global coverage +- **Management**: GitHub Actions only (automated on push to main) +- **Data**: Persistent, protected with backups and rollback procedures + +**Important**: Never deploy directly to production apps using local tools. Production deployments happen automatically via GitHub Actions when code is pushed to the main branch. + +## CI/CD Best Practices + +### Deploy Token Management + +Following Fly.io recommendations for secure token management: + +```bash +# Create dedicated deploy tokens (not personal auth tokens) +fly tokens create deploy --app --name "GitHub Actions" + +# Rotate tokens regularly (quarterly recommended) +fly tokens list +fly tokens revoke +``` + +### Remote Builders + +Always use `--remote-only` flag for deployments to leverage Fly.io's optimized build environment: + +- Faster builds on AMD EPYC infrastructure +- Consistent build environment +- No local Docker daemon requirements +- Better caching and optimization + +### Deployment Verification + +The automated workflow includes comprehensive verification: + +- Infrastructure tests before deployment +- App existence validation +- Health check verification +- Deployment status monitoring +- Automated rollback on failure + +### Security Considerations + +- Deploy tokens have limited scope (app-specific) +- Secrets are managed through Fly.io platform +- No sensitive data in repository +- Automated security scanning in CI/CD + +## Rollback Procedures + +### Application Rollback + +Rollback to previous version: + +```bash +# List recent releases +fly releases --app magnet-9rl + +# Rollback to specific version +fly releases rollback v2 --app magnet-9rl +``` + +### Volume Rollback + +Volume data cannot be automatically rolled back. Consider these strategies: + +1. **Snapshot Strategy**: Create volume snapshots before major changes: + +```bash +# Create snapshot (when feature becomes available) +fly volumes snapshot create magnet_9rl_data --app magnet-9rl +``` + +2. **Backup Strategy**: Export critical data before changes: + +```bash +# SSH into machine and backup data +fly ssh console --app magnet-9rl +tar -czf /tmp/backup.tar.gz /opt/solanum/var +``` + +3. **Blue-Green Strategy**: Maintain parallel environments for critical updates. + +### Secret Rotation + +If secrets are compromised: + +```bash +# Rotate Tailscale keys +fly secrets set TAILSCALE_AUTHKEY=tskey-auth-new-key --app magnet-9rl + +# Rotate passwords (coordinate across all affected apps) +NEW_SERVICES_PASS=$(openssl rand -base64 24) +fly secrets set SERVICES_PASSWORD="$NEW_SERVICES_PASS" --app magnet-9rl +fly secrets set SERVICES_PASSWORD="$NEW_SERVICES_PASS" --app magnet-atheme +``` + +### Emergency Procedures + +1. **Complete Service Restart**: + +```bash +fly machine restart --app magnet-9rl +``` + +2. **Scale Down (Emergency Stop)**: + +```bash +fly scale count 0 --app magnet-9rl +``` + +3. **Scale Up (Recovery)**: + +```bash +fly scale count 1 --app magnet-9rl +``` + +## Monitoring and Logs + +### Real-time Logs + +```bash +fly logs --app magnet-9rl +``` + +### Health Monitoring + +```bash +# Check machine status +fly machine list --app magnet-9rl + +# Monitor metrics +fly machine status --app magnet-9rl +``` + +## Troubleshooting + +### Common Issues + +1. **Volume Mount Failures**: Ensure volumes exist and are in the correct region +2. **Health Check Failures**: Verify service is listening on port 8080 +3. **Tailscale Connection Issues**: Check auth key validity and network access +4. **Database Connection Issues**: Verify PostgreSQL attachment and credentials + +### Debug Commands + +```bash +# SSH into running machine +fly ssh console --app magnet-9rl + +# Check Tailscale status +fly ssh console --app magnet-9rl -C "tailscale status" + +# View service logs +fly ssh console --app magnet-9rl -C "journalctl -f" +``` + +## Security Considerations + +- Use ephemeral Tailscale auth keys (automatically cleaned up when containers stop) +- Rotate passwords regularly (monthly recommended) +- Monitor access logs for unusual activity +- Keep Fly.io CLI and base images updated +- Use least-privilege access for operational accounts \ No newline at end of file diff --git a/scripts/cleanup-dev-env.pl b/scripts/cleanup-dev-env.pl new file mode 100755 index 0000000..c69b5ce --- /dev/null +++ b/scripts/cleanup-dev-env.pl @@ -0,0 +1,215 @@ +#!/usr/bin/env perl +# ABOUTME: Development environment cleanup for Magnet IRC Network +# ABOUTME: Safely removes per-user dev environments with confirmation prompts + +use strict; +use warnings; +use Getopt::Long; + +sub get_username { + my $username = $ENV{USER} || $ENV{USERNAME} || `whoami`; + chomp $username if $username; + $username =~ s/[^a-z0-9\-]//gi; # Sanitize for Fly.io app names + return lc($username); +} + +# Dev environment apps +my @DEV_APPS = qw(magnet-hub magnet-services); + +sub check_prerequisites { + print "🔍 Checking prerequisites...\n"; + + # Check flyctl availability + my $fly_version = `flyctl version 2>&1`; + if ($? != 0) { + die "❌ flyctl not available. Please install Fly.io CLI first.\n"; + } + + # Check authentication + my $auth_output = `flyctl auth whoami 2>&1`; + if ($? != 0 || $auth_output =~ /not logged in/i) { + die "❌ Not authenticated with Fly.io. Run 'flyctl auth login' first.\n"; + } + chomp $auth_output; + print "✅ Authenticated as: $auth_output\n"; + + return 1; +} + +sub get_dev_app_name { + my ($base_name, $username) = @_; + return "$base_name-$username"; +} + +sub app_exists { + my ($app_name) = @_; + + my $output = `flyctl status --app $app_name 2>&1`; + return $? == 0; +} + +sub list_dev_apps { + my ($username) = @_; + + my @existing_apps; + + foreach my $base_name (@DEV_APPS) { + my $app_name = get_dev_app_name($base_name, $username); + if (app_exists($app_name)) { + push @existing_apps, $app_name; + } + } + + return @existing_apps; +} + +sub confirm_cleanup { + my ($username, @apps) = @_; + + if (@apps == 0) { + print "ℹ️ No development apps found for $username\n"; + return 0; + } + + print "⚠️ WARNING: This will permanently DELETE the following apps:\n\n"; + foreach my $app (@apps) { + print " 🗑️ $app (including all volumes and data)\n"; + } + print "\n"; + print "This action CANNOT be undone!\n"; + print "Type 'DELETE' to confirm: "; + + my $confirmation = ; + chomp $confirmation; + + return $confirmation eq 'DELETE'; +} + +sub cleanup_dev_app { + my ($app_name) = @_; + + print "🗑️ Destroying app $app_name...\n"; + + my $cmd = "flyctl apps destroy $app_name --yes"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Destroyed $app_name\n"; + return 1; + } else { + print "❌ Failed to destroy $app_name:\n$output\n"; + return 0; + } +} + +sub main { + my $username = get_username(); + my $force = 0; + my $list_only = 0; + my $help = 0; + + GetOptions( + 'user=s' => \$username, + 'force' => \$force, + 'list' => \$list_only, + 'help|h' => \$help, + ) or die "Error in command line arguments\n"; + + if ($help) { + print < { + name => 'magnet_9rl_data', + region => 'ord', + size => 3, + app => 'magnet-9rl' + }, + 'magnet-1eu' => { + name => 'magnet_1eu_data', + region => 'ams', + size => 3, + app => 'magnet-1eu' + }, + 'magnet-atheme' => { + name => 'magnet_atheme_data', + region => 'ord', + size => 3, + app => 'magnet-atheme' + } +); + +sub check_fly_cli { + my $output = `fly version 2>&1`; + if ($? != 0) { + die "ERROR: Fly CLI not available. Please install fly CLI first.\n"; + } + print "✓ Fly CLI available\n"; +} + +sub check_authentication { + my $output = `fly auth whoami 2>&1`; + if ($? != 0 || $output =~ /not logged in/i) { + die "ERROR: Not authenticated with Fly.io. Run 'fly auth login' first.\n"; + } + chomp $output; + print "✓ Authenticated as: $output\n"; +} + +sub volume_exists { + my ($app, $volume_name) = @_; + + my $output = `fly volumes list --app $app --json 2>/dev/null`; + return 0 if $? != 0; + + # Simple JSON parsing for volume names + return $output =~ /"name":\s*"$volume_name"/; +} + +sub create_volume { + my ($app, $volume_spec) = @_; + + my $name = $volume_spec->{name}; + my $region = $volume_spec->{region}; + my $size = $volume_spec->{size}; + + if (volume_exists($app, $name)) { + print "✓ Volume $name already exists for $app\n"; + return 1; + } + + print "Creating volume $name for $app ($size GB in $region)...\n"; + + my $cmd = "fly volumes create $name --region $region --size $size --app $app"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✓ Created volume $name for $app\n"; + return 1; + } else { + print "✗ Failed to create volume $name for $app:\n$output\n"; + return 0; + } +} + +sub verify_volumes { + my $all_good = 1; + + print "\nVerifying volumes...\n"; + + foreach my $app (sort keys %VOLUMES) { + my $volume_spec = $VOLUMES{$app}; + my $name = $volume_spec->{name}; + + if (volume_exists($app, $name)) { + print "✓ Volume $name exists for $app\n"; + } else { + print "✗ Volume $name missing for $app\n"; + $all_good = 0; + } + } + + return $all_good; +} + +sub main { + my $dry_run = 0; + my $help = 0; + my $production = 0; + + GetOptions( + 'dry-run' => \$dry_run, + 'production' => \$production, + 'help|h' => \$help, + ) or die "Error in command line arguments\n"; + + if ($help) { + print <{name}, "($app)", $volume_spec->{size}, $volume_spec->{region}; + } + exit 0; + } + + # Create volumes + print "\nCreating volumes...\n"; + my $success_count = 0; + my $total_count = scalar keys %VOLUMES; + + foreach my $app (sort keys %VOLUMES) { + if (create_volume($app, $VOLUMES{$app})) { + $success_count++; + } + } + + print "\n"; + + # Verify all volumes + if (verify_volumes()) { + print "\n✓ All volumes created successfully!\n"; + exit 0; + } else { + print "\n✗ Some volumes failed to create properly\n"; + exit 1; + } +} + +main() unless caller; \ No newline at end of file diff --git a/scripts/deploy-dev.pl b/scripts/deploy-dev.pl new file mode 100755 index 0000000..4592cf3 --- /dev/null +++ b/scripts/deploy-dev.pl @@ -0,0 +1,334 @@ +#!/usr/bin/env perl +# ABOUTME: Per-user development environment deployment for Magnet IRC Network +# ABOUTME: Creates isolated dev environments following Fly.io per-user dev patterns + +use strict; +use warnings; +use Getopt::Long; +use JSON::PP; + +sub get_username { + my $username = $ENV{USER} || $ENV{USERNAME} || `whoami`; + chomp $username if $username; + $username =~ s/[^a-z0-9\-]//gi; # Sanitize for Fly.io app names + return lc($username); +} + +# Single-region dev configuration (US only for simplicity) +my %DEV_CONFIG = ( + 'magnet-hub' => { + config => 'servers/magnet-9rl/fly.toml', + region => 'ord', + description => 'Development IRC Hub', + health_check => '/health', + required_secrets => ['TAILSCALE_AUTHKEY', 'SERVICES_PASSWORD', 'OPER_PASSWORD'], + }, + 'magnet-services' => { + config => 'servers/magnet-atheme/fly.toml', + region => 'ord', + description => 'Development IRC Services', + health_check => '/health', + required_secrets => ['TAILSCALE_AUTHKEY', 'SERVICES_PASSWORD'], + }, +); + +sub check_prerequisites { + print "🔍 Checking development deployment prerequisites...\n"; + + # Check flyctl availability + my $fly_version = `flyctl version 2>&1`; + if ($? != 0) { + die "❌ flyctl not available. Please install Fly.io CLI first.\n"; + } + print "✅ flyctl available\n"; + + # Check authentication + my $auth_output = `flyctl auth whoami 2>&1`; + if ($? != 0 || $auth_output =~ /not logged in/i) { + die "❌ Not authenticated with Fly.io. Run 'flyctl auth login' first.\n"; + } + chomp $auth_output; + print "✅ Authenticated as: $auth_output\n"; + + return 1; +} + +sub get_dev_app_name { + my ($base_name, $username) = @_; + return "$base_name-$username"; +} + +sub app_exists { + my ($app_name) = @_; + + my $output = `flyctl status --app $app_name 2>&1`; + return $? == 0; +} + +sub create_dev_app { + my ($base_name, $username, $config) = @_; + + my $app_name = get_dev_app_name($base_name, $username); + + if (app_exists($app_name)) { + print "✅ Dev app $app_name already exists\n"; + return 1; + } + + print "🏗️ Creating dev app $app_name...\n"; + + my $create_cmd = "flyctl apps create $app_name --org personal"; + my $output = `$create_cmd 2>&1`; + + if ($? == 0) { + print "✅ Created dev app $app_name\n"; + return 1; + } else { + print "❌ Failed to create dev app $app_name:\n$output\n"; + return 0; + } +} + +sub deploy_dev_app { + my ($base_name, $username, $config, $remote_only) = @_; + + my $app_name = get_dev_app_name($base_name, $username); + print "🚀 Deploying $app_name ($config->{description})...\n"; + + # Construct deploy command + my $deploy_cmd = "flyctl deploy --config $config->{config} --app $app_name"; + $deploy_cmd .= " --remote-only" if $remote_only; + + print "Running: $deploy_cmd\n"; + my $output = `$deploy_cmd 2>&1`; + + if ($? == 0) { + print "✅ Successfully deployed $app_name\n"; + return 1; + } else { + print "❌ Failed to deploy $app_name:\n$output\n"; + return 0; + } +} + +sub verify_dev_deployment { + my ($base_name, $username, $config) = @_; + + my $app_name = get_dev_app_name($base_name, $username); + print "🔍 Verifying deployment of $app_name...\n"; + + # Allow time for deployment to stabilize + sleep 10; + + # Check app status + my $status_output = `flyctl status --app $app_name --json 2>/dev/null`; + if ($? != 0) { + print "❌ Could not get status for $app_name\n"; + return 0; + } + + my $status_data = eval { decode_json($status_output) }; + if (!$status_data) { + print "❌ Could not parse status JSON for $app_name\n"; + return 0; + } + + my $app_status = $status_data->{Status} || 'unknown'; + print "App status: $app_status\n"; + + # Test health endpoint if available + my $hostname = $status_data->{Hostname}; + if ($hostname && $hostname =~ /fly\.dev$/) { + my $health_url = "https://$hostname$config->{health_check}"; + print "Testing health endpoint: $health_url\n"; + + my $health_response = `curl -f -s "$health_url" 2>&1`; + if ($? == 0) { + print "✅ Health check passed for $app_name\n"; + return 1; + } else { + print "⚠️ Health check failed for $app_name\n"; + return 0; + } + } + + # If no health endpoint, just check if app is running + if ($app_status eq 'running') { + print "✅ $app_name is running\n"; + return 1; + } else { + print "⚠️ $app_name status is $app_status\n"; + return 0; + } +} + +sub list_dev_apps { + my ($username) = @_; + + print "📋 Development apps for $username:\n"; + + foreach my $base_name (sort keys %DEV_CONFIG) { + my $app_name = get_dev_app_name($base_name, $username); + my $config = $DEV_CONFIG{$base_name}; + + if (app_exists($app_name)) { + print "✅ $app_name ($config->{description})\n"; + + # Get app URL + my $status_output = `flyctl status --app $app_name --json 2>/dev/null`; + if ($? == 0) { + my $status_data = eval { decode_json($status_output) }; + if ($status_data && $status_data->{Hostname}) { + print " URL: https://$status_data->{Hostname}\n"; + } + } + } else { + print "❌ $app_name (not created)\n"; + } + } +} + +sub generate_dev_report { + my ($username, $deployment_results) = @_; + + print "\n" . "="x50 . "\n"; + print "DEVELOPMENT DEPLOYMENT REPORT\n"; + print "="x50 . "\n"; + printf "Developer: %s\n", $username; + printf "Date: %s\n", scalar localtime; + print "\n"; + + foreach my $base_name (sort keys %$deployment_results) { + my $app_name = get_dev_app_name($base_name, $username); + my $result = $deployment_results->{$base_name}; + my $status_icon = $result->{success} ? "✅" : "❌"; + + printf "%s %s (%s)\n", $status_icon, $app_name, $DEV_CONFIG{$base_name}->{description}; + printf " Region: %s\n", $DEV_CONFIG{$base_name}->{region}; + printf " Status: %s\n", $result->{status} || 'unknown'; + + if ($result->{health_check}) { + printf " Health: %s\n", $result->{health_check} ? "✅ Passed" : "❌ Failed"; + } + print "\n"; + } + + print "💡 Access your development environment:\n"; + print " flyctl ssh console --app " . get_dev_app_name('magnet-hub', $username) . "\n"; + print " flyctl logs --app " . get_dev_app_name('magnet-hub', $username) . "\n"; +} + +sub main { + my $username = get_username(); + my $dry_run = 0; + my $remote_only = 1; # Default to remote builds + my $list_only = 0; + my $help = 0; + + GetOptions( + 'user=s' => \$username, + 'dry-run' => \$dry_run, + 'local-build' => sub { $remote_only = 0 }, + 'list' => \$list_only, + 'help|h' => \$help, + ) or die "Error in command line arguments\n"; + + if ($help) { + print <{description}; + printf " Config: %s\n", $config->{config}; + printf " Region: %s\n", $config->{region}; + print "\n"; + } + exit 0; + } + + # Create and deploy dev apps + print "🚀 Setting up development environment...\n"; + my %deployment_results; + + foreach my $base_name (sort keys %DEV_CONFIG) { + my $config = $DEV_CONFIG{$base_name}; + + # Create app if needed + unless (create_dev_app($base_name, $username, $config)) { + $deployment_results{$base_name} = { success => 0 }; + next; + } + + # Deploy app + my $success = deploy_dev_app($base_name, $username, $config, $remote_only); + $deployment_results{$base_name} = { success => $success }; + + if ($success) { + my $verification = verify_dev_deployment($base_name, $username, $config); + $deployment_results{$base_name}->{health_check} = $verification; + } + + print "\n"; + } + + # Generate final report + generate_dev_report($username, \%deployment_results); + + # Exit with appropriate code + my $failed_deployments = grep { !$_->{success} } values %deployment_results; + if ($failed_deployments > 0) { + print "❌ $failed_deployments deployment(s) failed\n"; + exit 1; + } else { + print "✅ Development environment ready!\n"; + exit 0; + } +} + +main() unless caller; \ No newline at end of file diff --git a/scripts/manage-production.pl b/scripts/manage-production.pl new file mode 100755 index 0000000..dbb764e --- /dev/null +++ b/scripts/manage-production.pl @@ -0,0 +1,318 @@ +#!/usr/bin/env perl +# ABOUTME: Production machine management for Magnet IRC Network +# ABOUTME: Start, stop, restart, and destroy production infrastructure with safety checks + +use strict; +use warnings; +use Getopt::Long; + +# Production applications +my @PRODUCTION_APPS = qw(magnet-9rl magnet-1eu magnet-atheme magnet-postgres); + +sub check_prerequisites { + print "🔍 Checking prerequisites...\n"; + + # Check flyctl availability + my $fly_version = `flyctl version 2>&1`; + if ($? != 0) { + die "❌ flyctl not available. Please install Fly.io CLI first.\n"; + } + + # Check authentication + my $auth_output = `flyctl auth whoami 2>&1`; + if ($? != 0 || $auth_output =~ /not logged in/i) { + die "❌ Not authenticated with Fly.io. Run 'flyctl auth login' first.\n"; + } + chomp $auth_output; + print "✅ Authenticated as: $auth_output\n"; + + return 1; +} + +sub app_exists { + my ($app_name) = @_; + + my $output = `flyctl status --app $app_name 2>&1`; + return $? == 0; +} + +sub get_app_status { + my ($app_name) = @_; + + unless (app_exists($app_name)) { + return "NOT_FOUND"; + } + + my $output = `flyctl status --app $app_name 2>&1`; + if ($? != 0) { + return "ERROR"; + } + + # Parse status from output + if ($output =~ /Machines.*?running/is) { + return "RUNNING"; + } elsif ($output =~ /Machines.*?stopped/is) { + return "STOPPED"; + } elsif ($output =~ /No machines/is) { + return "NO_MACHINES"; + } else { + return "UNKNOWN"; + } +} + +sub list_production_status { + print "Production Infrastructure Status:\n"; + print "=" x 40 . "\n"; + + foreach my $app (@PRODUCTION_APPS) { + my $status = get_app_status($app); + my $emoji = $status eq "RUNNING" ? "🟢" : + $status eq "STOPPED" ? "🟡" : + $status eq "NOT_FOUND" ? "❌" : "❓"; + + printf "%-20s %s %s\n", $app, $emoji, $status; + } + print "\n"; +} + +sub stop_machines { + my (@apps) = @_; + @apps = @PRODUCTION_APPS unless @apps; + + print "🛑 Stopping machines...\n"; + + my $success_count = 0; + foreach my $app (@apps) { + unless (app_exists($app)) { + print "⚠️ App $app does not exist, skipping\n"; + next; + } + + print "Stopping machines for $app...\n"; + my $cmd = "flyctl machine stop --app $app"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Stopped machines for $app\n"; + $success_count++; + } else { + print "❌ Failed to stop machines for $app:\n$output\n"; + } + } + + return $success_count; +} + +sub start_machines { + my (@apps) = @_; + @apps = @PRODUCTION_APPS unless @apps; + + print "🚀 Starting machines...\n"; + + my $success_count = 0; + foreach my $app (@apps) { + unless (app_exists($app)) { + print "⚠️ App $app does not exist, skipping\n"; + next; + } + + print "Starting machines for $app...\n"; + my $cmd = "flyctl machine start --app $app"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Started machines for $app\n"; + $success_count++; + } else { + print "❌ Failed to start machines for $app:\n$output\n"; + } + } + + return $success_count; +} + +sub restart_machines { + my (@apps) = @_; + @apps = @PRODUCTION_APPS unless @apps; + + print "🔄 Restarting machines...\n"; + + my $success_count = 0; + foreach my $app (@apps) { + unless (app_exists($app)) { + print "⚠️ App $app does not exist, skipping\n"; + next; + } + + print "Restarting machines for $app...\n"; + my $cmd = "flyctl machine restart --app $app"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Restarted machines for $app\n"; + $success_count++; + } else { + print "❌ Failed to restart machines for $app:\n$output\n"; + } + } + + return $success_count; +} + +sub confirm_destruction { + my (@apps) = @_; + + print "⚠️ DANGER: This will permanently DELETE the following production apps:\n\n"; + foreach my $app (@apps) { + print " 💥 $app (including ALL data, volumes, and configurations)\n"; + } + print "\n"; + print "🚨 THIS WILL DESTROY THE ENTIRE MAGNET IRC NETWORK!\n"; + print "🚨 ALL USER DATA, CHANNELS, AND SERVICES WILL BE LOST!\n"; + print "🚨 THIS ACTION CANNOT BE UNDONE!\n\n"; + print "Type 'BURN IT ALL DOWN' to confirm: "; + + my $confirmation = ; + chomp $confirmation; + + return $confirmation eq 'BURN IT ALL DOWN'; +} + +sub destroy_production { + my (@apps) = @_; + @apps = @PRODUCTION_APPS unless @apps; + + # Filter to only existing apps + my @existing_apps = grep { app_exists($_) } @apps; + + if (@existing_apps == 0) { + print "ℹ️ No production apps found to destroy\n"; + return 0; + } + + unless (confirm_destruction(@existing_apps)) { + print "❌ Destruction cancelled by user\n"; + return 0; + } + + print "\n💥 DESTROYING PRODUCTION INFRASTRUCTURE...\n\n"; + + my $success_count = 0; + foreach my $app (@existing_apps) { + print "Destroying $app...\n"; + my $cmd = "flyctl apps destroy $app --yes"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Destroyed $app\n"; + $success_count++; + } else { + print "❌ Failed to destroy $app:\n$output\n"; + } + print "\n"; + } + + print "=" x 50 . "\n"; + printf "Destruction Summary: %d/%d apps destroyed\n", $success_count, scalar(@existing_apps); + + return $success_count; +} + +sub main { + my $action = ''; + my $apps_arg = ''; + my $list = 0; + my $help = 0; + + GetOptions( + 'action=s' => \$action, + 'apps=s' => \$apps_arg, + 'list' => \$list, + 'help|h' => \$help, + ) or die "Error in command line arguments\n"; + + if ($help) { + print <&1`; + if ($? != 0) { + die "❌ flyctl not available. Please install Fly.io CLI first.\n"; + } + print "✅ flyctl available\n"; + + # Check authentication + my $auth_output = `flyctl auth whoami 2>&1`; + if ($? != 0 || $auth_output =~ /not logged in/i) { + die "❌ Not authenticated with Fly.io. Run 'flyctl auth login' first.\n"; + } + chomp $auth_output; + print "✅ Authenticated as: $auth_output\n"; + + return 1; +} + +sub app_exists { + my ($app_name) = @_; + + my $output = `flyctl status --app $app_name 2>&1`; + return $? == 0; +} + +sub create_deploy_tokens { + my ($token_name) = @_; + $token_name ||= "GitHub Actions"; + + print "🔑 Creating deploy tokens...\n"; + + my @created_tokens; + + foreach my $app (@APPS) { + unless (app_exists($app)) { + print "⚠️ App $app does not exist. Please create it first.\n"; + next; + } + + print "Creating deploy token for $app...\n"; + + my $cmd = "flyctl tokens create deploy --app $app --name \"$token_name\""; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + # Extract token from output + if ($output =~ /FlyV1\s+([^\s]+)/) { + my $token = $1; + push @created_tokens, { + app => $app, + token => $token + }; + print "✅ Created deploy token for $app\n"; + } else { + print "⚠️ Could not extract token for $app\n"; + } + } else { + print "❌ Failed to create token for $app:\n$output\n"; + } + } + + return @created_tokens; +} + +sub list_existing_tokens { + print "📋 Listing existing deploy tokens...\n"; + + my $output = `flyctl tokens list 2>&1`; + if ($? != 0) { + print "❌ Could not list tokens:\n$output\n"; + return; + } + + print $output; +} + +sub revoke_tokens { + my ($token_pattern) = @_; + + print "🗑️ Revoking tokens matching pattern: $token_pattern\n"; + + my $list_output = `flyctl tokens list 2>&1`; + if ($? != 0) { + print "❌ Could not list tokens for revocation\n"; + return; + } + + my @token_ids = $list_output =~ /^(\S+)\s+.*$token_pattern/gm; + + if (@token_ids == 0) { + print "ℹ️ No tokens found matching pattern: $token_pattern\n"; + return; + } + + foreach my $token_id (@token_ids) { + print "Revoking token: $token_id\n"; + + my $cmd = "flyctl tokens revoke $token_id"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Revoked token: $token_id\n"; + } else { + print "❌ Failed to revoke token $token_id:\n$output\n"; + } + } +} + +sub display_github_instructions { + my (@tokens) = @_; + + if (@tokens == 0) { + print "ℹ️ No tokens to configure\n"; + return; + } + + print "\n" . "="x60 . "\n"; + print "GITHUB ACTIONS SETUP INSTRUCTIONS\n"; + print "="x60 . "\n\n"; + + print "1. Go to your GitHub repository\n"; + print "2. Navigate to Settings → Secrets and variables → Actions\n"; + print "3. Create a new repository secret:\n\n"; + + print " Name: FLY_API_TOKEN\n"; + print " Value: (choose one of the following)\n\n"; + + foreach my $token_info (@tokens) { + printf " # For %s: %s\n", $token_info->{app}, $token_info->{token}; + } + + print "\n⚠️ IMPORTANT:\n"; + print " - Use a single token that has access to all required apps\n"; + print " - Store the token securely - it won't be shown again\n"; + print " - Rotate tokens quarterly for security\n"; + print " - Never commit tokens to your repository\n\n"; + + print "4. Test the workflow by pushing to the main branch\n"; + print "5. Monitor deployment in GitHub Actions tab\n\n"; +} + +sub main { + my $create = 0; + my $list = 0; + my $revoke_pattern = ''; + my $token_name = 'GitHub Actions'; + my $production = 0; + my $help = 0; + + GetOptions( + 'create' => \$create, + 'list' => \$list, + 'revoke=s' => \$revoke_pattern, + 'name=s' => \$token_name, + 'production' => \$production, + 'help|h' => \$help, + ) or die "Error in command line arguments\n"; + + if ($help) { + print <&1`; + if ($? != 0) { + die "❌ flyctl not available. Please install Fly.io CLI first.\n"; + } + print "✅ flyctl available\n"; + + # Check authentication + my $auth_output = `flyctl auth whoami 2>&1`; + if ($? != 0 || $auth_output =~ /not logged in/i) { + die "❌ Not authenticated with Fly.io. Run 'flyctl auth login' first.\n"; + } + chomp $auth_output; + print "✅ Authenticated as: $auth_output\n"; + + return 1; +} + +sub get_dev_app_name { + my ($base_name, $username) = @_; + return "$base_name-$username"; +} + +sub app_exists { + my ($app_name) = @_; + + my $output = `flyctl status --app $app_name 2>&1`; + return $? == 0; +} + +sub volume_exists { + my ($app_name, $volume_name) = @_; + + my $output = `flyctl volumes list --app $app_name --json 2>/dev/null`; + return 0 if $? != 0; + + # Simple JSON parsing for volume names + return $output =~ /"name":\s*"$volume_name"/; +} + +sub create_dev_app { + my ($base_name, $username) = @_; + + my $app_name = get_dev_app_name($base_name, $username); + + if (app_exists($app_name)) { + print "✅ Dev app $app_name already exists\n"; + return 1; + } + + print "🏗️ Creating dev app $app_name...\n"; + + my $create_cmd = "flyctl apps create $app_name --org personal"; + my $output = `$create_cmd 2>&1`; + + if ($? == 0) { + print "✅ Created dev app $app_name\n"; + return 1; + } else { + print "❌ Failed to create dev app $app_name:\n$output\n"; + return 0; + } +} + +sub create_dev_volume { + my ($base_name, $username) = @_; + + my $app_name = get_dev_app_name($base_name, $username); + my $volume_name = "${base_name}_${username}_data"; + + if (volume_exists($app_name, $volume_name)) { + print "✅ Volume $volume_name already exists for $app_name\n"; + return 1; + } + + print "📦 Creating volume $volume_name for $app_name...\n"; + + my $cmd = "flyctl volumes create $volume_name --region ord --size 1 --app $app_name"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Created volume $volume_name for $app_name\n"; + return 1; + } else { + print "❌ Failed to create volume $volume_name for $app_name:\n$output\n"; + return 0; + } +} + +sub setup_dev_secrets { + my ($base_name, $username) = @_; + + my $app_name = get_dev_app_name($base_name, $username); + + print "🔐 Setting up development secrets for $app_name...\n"; + + # Generate development-specific secrets + my $tailscale_key = "tskey-auth-DEV-PLACEHOLDER"; # User must replace + my $services_pass = generate_password(24); + my $oper_pass = generate_password(24); + + my @secret_commands = ( + "flyctl secrets set TAILSCALE_AUTHKEY='$tailscale_key' --app $app_name", + "flyctl secrets set SERVICES_PASSWORD='$services_pass' --app $app_name", + "flyctl secrets set OPER_PASSWORD='$oper_pass' --app $app_name", + ); + + my $success_count = 0; + foreach my $cmd (@secret_commands) { + my $output = `$cmd 2>&1`; + if ($? == 0) { + $success_count++; + } else { + print "⚠️ Failed to set secret: $output\n"; + } + } + + if ($success_count == @secret_commands) { + print "✅ Development secrets configured for $app_name\n"; + + if ($tailscale_key =~ /PLACEHOLDER/) { + print "⚠️ Remember to update TAILSCALE_AUTHKEY with a real ephemeral key\n"; + } + + return 1; + } else { + print "❌ Some secrets failed to configure for $app_name\n"; + return 0; + } +} + +sub generate_password { + my ($length) = @_; + my @chars = ('a'..'z', 'A'..'Z', '0'..'9'); + my $password = ''; + for (1..$length) { + $password .= $chars[rand @chars]; + } + return $password; +} + +sub list_dev_environment { + my ($username) = @_; + + print "\n📋 Development Environment Status for $username:\n"; + print "=" x 50 . "\n"; + + foreach my $base_name (@DEV_APPS) { + my $app_name = get_dev_app_name($base_name, $username); + my $volume_name = "${base_name}_${username}_data"; + + printf "App: %-20s ", $app_name; + if (app_exists($app_name)) { + print "✅ Created"; + + # Check volume + if (volume_exists($app_name, $volume_name)) { + print " | Volume: ✅"; + } else { + print " | Volume: ❌"; + } + + # Get URL if available + my $status_output = `flyctl status --app $app_name --json 2>/dev/null`; + if ($? == 0) { + my $status_data = eval { decode_json($status_output) }; + if ($status_data && $status_data->{Hostname}) { + print " | URL: https://$status_data->{Hostname}"; + } + } + print "\n"; + } else { + print "❌ Not created\n"; + } + } + + print "\nNext steps:\n"; + print "1. Update Tailscale auth keys: flyctl secrets set TAILSCALE_AUTHKEY= --app \n"; + print "2. Deploy your environment: scripts/deploy-dev.pl\n"; + print "3. Access via: flyctl ssh console --app " . get_dev_app_name('magnet-hub', $username) . "\n"; +} + +sub cleanup_dev_environment { + my ($username, $confirm) = @_; + + unless ($confirm) { + print "⚠️ This will DELETE all development apps and volumes for $username!\n"; + print "Use --confirm to proceed with cleanup.\n"; + return 0; + } + + print "🗑️ Cleaning up development environment for $username...\n"; + + my $cleanup_count = 0; + foreach my $base_name (@DEV_APPS) { + my $app_name = get_dev_app_name($base_name, $username); + + if (app_exists($app_name)) { + print "Destroying app $app_name...\n"; + + my $cmd = "flyctl apps destroy $app_name --yes"; + my $output = `$cmd 2>&1`; + + if ($? == 0) { + print "✅ Destroyed $app_name\n"; + $cleanup_count++; + } else { + print "❌ Failed to destroy $app_name:\n$output\n"; + } + } + } + + print "\n✅ Cleanup complete: $cleanup_count apps destroyed\n"; + return 1; +} + +sub main { + my $username = get_username(); + my $list_only = 0; + my $cleanup = 0; + my $confirm = 0; + my $help = 0; + + GetOptions( + 'user=s' => \$username, + 'list' => \$list_only, + 'cleanup' => \$cleanup, + 'confirm' => \$confirm, + 'help|h' => \$help, + ) or die "Error in command line arguments\n"; + + if ($help) { + print < { + region => 'ord', + memory => '1gb', + cpus => 1, + volume_size => 3, + ports => [6667, 6697, 7000, 8080], + }, + 'magnet-1eu' => { + region => 'ams', + memory => '1gb', + cpus => 1, + volume_size => 3, + ports => [6667, 6697, 7000, 8080], + }, + 'magnet-atheme' => { + region => 'ord', + memory => '2gb', + cpus => 2, + volume_size => 3, + ports => [8080], + }, +); + +# Test 1: Verify fly.toml files exist +subtest 'fly.toml files exist' => sub { + foreach my $app (keys %EXPECTED_APPS) { + my $fly_toml_path = "servers/$app/fly.toml"; + ok(-f $fly_toml_path, "fly.toml exists for $app"); + } +}; + +# Test 2: Validate fly.toml configuration structure +subtest 'fly.toml configuration validity' => sub { + foreach my $app (keys %EXPECTED_APPS) { + my $fly_toml_path = "servers/$app/fly.toml"; + skip_all "fly.toml not found for $app" unless -f $fly_toml_path; + + open my $fh, '<', $fly_toml_path or die "Can't open $fly_toml_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + # Basic validation without TOML parser + like($content, qr/^app\s*=\s*"$app"/m, "App name matches for $app"); + like($content, qr/^primary_region\s*=\s*"$EXPECTED_APPS{$app}->{region}"/m, + "Primary region correct for $app"); + like($content, qr/\[vm\]/m, "VM configuration exists for $app"); + like($content, qr/\[mounts\]/m, "Mount configuration exists for $app"); + } +}; + +# Test 3: Validate resource allocation +subtest 'resource allocation' => sub { + foreach my $app (keys %EXPECTED_APPS) { + my $fly_toml_path = "servers/$app/fly.toml"; + skip_all "fly.toml not found for $app" unless -f $fly_toml_path; + + open my $fh, '<', $fly_toml_path or die "Can't open $fly_toml_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/memory\s*=\s*"$EXPECTED_APPS{$app}->{memory}"/m, + "Memory allocation correct for $app"); + like($content, qr/cpus\s*=\s*$EXPECTED_APPS{$app}->{cpus}/m, + "CPU allocation correct for $app"); + } +}; + +# Test 4: Validate volume configuration +subtest 'volume configuration' => sub { + foreach my $app (keys %EXPECTED_APPS) { + my $fly_toml_path = "servers/$app/fly.toml"; + skip_all "fly.toml not found for $app" unless -f $fly_toml_path; + + open my $fh, '<', $fly_toml_path or die "Can't open $fly_toml_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/source\s*=/m, "Volume source defined for $app"); + like($content, qr/destination\s*=/m, "Volume destination defined for $app"); + } +}; + +# Test 5: Validate health check configuration +subtest 'health check endpoints' => sub { + foreach my $app (keys %EXPECTED_APPS) { + my $fly_toml_path = "servers/$app/fly.toml"; + skip_all "fly.toml not found for $app" unless -f $fly_toml_path; + + open my $fh, '<', $fly_toml_path or die "Can't open $fly_toml_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/\[http_service\]/m, "HTTP service defined for $app"); + like($content, qr/internal_port\s*=\s*8080/m, + "Health check port is 8080 for $app"); + like($content, qr/path\s*=\s*"\/health"/m, "Health check path is /health for $app"); + } +}; + +# Test 6: Validate service ports +subtest 'service ports configuration' => sub { + foreach my $app (keys %EXPECTED_APPS) { + my $fly_toml_path = "servers/$app/fly.toml"; + skip_all "fly.toml not found for $app" unless -f $fly_toml_path; + + open my $fh, '<', $fly_toml_path or die "Can't open $fly_toml_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + if ($app eq 'magnet-9rl' || $app eq 'magnet-1eu') { + # IRC servers need additional service ports + like($content, qr/\[\[services\]\]/m, "Services section exists for IRC server $app"); + } else { + pass("Service ports check for $app"); + } + } +}; + +# Test 7: Volume creation script exists and is executable +subtest 'volume creation script' => sub { + my $script_path = 'scripts/create-volumes.pl'; + ok(-f $script_path, 'create-volumes.pl exists'); + skip_all "create-volumes.pl not found" unless -f $script_path; + ok(-x $script_path, 'create-volumes.pl is executable'); +}; + +# Test 8: Documentation exists +subtest 'deployment documentation' => sub { + my $doc_path = 'docs/deployment-prerequisites.md'; + ok(-f $doc_path, 'deployment-prerequisites.md exists'); + skip_all "deployment-prerequisites.md not found" unless -f $doc_path; + + open my $fh, '<', $doc_path or die "Can't open $doc_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/Fly\.io CLI/i, 'Documents Fly.io CLI requirements'); + like($content, qr/Authentication/i, 'Documents authentication setup'); + like($content, qr/Rollback/i, 'Documents rollback procedures'); + like($content, qr/GitHub Actions/i, 'Documents CI/CD with GitHub Actions'); + like($content, qr/deploy.*token/i, 'Documents deploy token management'); + like($content, qr/remote.*only/i, 'Documents remote builders best practice'); +}; + +# Test 9: Verify Fly.io app deployment (requires fly CLI) +subtest 'fly.io app deployment validation' => sub { + skip_all "Fly CLI not available or not authenticated" + unless system('fly version >/dev/null 2>&1') == 0; + + foreach my $app (keys %EXPECTED_APPS) { + my $cmd = "fly status --app $app 2>&1"; + my $output = `$cmd`; + + if ($? == 0 && $output !~ /Error/) { + pass("App $app is deployed on Fly.io"); + } else { + fail("App $app is not yet deployed on Fly.io"); + } + } +}; + +# Test 10: Verify volume attachments +subtest 'volume attachments' => sub { + skip_all "Fly CLI not available or not authenticated" + unless system('fly version >/dev/null 2>&1') == 0; + + foreach my $app (keys %EXPECTED_APPS) { + my $cmd = "fly volumes list --app $app 2>&1"; + my $output = `$cmd`; + + if ($? == 0 && $output !~ /Error/) { + like($output, qr/3gb/i, "Volume size correct for $app"); + } else { + fail("Volumes not yet created for $app"); + } + } +}; + +# Test 11: GitHub Actions workflow exists and is valid +subtest 'github actions workflow' => sub { + my $workflow_path = '.github/workflows/fly.yml'; + ok(-f $workflow_path, 'GitHub Actions workflow exists'); + skip_all "workflow file not found" unless -f $workflow_path; + + open my $fh, '<', $workflow_path or die "Can't open $workflow_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/name:\s*Deploy to Fly\.io/i, 'Workflow has correct name'); + like($content, qr/on:\s*\n\s*push:/m, 'Workflow triggers on push'); + like($content, qr/FLY_API_TOKEN/i, 'Workflow uses deploy token'); + like($content, qr/--remote-only/i, 'Workflow uses remote builders'); + like($content, qr/prove.*t/i, 'Workflow runs infrastructure tests'); +}; + +# Test 12: Development environment scripts exist and are executable +subtest 'development environment scripts' => sub { + my @scripts = ( + 'scripts/deploy-dev.pl', + 'scripts/setup-dev-env.pl', + 'scripts/cleanup-dev-env.pl', + 'scripts/setup-deploy-tokens.pl' + ); + + foreach my $script (@scripts) { + ok(-f $script, "$script exists"); + skip_all "$script not found" unless -f $script; + ok(-x $script, "$script is executable"); + } +}; + +# Test 13: Development deployment script configuration +subtest 'development deployment script configuration' => sub { + my $script_path = 'scripts/deploy-dev.pl'; + skip_all "deploy-dev.pl not found" unless -f $script_path; + + open my $fh, '<', $script_path or die "Can't open $script_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/per.*user.*dev/i, 'Implements per-user development environments'); + like($content, qr/health.*check/i, 'Implements health checking'); + like($content, qr/remote.*only/i, 'Uses remote builders by default'); + like($content, qr/magnet-hub.*magnet-services/s, 'Includes dev applications'); +}; + +# Test 14: Token management script functionality +subtest 'token management script' => sub { + my $script_path = 'scripts/setup-deploy-tokens.pl'; + skip_all "setup-deploy-tokens.pl not found" unless -f $script_path; + + open my $fh, '<', $script_path or die "Can't open $script_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/tokens.*create.*deploy/i, 'Creates deploy tokens'); + like($content, qr/GitHub.*Actions/i, 'Includes GitHub Actions setup'); + like($content, qr/revoke.*tokens/i, 'Supports token revocation'); +}; + +# Test 15: Development environment setup script +subtest 'development environment setup script' => sub { + my $script_path = 'scripts/setup-dev-env.pl'; + skip_all "setup-dev-env.pl not found" unless -f $script_path; + + open my $fh, '<', $script_path or die "Can't open $script_path: $!"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr/per.*user.*dev/i, 'Implements per-user development setup'); + like($content, qr/volume.*create/i, 'Creates development volumes'); + like($content, qr/secrets.*set/i, 'Configures development secrets'); + like($content, qr/cleanup/i, 'Includes cleanup functionality'); +}; + +done_testing(); \ No newline at end of file