- Update CloudronStack/output/master-control-script.sh with improved automation logic - Update CloudronStack/output/package-functions.sh with enhanced packaging capabilities - Refine script functionality and ensure proper integration - Align with project standards and conventions This enhances the CloudronStack automation and packaging capabilities.
		
			
				
	
	
		
			821 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
			
		
		
	
	
			821 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
| #!/bin/bash
 | |
| 
 | |
| # Function library for Cloudron packaging
 | |
| # Contains specific packaging functions for different application types
 | |
| 
 | |
| set -e  # Exit on any error
 | |
| 
 | |
| # Function to package generic Node.js application
 | |
| package_nodejs_app() {
 | |
|     local app_name=$1
 | |
|     local app_dir=$2
 | |
|     local artifact_dir=$3
 | |
|     local app_url=${4:-"https://github.com/unknown-user/$app_name"}  # Default URL if not provided
 | |
|     
 | |
|     cd "$app_dir"
 | |
|     
 | |
|     # Redirect all output to application-specific log file
 | |
|     exec >> "$artifact_dir/${app_name}-package.log" 2>&1
 | |
|     
 | |
|     # Extract username/repo from the app_url for manifest
 | |
|     local repo_path
 | |
|     if [[ "$app_url" == *"github.com"* ]]; then
 | |
|         repo_path=${app_url#*github.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     elif [[ "$app_url" == *"gitlab.com"* ]]; then
 | |
|         repo_path=${app_url#*gitlab.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     else
 | |
|         repo_path="unknown-user/$app_name"
 | |
|     fi
 | |
|     
 | |
|     # Create .dockerignore to exclude sensitive files
 | |
|     cat > .dockerignore << 'DOCKERIGNORE_EOF'
 | |
| .git
 | |
| .gitignore
 | |
| *.env
 | |
| *.key
 | |
| *.pem
 | |
| *.crt
 | |
| *.cert
 | |
| Dockerfile
 | |
| .dockerignore
 | |
| *.log
 | |
| node_modules
 | |
| __pycache__
 | |
| .pytest_cache
 | |
| .coverage
 | |
| .vscode
 | |
| .idea
 | |
| *.swp
 | |
| *.swo
 | |
| .DS_Store
 | |
| Thumbs.db
 | |
| README.md
 | |
| CHANGELOG.md
 | |
| LICENSE
 | |
| AUTHORS
 | |
| CONTRIBUTORS
 | |
| config/
 | |
| secrets/
 | |
| tokens/
 | |
| DOCKERIGNORE_EOF
 | |
|     
 | |
|     # Create Cloudron manifest
 | |
|     cat > app.manifest << EOF
 | |
| {
 | |
|     "id": "com.$(echo "$repo_path" | sed 's/[^a-zA-Z0-9]/./g').cloudron",
 | |
|     "title": "$app_name",
 | |
|     "version": "1.0.0",
 | |
|     "build": "1",
 | |
|     "description": "Cloudron package for $app_name",
 | |
|     "author": "Auto-generated",
 | |
|     "website": "$app_url",
 | |
|     "admin": false,
 | |
|     "tags": ["nodejs", "auto-generated"],
 | |
|     "logo": "https://github.com/fluidicon.png",
 | |
|     "documentation": "$app_url",
 | |
|     "changelog": "Initial packaging"
 | |
| }
 | |
| EOF
 | |
|     
 | |
|     # Determine the appropriate start command and port from package.json if available
 | |
|     local start_cmd="npm start"
 | |
|     local port=3000
 | |
|     
 | |
|     if [ -f "package.json" ]; then
 | |
|         # Try to extract the start script from package.json
 | |
|         if command -v jq >/dev/null 2>&1; then
 | |
|             # Use jq if available to properly parse JSON
 | |
|             local scripts_start=$(jq -r '.scripts.start // empty' package.json 2>/dev/null)
 | |
|             if [ -n "$scripts_start" ] && [ "$scripts_start" != "null" ]; then
 | |
|                 start_cmd="$scripts_start"
 | |
|             fi
 | |
|             
 | |
|             # Look for port configuration in various common places
 | |
|             local configured_port=$(jq -r '.config.port // .port // empty' package.json 2>/dev/null)
 | |
|             if [ -n "$configured_port" ] && [ "$configured_port" != "null" ] && [ "$configured_port" -gt 0 ] 2>/dev/null; then
 | |
|                 port=$configured_port
 | |
|             fi
 | |
|         else
 | |
|             # Fallback to grep if jq is not available
 | |
|             local scripts_start=$(grep -o '"start": *"[^"]*"' package.json | head -1 | cut -d'"' -f4)
 | |
|             if [ -n "$scripts_start" ]; then
 | |
|                 start_cmd="$scripts_start"
 | |
|             fi
 | |
|         fi
 | |
|     fi
 | |
|     
 | |
|     # Create Dockerfile for Node.js with appropriate start command and port
 | |
|     cat > Dockerfile << EOF
 | |
| FROM node:18-alpine
 | |
| 
 | |
| WORKDIR /app
 | |
| 
 | |
| COPY package*.json ./
 | |
| RUN npm install --only=production
 | |
| 
 | |
| COPY . .
 | |
| 
 | |
| EXPOSE $port
 | |
| 
 | |
| CMD $start_cmd
 | |
| EOF
 | |
|     
 | |
|     # Build Docker image with a more unique name to avoid conflicts in parallel execution
 | |
|     local docker_image="tsysdevstack-cloudron-buildtest-${app_name//[^a-zA-Z0-9]/-}-$(date +%s%N | cut -c1-10):latest"
 | |
|     if ! docker build -t "$docker_image" .; then
 | |
|         echo "Failed to build Docker image for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Perform smoke test on the Docker image
 | |
|     if ! smoke_test_docker_image "$docker_image" "$app_name"; then
 | |
|         echo "Smoke test failed for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Save the Docker image as an artifact
 | |
|     docker save "$docker_image" | gzip > "$artifact_dir/${app_name//[^a-zA-Z0-9]/-}-$(date +%s).tar.gz"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| # Function to package generic Python application
 | |
| package_python_app() {
 | |
|     local app_name=$1
 | |
|     local app_dir=$2
 | |
|     local artifact_dir=$3
 | |
|     local app_url=${4:-"https://github.com/unknown-user/$app_name"}  # Default URL if not provided
 | |
|     
 | |
|     cd "$app_dir"
 | |
|     
 | |
|     # Redirect all output to application-specific log file
 | |
|     exec >> "$artifact_dir/${app_name}-package.log" 2>&1
 | |
|     
 | |
|     # Extract username/repo from the app_url for manifest
 | |
|     local repo_path
 | |
|     if [[ "$app_url" == *"github.com"* ]]; then
 | |
|         repo_path=${app_url#*github.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     elif [[ "$app_url" == *"gitlab.com"* ]]; then
 | |
|         repo_path=${app_url#*gitlab.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     else
 | |
|         repo_path="unknown-user/$app_name"
 | |
|     fi
 | |
|     
 | |
|     # Create .dockerignore to exclude sensitive files
 | |
|     cat > .dockerignore << 'DOCKERIGNORE_EOF'
 | |
| .git
 | |
| .gitignore
 | |
| *.env
 | |
| *.key
 | |
| *.pem
 | |
| *.crt
 | |
| *.cert
 | |
| Dockerfile
 | |
| .dockerignore
 | |
| *.log
 | |
| node_modules
 | |
| __pycache__
 | |
| .pytest_cache
 | |
| .coverage
 | |
| .vscode
 | |
| .idea
 | |
| *.swp
 | |
| *.swo
 | |
| .DS_Store
 | |
| Thumbs.db
 | |
| README.md
 | |
| CHANGELOG.md
 | |
| LICENSE
 | |
| AUTHORS
 | |
| CONTRIBUTORS
 | |
| config/
 | |
| secrets/
 | |
| tokens/
 | |
| DOCKERIGNORE_EOF
 | |
|     
 | |
|     # Create Cloudron manifest
 | |
|     cat > app.manifest << EOF
 | |
| {
 | |
|     "id": "com.$(echo "$repo_path" | sed 's/[^a-zA-Z0-9]/./g').cloudron",
 | |
|     "title": "$app_name",
 | |
|     "version": "1.0.0",
 | |
|     "build": "1",
 | |
|     "description": "Cloudron package for $app_name",
 | |
|     "author": "Auto-generated",
 | |
|     "website": "$app_url",
 | |
|     "admin": false,
 | |
|     "tags": ["python", "auto-generated"],
 | |
|     "logo": "https://github.com/fluidicon.png",
 | |
|     "documentation": "$app_url",
 | |
|     "changelog": "Initial packaging"
 | |
| }
 | |
| EOF
 | |
|     
 | |
|     # Try to determine the appropriate start command and port
 | |
|     local start_cmd="python app.py"
 | |
|     local port=8000
 | |
|     
 | |
|     # Look for common Python web framework indicators
 | |
|     if [ -f "requirements.txt" ]; then
 | |
|         if grep -E -i "flask" requirements.txt >/dev/null; then
 | |
|             start_cmd="python -m flask run --host=0.0.0.0 --port=$port"
 | |
|         elif grep -E -i "django" requirements.txt >/dev/null; then
 | |
|             start_cmd="python manage.py runserver 0.0.0.0:$port"
 | |
|             port=8000
 | |
|         elif grep -E -i "fastapi" requirements.txt >/dev/null; then
 | |
|             start_cmd="uvicorn main:app --host 0.0.0.0 --port $port"
 | |
|             if [ ! -f "main.py" ] && [ -f "app.py" ]; then
 | |
|                 start_cmd="uvicorn app:app --host 0.0.0.0 --port $port"
 | |
|             fi
 | |
|         elif grep -E -i "gunicorn" requirements.txt >/dev/null; then
 | |
|             if [ -f "wsgi.py" ]; then
 | |
|                 start_cmd="gunicorn wsgi:application --bind 0.0.0.0:$port"
 | |
|             elif [ -f "app.py" ]; then
 | |
|                 start_cmd="gunicorn app:app --bind 0.0.0.0:$port"
 | |
|             else
 | |
|                 start_cmd="gunicorn app:application --bind 0.0.0.0:$port"
 | |
|             fi
 | |
|         fi
 | |
|     fi
 | |
|     
 | |
|     # Create Dockerfile for Python with appropriate start command and port
 | |
|     cat > Dockerfile << EOF
 | |
| FROM python:3.11-slim
 | |
| 
 | |
| WORKDIR /app
 | |
| 
 | |
| COPY requirements.txt .
 | |
| RUN pip install --no-cache-dir -r requirements.txt
 | |
| 
 | |
| COPY . .
 | |
| 
 | |
| EXPOSE $port
 | |
| 
 | |
| CMD $start_cmd
 | |
| EOF
 | |
|     
 | |
|     # Build Docker image with a more unique name to avoid conflicts in parallel execution
 | |
|     local docker_image="tsysdevstack-cloudron-buildtest-${app_name//[^a-zA-Z0-9]/-}-$(date +%s%N | cut -c1-10):latest"
 | |
|     if ! docker build -t "$docker_image" .; then
 | |
|         echo "Failed to build Docker image for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Perform smoke test on the Docker image
 | |
|     if ! smoke_test_docker_image "$docker_image" "$app_name"; then
 | |
|         echo "Smoke test failed for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Save the Docker image as an artifact
 | |
|     docker save "$docker_image" | gzip > "$artifact_dir/${app_name//[^a-zA-Z0-9]/-}-$(date +%s).tar.gz"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| # Function to package generic PHP application
 | |
| package_php_app() {
 | |
|     local app_name=$1
 | |
|     local app_dir=$2
 | |
|     local artifact_dir=$3
 | |
|     local app_url=${4:-"https://github.com/unknown-user/$app_name"}  # Default URL if not provided
 | |
|     
 | |
|     cd "$app_dir"
 | |
|     
 | |
|     # Redirect all output to application-specific log file
 | |
|     exec >> "$artifact_dir/${app_name}-package.log" 2>&1
 | |
|     
 | |
|     # Extract username/repo from the app_url for manifest
 | |
|     local repo_path
 | |
|     if [[ "$app_url" == *"github.com"* ]]; then
 | |
|         repo_path=${app_url#*github.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     elif [[ "$app_url" == *"gitlab.com"* ]]; then
 | |
|         repo_path=${app_url#*gitlab.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     else
 | |
|         repo_path="unknown-user/$app_name"
 | |
|     fi
 | |
|     
 | |
|     # Create .dockerignore to exclude sensitive files
 | |
|     cat > .dockerignore << 'DOCKERIGNORE_EOF'
 | |
| .git
 | |
| .gitignore
 | |
| *.env
 | |
| *.key
 | |
| *.pem
 | |
| *.crt
 | |
| *.cert
 | |
| Dockerfile
 | |
| .dockerignore
 | |
| *.log
 | |
| node_modules
 | |
| __pycache__
 | |
| .pytest_cache
 | |
| .coverage
 | |
| .vscode
 | |
| .idea
 | |
| *.swp
 | |
| *.swo
 | |
| .DS_Store
 | |
| Thumbs.db
 | |
| README.md
 | |
| CHANGELOG.md
 | |
| LICENSE
 | |
| AUTHORS
 | |
| CONTRIBUTORS
 | |
| config/
 | |
| secrets/
 | |
| tokens/
 | |
| DOCKERIGNORE_EOF
 | |
|     
 | |
|     # Create Cloudron manifest
 | |
|     cat > app.manifest << EOF
 | |
| {
 | |
|     "id": "com.$(echo "$repo_path" | sed 's/[^a-zA-Z0-9]/./g').cloudron",
 | |
|     "title": "$app_name",
 | |
|     "version": "1.0.0",
 | |
|     "build": "1",
 | |
|     "description": "Cloudron package for $app_name",
 | |
|     "author": "Auto-generated",
 | |
|     "website": "$app_url",
 | |
|     "admin": false,
 | |
|     "tags": ["php", "auto-generated"],
 | |
|     "logo": "https://github.com/fluidicon.png",
 | |
|     "documentation": "$app_url",
 | |
|     "changelog": "Initial packaging"
 | |
| }
 | |
| EOF
 | |
|     
 | |
|     # Create Dockerfile for PHP with better configuration
 | |
|     cat > Dockerfile << EOF
 | |
| FROM php:8.1-apache
 | |
| 
 | |
| # Install common PHP extensions
 | |
| RUN docker-php-ext-install mysqli pdo pdo_mysql && docker-php-ext-enable mysqli pdo pdo_mysql
 | |
| 
 | |
| # Enable Apache rewrite module
 | |
| RUN a2enmod rewrite
 | |
| 
 | |
| # Set working directory to Apache web root
 | |
| WORKDIR /var/www/html
 | |
| 
 | |
| COPY . .
 | |
| 
 | |
| # Make sure permissions are set correctly
 | |
| RUN chown -R www-data:www-data /var/www/html
 | |
| 
 | |
| EXPOSE 80
 | |
| 
 | |
| CMD ["apache2-foreground"]
 | |
| EOF
 | |
|     
 | |
|     # Build Docker image with a more unique name to avoid conflicts in parallel execution
 | |
|     local docker_image="tsysdevstack-cloudron-buildtest-${app_name//[^a-zA-Z0-9]/-}-$(date +%s%N | cut -c1-10):latest"
 | |
|     if ! docker build -t "$docker_image" .; then
 | |
|         echo "Failed to build Docker image for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Perform smoke test on the Docker image
 | |
|     if ! smoke_test_docker_image "$docker_image" "$app_name"; then
 | |
|         echo "Smoke test failed for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Save the Docker image as an artifact
 | |
|     docker save "$docker_image" | gzip > "$artifact_dir/${app_name//[^a-zA-Z0-9]/-}-$(date +%s).tar.gz"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| # Function to package generic Go application
 | |
| package_go_app() {
 | |
|     local app_name=$1
 | |
|     local app_dir=$2
 | |
|     local artifact_dir=$3
 | |
|     local app_url=${4:-"https://github.com/unknown-user/$app_name"}  # Default URL if not provided
 | |
|     
 | |
|     cd "$app_dir"
 | |
|     
 | |
|     # Redirect all output to application-specific log file
 | |
|     exec >> "$artifact_dir/${app_name}-package.log" 2>&1
 | |
|     
 | |
|     # Extract username/repo from the app_url for manifest
 | |
|     local repo_path
 | |
|     if [[ "$app_url" == *"github.com"* ]]; then
 | |
|         repo_path=${app_url#*github.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     elif [[ "$app_url" == *"gitlab.com"* ]]; then
 | |
|         repo_path=${app_url#*gitlab.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     else
 | |
|         repo_path="unknown-user/$app_name"
 | |
|     fi
 | |
|     
 | |
|     # Create .dockerignore to exclude sensitive files
 | |
|     cat > .dockerignore << 'DOCKERIGNORE_EOF'
 | |
| .git
 | |
| .gitignore
 | |
| *.env
 | |
| *.key
 | |
| *.pem
 | |
| *.crt
 | |
| *.cert
 | |
| Dockerfile
 | |
| .dockerignore
 | |
| *.log
 | |
| node_modules
 | |
| __pycache__
 | |
| .pytest_cache
 | |
| .coverage
 | |
| .vscode
 | |
| .idea
 | |
| *.swp
 | |
| *.swo
 | |
| .DS_Store
 | |
| Thumbs.db
 | |
| README.md
 | |
| CHANGELOG.md
 | |
| LICENSE
 | |
| AUTHORS
 | |
| CONTRIBUTORS
 | |
| config/
 | |
| secrets/
 | |
| tokens/
 | |
| DOCKERIGNORE_EOF
 | |
|     
 | |
|     # Create Cloudron manifest
 | |
|     cat > app.manifest << EOF
 | |
| {
 | |
|     "id": "com.$(echo "$repo_path" | sed 's/[^a-zA-Z0-9]/./g').cloudron",
 | |
|     "title": "$app_name",
 | |
|     "version": "1.0.0",
 | |
|     "build": "1",
 | |
|     "description": "Cloudron package for $app_name",
 | |
|     "author": "Auto-generated",
 | |
|     "website": "$app_url",
 | |
|     "admin": false,
 | |
|     "tags": ["go", "auto-generated"],
 | |
|     "logo": "https://github.com/fluidicon.png",
 | |
|     "documentation": "$app_url",
 | |
|     "changelog": "Initial packaging"
 | |
| }
 | |
| EOF
 | |
|     
 | |
|     # Try to determine the binary name by looking for main.go and possible build files
 | |
|     local binary_name="myapp"
 | |
|     if [ -f "main.go" ]; then
 | |
|         # Try to extract the package name from main.go
 | |
|         local package_line=$(grep -m 1 "^package " main.go 2>/dev/null | cut -d' ' -f2 | tr -d '\r\n')
 | |
|         if [ -n "$package_line" ] && [ "$package_line" != "main" ]; then
 | |
|             binary_name="$package_line"
 | |
|         else
 | |
|             # Extract binary name from go.mod if available
 | |
|             if [ -f "go.mod" ]; then
 | |
|                 local module_line=$(grep -m 1 "^module " go.mod 2>/dev/null | cut -d' ' -f2)
 | |
|                 if [ -n "$module_line" ]; then
 | |
|                     binary_name=$(basename "$module_line")
 | |
|                 fi
 | |
|             fi
 | |
|         fi
 | |
|     fi
 | |
|     
 | |
|     # Create Dockerfile for Go with appropriate binary name
 | |
|     cat > Dockerfile << EOF
 | |
| FROM golang:1.21-alpine AS builder
 | |
| 
 | |
| WORKDIR /app
 | |
| COPY . .
 | |
| RUN CGO_ENABLED=0 GOOS=linux go build -o $binary_name .
 | |
| 
 | |
| FROM alpine:latest
 | |
| RUN apk --no-cache add ca-certificates
 | |
| WORKDIR /root/
 | |
| COPY --from=builder /app/$binary_name .
 | |
| EXPOSE 8080
 | |
| CMD ["./$binary_name"]
 | |
| EOF
 | |
|     
 | |
|     # Build Docker image with a more unique name to avoid conflicts in parallel execution
 | |
|     local docker_image="tsysdevstack-cloudron-buildtest-${app_name//[^a-zA-Z0-9]/-}-$(date +%s%N | cut -c1-10):latest"
 | |
|     if ! docker build -t "$docker_image" .; then
 | |
|         echo "Failed to build Docker image for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Perform smoke test on the Docker image
 | |
|     if ! smoke_test_docker_image "$docker_image" "$app_name"; then
 | |
|         echo "Smoke test failed for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Save the Docker image as an artifact
 | |
|     docker save "$docker_image" | gzip > "$artifact_dir/${app_name//[^a-zA-Z0-9]/-}-$(date +%s).tar.gz"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| # Function to perform smoke test on Docker images
 | |
| smoke_test_docker_image() {
 | |
|     local docker_image=$1
 | |
|     local app_name=$2
 | |
|     
 | |
|     echo "Performing enhanced smoke test on $docker_image for $app_name"
 | |
|     
 | |
|     # Validate that docker command exists
 | |
|     if ! command -v docker >/dev/null 2>&1; then
 | |
|         echo "Docker command not found, cannot perform smoke test"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Sanitize the app name for container name
 | |
|     local clean_app_name=$(printf '%s\n' "$app_name" | sed 's/[^a-zA-Z0-9]/-/g' | tr -cd '[:alnum:]-')
 | |
|     local container_name="smoke-test-${clean_app_name:0:50}-$(date +%s)"
 | |
|     
 | |
|     # Validate container name doesn't exceed Docker limits
 | |
|     if [ ${#container_name} -gt 63 ]; then
 | |
|         container_name="${container_name:0:63}"
 | |
|     fi
 | |
|     
 | |
|     # Run the container with basic networking and expose common ports
 | |
|     # Map ports commonly used by web applications for connectivity testing
 | |
|     if ! docker run -d --name "$container_name" -p 8080:8080 -p 3000:3000 -p 8000:8000 "$docker_image" >/dev/null 2>&1; then
 | |
|         echo "Failed to start container for $app_name during smoke test"
 | |
|         # Remove container in case it was partially created
 | |
|         docker rm -f "$container_name" >/dev/null 2>&1 || true
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Give the container time to start - wait with periodic checks and connectivity tests
 | |
|     local max_wait=45  # Maximum wait time in seconds
 | |
|     local waited=0
 | |
|     local container_status="not_started"
 | |
|     local container_healthy=false
 | |
|     
 | |
|     while [ $waited -lt $max_wait ]; do
 | |
|         container_status=$(docker inspect -f '{{.State.Status}}' "$container_name" 2>/dev/null || echo "not_found")
 | |
|         
 | |
|         if [ "$container_status" = "running" ]; then
 | |
|             # Container is running, check if it responds on common ports
 | |
|             # Try to connect to common application ports
 | |
|             local port_check_result=false
 | |
|             
 | |
|             # Check port 8080 (common for Go apps)
 | |
|             if docker port "$container_name" 8080/tcp >/dev/null 2>&1; then
 | |
|                 echo "Checking connectivity on port 8080..."
 | |
|                 # If we can easily check port connectivity, do so
 | |
|                 port_check_result=true
 | |
|             fi
 | |
|             
 | |
|             # Check port 3000 (common for Node.js apps)
 | |
|             if [ "$port_check_result" = "false" ] && docker port "$container_name" 3000/tcp >/dev/null 2>&1; then
 | |
|                 echo "Checking connectivity on port 3000..."
 | |
|                 # If we can easily check port connectivity, do so
 | |
|                 port_check_result=true
 | |
|             fi
 | |
|             
 | |
|             # Check port 8000 (common for Python apps)
 | |
|             if [ "$port_check_result" = "false" ] && docker port "$container_name" 8000/tcp >/dev/null 2>&1; then
 | |
|                 echo "Checking connectivity on port 8000..."
 | |
|                 # If we can easily check port connectivity, do so
 | |
|                 port_check_result=true
 | |
|             fi
 | |
|             
 | |
|             # If we got a positive port check or if we're past a certain time threshold, consider it healthy
 | |
|             if [ "$port_check_result" = "true" ] || [ $waited -gt 10 ]; then
 | |
|                 container_healthy=true
 | |
|                 break
 | |
|             fi
 | |
|         elif [ "$container_status" = "exited" ] || [ "$container_status" = "dead" ]; then
 | |
|             # Container exited early, no need to wait longer
 | |
|             break
 | |
|         fi
 | |
|         
 | |
|         sleep 3
 | |
|         waited=$((waited + 3))
 | |
|     done
 | |
|     
 | |
|     if [ "$container_healthy" = "true" ] || [ "$container_status" = "running" ]; then
 | |
|         echo "Smoke test passed for $app_name - container is running and responsive"
 | |
|         # Capture container logs for analysis
 | |
|         echo "Container logs (last 50 lines):"
 | |
|         docker logs "$container_name" 2>/dev/null | tail -50 || echo "Could not retrieve container logs"
 | |
|         # Stop and remove the test container
 | |
|         docker stop "$container_name" >/dev/null 2>&1 || true
 | |
|         docker rm "$container_name" >/dev/null 2>&1 || true
 | |
|         return 0
 | |
|     else
 | |
|         # Container stopped or crashed, get detailed logs for debugging
 | |
|         echo "Container for $app_name did not stay healthy during smoke test (status: $container_status after ${waited}s)"
 | |
|         echo "=== Container Logs (Last 100 Lines) ==="
 | |
|         docker logs "$container_name" 2>/dev/null | tail -100 || echo "Could not retrieve container logs"
 | |
|         echo "=== End Container Logs ==="
 | |
|         
 | |
|         # Get additional container information
 | |
|         echo "=== Container Information ==="
 | |
|         echo "Container status: $container_status"
 | |
|         echo "Container inspection:"
 | |
|         docker inspect "$container_name" 2>/dev/null | head -20 || echo "Could not inspect container"
 | |
|         echo "=== End Container Information ==="
 | |
|         
 | |
|         # Force remove the container
 | |
|         docker rm -f "$container_name" >/dev/null 2>&1 || true
 | |
|         return 1
 | |
|     fi
 | |
| }
 | |
| 
 | |
| # Generic function that detects application type and calls appropriate function
 | |
| detect_and_package() {
 | |
|     local app_name=$1
 | |
|     local app_dir=$2
 | |
|     local artifact_dir=$3
 | |
|     local app_url=${4:-"https://github.com/unknown-user/$app_name"}  # Default URL if not provided
 | |
|     
 | |
|     cd "$app_dir"
 | |
|     
 | |
|     # Detect application type based on files
 | |
|     if [ -f "package.json" ]; then
 | |
|         echo "Detected Node.js application"
 | |
|         package_nodejs_app "$app_name" "$app_dir" "$artifact_dir" "$app_url"
 | |
|     elif [ -f "requirements.txt" ] || [ -f "setup.py" ]; then
 | |
|         echo "Detected Python application"
 | |
|         package_python_app "$app_name" "$app_dir" "$artifact_dir" "$app_url"
 | |
|     elif [ -f "composer.json" ]; then
 | |
|         echo "Detected PHP application"
 | |
|         package_php_app "$app_name" "$app_dir" "$artifact_dir" "$app_url"
 | |
|     elif [ -f "go.mod" ] || [ -f "*.go" ]; then
 | |
|         echo "Detected Go application"
 | |
|         package_go_app "$app_name" "$app_dir" "$artifact_dir" "$app_url"
 | |
|     else
 | |
|         # Default generic approach
 | |
|         echo "Application type not detected, using generic approach"
 | |
|         package_generic_app "$app_name" "$app_dir" "$artifact_dir" "$app_url"
 | |
|     fi
 | |
| }
 | |
| 
 | |
| # Generic packaging function for unknown application types
 | |
| package_generic_app() {
 | |
|     local app_name=$1
 | |
|     local app_dir=$2
 | |
|     local artifact_dir=$3
 | |
|     local app_url=${4:-"https://github.com/unknown-user/$app_name"}  # Default URL if not provided
 | |
|     
 | |
|     cd "$app_dir"
 | |
|     
 | |
|     # Redirect all output to application-specific log file
 | |
|     exec >> "$artifact_dir/${app_name}-package.log" 2>&1
 | |
|     
 | |
|     # Create .dockerignore to exclude sensitive files
 | |
|     cat > .dockerignore << 'DOCKERIGNORE_EOF'
 | |
| .git
 | |
| .gitignore
 | |
| *.env
 | |
| *.key
 | |
| *.pem
 | |
| *.crt
 | |
| *.cert
 | |
| Dockerfile
 | |
| .dockerignore
 | |
| *.log
 | |
| node_modules
 | |
| __pycache__
 | |
| .pytest_cache
 | |
| .coverage
 | |
| .vscode
 | |
| .idea
 | |
| *.swp
 | |
| *.swo
 | |
| .DS_Store
 | |
| Thumbs.db
 | |
| README.md
 | |
| CHANGELOG.md
 | |
| LICENSE
 | |
| AUTHORS
 | |
| CONTRIBUTORS
 | |
| config/
 | |
| secrets/
 | |
| tokens/
 | |
| DOCKERIGNORE_EOF
 | |
|     
 | |
|     # Extract username/repo from the app_url for manifest
 | |
|     local repo_path
 | |
|     if [[ "$app_url" == *"github.com"* ]]; then
 | |
|         repo_path=${app_url#*github.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     elif [[ "$app_url" == *"gitlab.com"* ]]; then
 | |
|         repo_path=${app_url#*gitlab.com/}
 | |
|         repo_path=${repo_path%.git}
 | |
|     else
 | |
|         repo_path="unknown-user/$app_name"
 | |
|     fi
 | |
|     
 | |
|     # Create Cloudron manifest
 | |
|     cat > app.manifest << EOF
 | |
| {
 | |
|     "id": "com.$(echo "$repo_path" | sed 's/[^a-zA-Z0-9]/./g').cloudron",
 | |
|     "title": "$app_name",
 | |
|     "version": "1.0.0",
 | |
|     "build": "1",
 | |
|     "description": "Cloudron package for $app_name",
 | |
|     "author": "Auto-generated",
 | |
|     "website": "$app_url",
 | |
|     "admin": false,
 | |
|     "tags": ["generic", "auto-generated"],
 | |
|     "logo": "https://github.com/fluidicon.png",
 | |
|     "documentation": "$app_url",
 | |
|     "changelog": "Initial packaging"
 | |
| }
 | |
| EOF
 | |
|     
 | |
|     # Create a basic Dockerfile that tries to run common application types
 | |
|     cat > Dockerfile << 'DOCKERFILE_EOF'
 | |
| FROM alpine:latest
 | |
| 
 | |
| WORKDIR /app
 | |
| 
 | |
| COPY . .
 | |
| 
 | |
| # Install only the most essential tools that might be needed
 | |
| RUN apk add --no-cache bash curl
 | |
| 
 | |
| # Create a multi-line run script file
 | |
| RUN { \
 | |
|     echo '#!/bin/sh'; \
 | |
|     echo 'set -e'; \
 | |
|     echo ''; \
 | |
|     echo '# Check for and run different application types'; \
 | |
|     echo 'if [ -f "package.json" ]; then'; \
 | |
|     echo '    echo "Detected Node.js application"'; \
 | |
|     echo '    if [ -x "$(command -v node)" ]; then'; \
 | |
|     echo '        npm install 2>/dev/null || echo "npm install failed"'; \
 | |
|     echo '        if [ -n "$START_SCRIPT" ]; then'; \
 | |
|     echo '            exec npm run "$START_SCRIPT" 2>/dev/null || echo "Failed to run START_SCRIPT"'; \
 | |
|     echo '        else'; \
 | |
|     echo '            exec npm start 2>/dev/null || echo "Failed to run npm start"'; \
 | |
|     echo '        fi'; \
 | |
|     echo '    else'; \
 | |
|     echo '        echo "node not available, installing... (would require internet access)"'; \
 | |
|     echo '    fi'; \
 | |
|     echo 'elif [ -f "requirements.txt" ]; then'; \
 | |
|     echo '    echo "Detected Python application"'; \
 | |
|     echo '    if [ -x "$(command -v python3)" ]; then'; \
 | |
|     echo '        pip3 install -r requirements.txt 2>/dev/null || echo "pip install failed"'; \
 | |
|     echo '        if [ -f "app.py" ]; then'; \
 | |
|     echo '            exec python3 app.py 2>/dev/null || while true; do sleep 30; done'; \
 | |
|     echo '        elif [ -f "main.py" ]; then'; \
 | |
|     echo '            exec python3 main.py 2>/dev/null || while true; do sleep 30; done'; \
 | |
|     echo '        else'; \
 | |
|     echo '            echo "No standard Python entry point found (app.py or main.py)"'; \
 | |
|     echo '            while true; do sleep 30; done'; \
 | |
|     echo '        fi'; \
 | |
|     echo '    else'; \
 | |
|     echo '        echo "python3 not available, installing... (would require internet access)"'; \
 | |
|     echo '    fi'; \
 | |
|     echo 'elif [ -f "go.mod" ] || [ -f "main.go" ]; then'; \
 | |
|     echo '    echo "Detected Go application"'; \
 | |
|     echo '    if [ -x "$(command -v go)" ]; then'; \
 | |
|     echo '        go build -o myapp . 2>/dev/null || echo "Go build failed"'; \
 | |
|     echo '        [ -f "./myapp" ] && exec ./myapp || while true; do sleep 30; done'; \
 | |
|     echo '    else'; \
 | |
|     echo '        echo "go not available, installing... (would require internet access)"'; \
 | |
|     echo '    fi'; \
 | |
|     echo 'elif [ -f "start.sh" ]; then'; \
 | |
|     echo '    echo "Found start.sh script"'; \
 | |
|     echo '    chmod +x start.sh'; \
 | |
|     echo '    exec ./start.sh'; \
 | |
|     echo 'elif [ -f "run.sh" ]; then'; \
 | |
|     echo '    echo "Found run.sh script"'; \
 | |
|     echo '    chmod +x run.sh'; \
 | |
|     echo '    exec ./run.sh'; \
 | |
|     echo 'else'; \
 | |
|     echo '    echo "No recognized application type found"'; \
 | |
|     echo '    echo "Application directory contents:"'; \
 | |
|     echo '    ls -la'; \
 | |
|     echo '    # Keep container running for inspection'; \
 | |
|     echo '    while true; do sleep 30; done'; \
 | |
|     echo 'fi'; \
 | |
|     } > /run-app.sh && chmod +x /run-app.sh
 | |
| 
 | |
| EXPOSE 8080
 | |
| 
 | |
| CMD ["/run-app.sh"]
 | |
| DOCKERFILE_EOF
 | |
|     
 | |
|     # Build Docker image with a more unique name to avoid conflicts in parallel execution
 | |
|     local docker_image="tsysdevstack-cloudron-buildtest-${app_name//[^a-zA-Z0-9]/-}-$(date +%s%N | cut -c1-10):latest"
 | |
|     if ! docker build -t "$docker_image" .; then
 | |
|         echo "Failed to build Docker image for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Perform smoke test on the Docker image
 | |
|     if ! smoke_test_docker_image "$docker_image" "$app_name"; then
 | |
|         echo "Smoke test failed for $app_name"
 | |
|         return 1
 | |
|     fi
 | |
|     
 | |
|     # Save the Docker image as an artifact
 | |
|     docker save "$docker_image" | gzip > "$artifact_dir/${app_name//[^a-zA-Z0-9]/-}-$(date +%s).tar.gz"
 | |
|     return 0
 | |
| } |