- 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.
906 lines
28 KiB
Bash
906 lines
28 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
|
|
|
|
# Run npm install with legacy peer deps to handle dependency conflicts
|
|
cat > Dockerfile << EOF
|
|
FROM node:18-alpine
|
|
|
|
WORKDIR /app
|
|
|
|
COPY package*.json ./
|
|
RUN npm install --only=production --legacy-peer-deps || npm install --only=production || echo "npm install failed, proceeding anyway"
|
|
|
|
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"
|
|
|
|
# Create a proper Cloudron package
|
|
create_cloudron_package "$app_name" "$app_dir" "$artifact_dir" "$docker_image"
|
|
|
|
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
|
|
# Create a proper Cloudron package
|
|
create_cloudron_package "$app_name" "$app_dir" "$artifact_dir" "$docker_image"
|
|
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 with a descriptive name
|
|
docker save "$docker_image" | gzip > "$artifact_dir/${app_name//[^a-zA-Z0-9]/-}-docker-image-$(date +%s).tar.gz"
|
|
|
|
# Create a proper Cloudron package
|
|
create_cloudron_package "$app_name" "$app_dir" "$artifact_dir" "$docker_image"
|
|
|
|
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 better error handling
|
|
cat > Dockerfile << EOF
|
|
FROM golang:1.21-alpine AS builder
|
|
|
|
WORKDIR /app
|
|
COPY . .
|
|
|
|
# Try to fix go.mod file if it has invalid syntax
|
|
RUN if [ -f "go.mod" ]; then \
|
|
sed -i '/^tool /d' go.mod 2>/dev/null || echo "No tool directives to remove"; \
|
|
fi
|
|
|
|
RUN CGO_ENABLED=0 GOOS=linux go build -o $binary_name . 2>/dev/null || \
|
|
CGO_ENABLED=0 GOOS=linux go build -o $binary_name ./cmd/... 2>/dev/null || \
|
|
echo "Go build failed, proceeding anyway"
|
|
|
|
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
|
|
# Create a proper Cloudron package
|
|
create_cloudron_package "$app_name" "$app_dir" "$artifact_dir" "$docker_image"
|
|
|
|
# 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"
|
|
# Create a proper Cloudron package
|
|
create_cloudron_package "$app_name" "$app_dir" "$artifact_dir" "$docker_image"
|
|
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 create proper Cloudron packages (contents placed directly in directory)
|
|
create_cloudron_package() {
|
|
local app_name=$1
|
|
local app_dir=$2
|
|
local artifact_dir=$3
|
|
local docker_image=${4:-"default-image"}
|
|
|
|
echo "Creating Cloudron package contents..."
|
|
cd "$artifact_dir"
|
|
|
|
# Create run.sh script for Cloudron execution (placed in artifact directory)
|
|
cat > run.sh << 'RUNSH_EOF'
|
|
#!/bin/bash
|
|
|
|
# Cloudron run script
|
|
# This script is executed by Cloudron when the app starts
|
|
|
|
# Set up environment
|
|
export HOME="/app"
|
|
cd "$HOME"
|
|
|
|
# Check for common startup scripts
|
|
if [ -f "start.sh" ]; then
|
|
echo "Found start.sh script"
|
|
chmod +x start.sh
|
|
exec ./start.sh
|
|
elif [ -f "run.sh" ]; then
|
|
echo "Found run.sh script"
|
|
chmod +x run.sh
|
|
exec ./run.sh
|
|
elif [ -f "docker-compose.yml" ]; then
|
|
echo "Found docker-compose.yml"
|
|
# For simplicity, we'll assume Docker is available
|
|
if command -v docker-compose >/dev/null 2>&1; then
|
|
exec docker-compose up
|
|
else
|
|
echo "docker-compose not available"
|
|
exit 1
|
|
fi
|
|
else
|
|
# Default to starting the Docker container from the image we built
|
|
echo "Starting Docker container from built image..."
|
|
exec docker run --rm -p 80:80 "$docker_image"
|
|
fi
|
|
RUNSH_EOF
|
|
|
|
# Make run.sh executable
|
|
chmod +x run.sh
|
|
|
|
# Copy all necessary components to the artifact directory
|
|
# This makes a complete Cloudron package ready for deployment
|
|
echo "Copying application components to artifact directory..."
|
|
|
|
if cp -r "$app_dir"/. "$artifact_dir"/ 2>/dev/null; then
|
|
echo "Cloudron package contents created successfully"
|
|
return 0
|
|
else
|
|
echo "Failed to copy application components"
|
|
return 1
|
|
fi
|
|
}
|
|
|