feat: implement core Go application with web server
- Add Go modules with required dependencies (Gin, UUID, JWT, etc.) - Implement main web server with landing page endpoint - Add comprehensive API endpoints for health and status - Include proper error handling and request validation - Set up CORS middleware and security headers
This commit is contained in:
624
output/scripts/deploy.sh
Executable file
624
output/scripts/deploy.sh
Executable file
@@ -0,0 +1,624 @@
|
||||
#!/bin/bash
|
||||
|
||||
# YourDreamNameHere Production Deployment Script
|
||||
# This script deploys the YDN application to a fresh Ubuntu 24.04 server
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
DEPLOYMENT_USER="${DEPLOYMENT_USER:-root}"
|
||||
DEPLOYMENT_HOST="${DEPLOYMENT_HOST:-}"
|
||||
DOMAIN="${DOMAIN:-yourdreamnamehere.com}"
|
||||
DOCKER_REGISTRY="${DOCKER_REGISTRY:-}"
|
||||
VERSION="${VERSION:-latest}"
|
||||
ENVIRONMENT="${ENVIRONMENT:-production}"
|
||||
ENABLE_MONITORING="${ENABLE_MONITORING:-true}"
|
||||
ENABLE_LOGGING="${ENABLE_LOGGING:-true}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Validate environment
|
||||
validate_environment() {
|
||||
log_info "Validating deployment environment..."
|
||||
|
||||
if [ -z "$DEPLOYMENT_HOST" ]; then
|
||||
log_error "DEPLOYMENT_HOST environment variable is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
log_error "DOMAIN environment variable is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test SSH connection
|
||||
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" "echo 'SSH connection successful'"; then
|
||||
log_error "Failed to connect to $DEPLOYMENT_HOST via SSH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Environment validation passed"
|
||||
}
|
||||
|
||||
# Prepare remote server
|
||||
prepare_server() {
|
||||
log_info "Preparing remote server..."
|
||||
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" << 'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
# Update system
|
||||
apt-get update
|
||||
apt-get upgrade -y
|
||||
|
||||
# Install required packages
|
||||
apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
htop \
|
||||
unzip \
|
||||
software-properties-common \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
ufw \
|
||||
fail2ban \
|
||||
logrotate \
|
||||
certbot \
|
||||
python3-certbot-nginx
|
||||
|
||||
# Install Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt-get update
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
fi
|
||||
|
||||
# Install Docker Compose
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
fi
|
||||
|
||||
# Create deployment directory
|
||||
mkdir -p /opt/ydn
|
||||
cd /opt/ydn
|
||||
|
||||
# Create application user
|
||||
if ! id "ydn" &>/dev/null; then
|
||||
useradd -m -s /bin/bash ydn
|
||||
usermod -aG docker ydn
|
||||
fi
|
||||
|
||||
# Set up directory structure
|
||||
mkdir -p {configs,scripts,logs,ssl,backups,data}
|
||||
chown -R ydn:ydn /opt/ydn
|
||||
|
||||
# Configure firewall
|
||||
ufw --force reset
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow ssh
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw --force enable
|
||||
|
||||
# Configure fail2ban
|
||||
cat > /etc/fail2ban/jail.local << 'FAIL2BAN'
|
||||
[DEFAULT]
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
maxretry = 3
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ssh
|
||||
logpath = /var/log/auth.log
|
||||
|
||||
[nginx-http-auth]
|
||||
enabled = true
|
||||
port = http,https
|
||||
logpath = /var/log/nginx/error.log
|
||||
FAIL2BAN
|
||||
|
||||
systemctl enable fail2ban
|
||||
systemctl restart fail2ban
|
||||
|
||||
# Set up log rotation
|
||||
cat > /etc/logrotate.d/ydn << 'LOGROTATE'
|
||||
/opt/ydn/logs/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 ydn ydn
|
||||
postrotate
|
||||
docker kill -s USR1 ydn-nginx 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
LOGROTATE
|
||||
|
||||
log_success "Server preparation completed"
|
||||
EOF
|
||||
|
||||
log_success "Remote server prepared"
|
||||
}
|
||||
|
||||
# Deploy application files
|
||||
deploy_files() {
|
||||
log_info "Deploying application files..."
|
||||
|
||||
# Create temporary deployment package
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
# Copy necessary files
|
||||
cp -r "$PROJECT_DIR"/* "$TEMP_DIR/"
|
||||
|
||||
# Create production environment file
|
||||
cat > "$TEMP_DIR/.env.prod" << EOF
|
||||
# Production Environment Configuration
|
||||
APP_ENV=production
|
||||
APP_NAME=YourDreamNameHere
|
||||
DOMAIN=$DOMAIN
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=ydn-db
|
||||
DB_PORT=5432
|
||||
DB_USER=ydn_user
|
||||
DB_PASSWORD=$(openssl rand -base64 32)
|
||||
DB_NAME=ydn_db
|
||||
DB_SSLMODE=require
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=ydn-redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=$(openssl rand -base64 32)
|
||||
REDIS_DB=0
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=$(openssl rand -base64 64)
|
||||
JWT_EXPIRY=24h
|
||||
|
||||
# Stripe Configuration
|
||||
STRIPE_PUBLIC_KEY=$STRIPE_PUBLIC_KEY
|
||||
STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY
|
||||
STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET
|
||||
STRIPE_PRICE_ID=$STRIPE_PRICE_ID
|
||||
|
||||
# OVH Configuration
|
||||
OVH_ENDPOINT=$OVH_ENDPOINT
|
||||
OVH_APPLICATION_KEY=$OVH_APPLICATION_KEY
|
||||
OVH_APPLICATION_SECRET=$OVH_APPLICATION_SECRET
|
||||
OVH_CONSUMER_KEY=$OVH_CONSUMER_KEY
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=$SMTP_HOST
|
||||
SMTP_PORT=$SMTP_PORT
|
||||
SMTP_USER=$SMTP_USER
|
||||
SMTP_PASSWORD=$SMTP_PASSWORD
|
||||
SMTP_FROM=$SMTP_FROM
|
||||
|
||||
# Dolibarr Configuration
|
||||
DOLIBARR_URL=https://$DOMAIN/dolibarr
|
||||
DOLIBARR_API_TOKEN=$DOLIBARR_API_TOKEN
|
||||
|
||||
# Monitoring
|
||||
GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 16)
|
||||
|
||||
# Version
|
||||
VERSION=$VERSION
|
||||
EOF
|
||||
|
||||
# Copy files to remote server
|
||||
scp -r "$TEMP_DIR/"* "$DEPLOYMENT_USER@$DEPLOYMENT_HOST:/opt/ydn/"
|
||||
|
||||
# Set correct permissions
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" "chown -R ydn:ydn /opt/ydn"
|
||||
|
||||
log_success "Application files deployed"
|
||||
}
|
||||
|
||||
# Generate SSL certificates
|
||||
setup_ssl() {
|
||||
log_info "Setting up SSL certificates..."
|
||||
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" << EOF
|
||||
set -euo pipefail
|
||||
|
||||
cd /opt/ydn
|
||||
|
||||
# Generate initial nginx config for SSL challenge
|
||||
cat > configs/nginx.init.conf << 'NGINX'
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
server_name $DOMAIN;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://\$server_name\$request_uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
|
||||
# Start temporary nginx for SSL challenge
|
||||
docker run -d \
|
||||
--name ydn-nginx-temp \
|
||||
-p 80:80 \
|
||||
-v /opt/ydn/configs/nginx.init.conf:/etc/nginx/nginx.conf:ro \
|
||||
-v /var/www/html:/var/www/html \
|
||||
nginx:alpine
|
||||
|
||||
# Wait for nginx to start
|
||||
sleep 10
|
||||
|
||||
# Request SSL certificate
|
||||
if [ ! -d "/etc/letsencrypt/live/$DOMAIN" ]; then
|
||||
certbot certonly --webroot \
|
||||
-w /var/www/html \
|
||||
-d $DOMAIN \
|
||||
--email admin@$DOMAIN \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
--non-interactive
|
||||
fi
|
||||
|
||||
# Stop temporary nginx
|
||||
docker stop ydn-nginx-temp
|
||||
docker rm ydn-nginx-temp
|
||||
|
||||
# Copy certificates to project directory
|
||||
mkdir -p ssl
|
||||
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem ssl/
|
||||
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem ssl/
|
||||
chown -R ydn:ydn ssl
|
||||
|
||||
# Set up automatic renewal
|
||||
echo "0 12 * * * /usr/bin/certbot renew --quiet --deploy-hook 'docker kill -s HUP ydn-nginx'" | crontab -
|
||||
|
||||
EOF
|
||||
|
||||
log_success "SSL certificates configured"
|
||||
}
|
||||
|
||||
# Deploy application stack
|
||||
deploy_application() {
|
||||
log_info "Deploying application stack..."
|
||||
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" << EOF
|
||||
set -euo pipefail
|
||||
|
||||
cd /opt/ydn
|
||||
|
||||
# Switch to ydn user
|
||||
sudo -u ydn bash << 'USER_SCRIPT'
|
||||
set -euo pipefail
|
||||
|
||||
# Load environment variables
|
||||
source .env.prod
|
||||
|
||||
# Build and push Docker image (if registry is configured)
|
||||
if [ -n "$DOCKER_REGISTRY" ]; then
|
||||
docker build -t $DOCKER_REGISTRY/ydn-app:\$VERSION .
|
||||
docker push $DOCKER_REGISTRY/ydn-app:\$VERSION
|
||||
fi
|
||||
|
||||
# Create production docker-compose override
|
||||
cat > docker-compose.override.yml << 'OVERRIDE'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
ydn-app:
|
||||
image: ${DOCKER_REGISTRY:-ydn-app}:ydn-app:\${VERSION:-latest}
|
||||
environment:
|
||||
- \${APP_ENV}
|
||||
- \${DB_HOST}
|
||||
- \${DB_USER}
|
||||
- \${DB_PASSWORD}
|
||||
- \${DB_NAME}
|
||||
- \${DB_SSLMODE}
|
||||
- \${REDIS_HOST}
|
||||
- \${REDIS_PASSWORD}
|
||||
- \${JWT_SECRET}
|
||||
- \${STRIPE_PUBLIC_KEY}
|
||||
- \${STRIPE_SECRET_KEY}
|
||||
- \${STRIPE_WEBHOOK_SECRET}
|
||||
- \${STRIPE_PRICE_ID}
|
||||
- \${OVH_ENDPOINT}
|
||||
- \${OVH_APPLICATION_KEY}
|
||||
- \${OVH_APPLICATION_SECRET}
|
||||
- \${OVH_CONSUMER_KEY}
|
||||
- \${SMTP_HOST}
|
||||
- \${SMTP_USER}
|
||||
- \${SMTP_PASSWORD}
|
||||
- \${SMTP_FROM}
|
||||
- \${DOLIBARR_URL}
|
||||
- \${DOLIBARR_API_TOKEN}
|
||||
|
||||
ydn-nginx:
|
||||
volumes:
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
environment:
|
||||
- DOMAIN=\${DOMAIN}
|
||||
|
||||
ydn-backup:
|
||||
environment:
|
||||
- DB_PASSWORD=\${DB_PASSWORD}
|
||||
- BACKUP_WEBHOOK_URL=\${BACKUP_WEBHOOK_URL:-}
|
||||
OVERRIDE
|
||||
|
||||
# Deploy with monitoring if enabled
|
||||
if [ "$ENABLE_MONITORING" = "true" ]; then
|
||||
docker-compose -f docker-compose.prod.yml --profile monitoring up -d
|
||||
else
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
fi
|
||||
|
||||
# Wait for services to be ready
|
||||
sleep 30
|
||||
|
||||
# Run database migrations
|
||||
docker-compose -f docker-compose.prod.yml exec ydn-app /app/main migrate || true
|
||||
|
||||
USER_SCRIPT
|
||||
|
||||
EOF
|
||||
|
||||
log_success "Application stack deployed"
|
||||
}
|
||||
|
||||
# Health check
|
||||
health_check() {
|
||||
log_info "Performing health checks..."
|
||||
|
||||
# Wait for application to start
|
||||
sleep 60
|
||||
|
||||
# Check if services are running
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" << EOF
|
||||
set -euo pipefail
|
||||
|
||||
cd /opt/ydn
|
||||
|
||||
# Check Docker containers
|
||||
if ! docker-compose -f docker-compose.prod.yml ps | grep -q "Up"; then
|
||||
echo "ERROR: Some containers are not running"
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check application health
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost/health > /dev/null 2>&1; then
|
||||
echo "Application health check passed"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ \$i -eq 30 ]; then
|
||||
echo "ERROR: Application health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Check SSL certificate
|
||||
if ! curl -f https://$DOMAIN/health > /dev/null 2>&1; then
|
||||
echo "ERROR: SSL health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All health checks passed"
|
||||
EOF
|
||||
|
||||
log_success "Health checks completed"
|
||||
}
|
||||
|
||||
# Setup monitoring
|
||||
setup_monitoring() {
|
||||
if [ "$ENABLE_MONITORING" != "true" ]; then
|
||||
log_info "Monitoring is disabled"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Setting up monitoring..."
|
||||
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" << EOF
|
||||
set -euo pipefail
|
||||
|
||||
cd /opt/ydn
|
||||
|
||||
# Configure Prometheus
|
||||
cat > configs/prometheus.prod.yml << 'PROMETHEUS'
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
rule_files:
|
||||
- "rules/*.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'ydn-app'
|
||||
static_configs:
|
||||
- targets: ['ydn-app:8080']
|
||||
metrics_path: /metrics
|
||||
|
||||
- job_name: 'nginx'
|
||||
static_configs:
|
||||
- targets: ['ydn-nginx:9113']
|
||||
|
||||
- job_name: 'postgres'
|
||||
static_configs:
|
||||
- targets: ['ydn-db:5432']
|
||||
|
||||
- job_name: 'redis'
|
||||
static_configs:
|
||||
- targets: ['ydn-redis:6379']
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets:
|
||||
- alertmanager:9093
|
||||
PROMETHEUS
|
||||
|
||||
# Restart monitoring services
|
||||
sudo -u ydn docker-compose -f docker-compose.prod.yml --profile monitoring restart ydn-prometheus ydn-grafana
|
||||
|
||||
EOF
|
||||
|
||||
log_success "Monitoring configured"
|
||||
}
|
||||
|
||||
# Deploy to production
|
||||
deploy_production() {
|
||||
log_info "Starting production deployment..."
|
||||
|
||||
validate_environment
|
||||
prepare_server
|
||||
deploy_files
|
||||
setup_ssl
|
||||
deploy_application
|
||||
setup_monitoring
|
||||
health_check
|
||||
|
||||
log_success "🎉 Production deployment completed successfully!"
|
||||
log_info "Application is available at: https://$DOMAIN"
|
||||
|
||||
if [ "$ENABLE_MONITORING" = "true" ]; then
|
||||
log_info "Grafana is available at: https://$DOMAIN/grafana"
|
||||
fi
|
||||
|
||||
log_info "Dolibarr is available at: https://$DOMAIN/dolibarr"
|
||||
}
|
||||
|
||||
# Rollback deployment
|
||||
rollback() {
|
||||
log_warning "Rolling back deployment..."
|
||||
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" << EOF
|
||||
set -euo pipefail
|
||||
|
||||
cd /opt/ydn
|
||||
|
||||
# Get previous version from git
|
||||
PREVIOUS_VERSION=\$(git log --format=%H -n 2 | tail -n 1)
|
||||
|
||||
# Checkout previous version
|
||||
sudo -u ydn git checkout \$PREVIOUS_VERSION
|
||||
|
||||
# Redeploy
|
||||
sudo -u ydn docker-compose -f docker-compose.prod.yml down
|
||||
sudo -u ydn docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
EOF
|
||||
|
||||
log_success "Rollback completed"
|
||||
}
|
||||
|
||||
# Show help
|
||||
show_help() {
|
||||
cat << EOF
|
||||
YourDreamNameHere Production Deployment Script
|
||||
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Commands:
|
||||
deploy Deploy to production (default)
|
||||
rollback Rollback to previous version
|
||||
status Show deployment status
|
||||
help Show this help message
|
||||
|
||||
Environment Variables:
|
||||
DEPLOYMENT_HOST Target server hostname/IP (required)
|
||||
DOMAIN Domain name (required)
|
||||
DOCKER_REGISTRY Docker registry URL (optional)
|
||||
VERSION Version tag (default: latest)
|
||||
ENABLE_MONITORING Enable monitoring (default: true)
|
||||
ENABLE_LOGGING Enable logging (default: true)
|
||||
STRIPE_PUBLIC_KEY Stripe public key (required)
|
||||
STRIPE_SECRET_KEY Stripe secret key (required)
|
||||
STRIPE_WEBHOOK_SECRET Stripe webhook secret (required)
|
||||
STRIPE_PRICE_ID Stripe price ID (required)
|
||||
OVH_ENDPOINT OVH API endpoint
|
||||
OVH_APPLICATION_KEY OVH application key
|
||||
OVH_APPLICATION_SECRET OVH application secret
|
||||
OVH_CONSUMER_KEY OVH consumer key
|
||||
SMTP_HOST SMTP server hostname
|
||||
SMTP_PORT SMTP server port
|
||||
SMTP_USER SMTP username
|
||||
SMTP_PASSWORD SMTP password
|
||||
SMTP_FROM From email address
|
||||
DOLIBARR_API_TOKEN Dolibarr API token
|
||||
BACKUP_WEBHOOK_URL Backup notification webhook URL (optional)
|
||||
|
||||
Examples:
|
||||
# Deploy to production
|
||||
DEPLOYMENT_HOST=192.168.1.100 DOMAIN=example.com $0 deploy
|
||||
|
||||
# Deploy with monitoring disabled
|
||||
DEPLOYMENT_HOST=192.168.1.100 DOMAIN=example.com ENABLE_MONITORING=false $0 deploy
|
||||
|
||||
# Rollback deployment
|
||||
DEPLOYMENT_HOST=192.168.1.100 DOMAIN=example.com $0 rollback
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main execution
|
||||
case "${1:-deploy}" in
|
||||
deploy)
|
||||
deploy_production
|
||||
;;
|
||||
rollback)
|
||||
rollback
|
||||
;;
|
||||
status)
|
||||
ssh "$DEPLOYMENT_USER@$DEPLOYMENT_HOST" "cd /opt/ydn && docker-compose -f docker-compose.prod.yml ps"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user