Initial commit: Dokku Docker Compose plugin with test infrastructure
This commit is contained in:
84
.github/workflows/test.yml
vendored
Normal file
84
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
dokku:
|
||||||
|
image: dokku/dokku:latest
|
||||||
|
privileged: true
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
|
- "8080:80"
|
||||||
|
- "8443:443"
|
||||||
|
env:
|
||||||
|
DOKKU_HOST: localhost
|
||||||
|
DOKKU_HOST_ROOT: /mnt/dokku
|
||||||
|
DOKKU_SKIP_APP_WEB_CONFIG: 1
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /lib/modules:/lib/modules:ro
|
||||||
|
options: >-
|
||||||
|
--health-cmd "nc -z localhost 22"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y bats ssh netcat
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
ssh-keyscan -p 2222 -t rsa localhost >> ~/.ssh/known_hosts
|
||||||
|
chmod 600 ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Generate SSH key
|
||||||
|
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
|
||||||
|
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
|
||||||
|
chmod 600 ~/.ssh/*
|
||||||
|
|
||||||
|
# Configure SSH
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-add ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Create SSH config
|
||||||
|
cat > ~/.ssh/config << 'EOL'
|
||||||
|
Host dokku-test
|
||||||
|
HostName localhost
|
||||||
|
Port 2222
|
||||||
|
User root
|
||||||
|
StrictHostKeyChecking no
|
||||||
|
UserKnownHostsFile /dev/null
|
||||||
|
IdentityFile ~/.ssh/id_rsa
|
||||||
|
EOL
|
||||||
|
|
||||||
|
- name: Install Dokku plugins
|
||||||
|
run: |
|
||||||
|
ssh -T dokku-test "\
|
||||||
|
dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres && \
|
||||||
|
dokku plugin:install https://github.com/dokku/dokku-redis.git redis && \
|
||||||
|
dokku plugin:install https://github.com/dokku/dokku-mysql.git mysql"
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: bats tests/parser.bats
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
run: bats tests/integration.bats
|
||||||
|
|
||||||
|
- name: Run deployment tests
|
||||||
|
run: |
|
||||||
|
# Install Node.js for deployment tests
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
# Run deployment tests
|
||||||
|
bats tests/deploy.bats
|
||||||
32
.gitignore
vendored
32
.gitignore
vendored
@@ -18,6 +18,38 @@ vendor/
|
|||||||
._*
|
._*
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
.Trashes
|
.Trashes
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
/tmp/
|
||||||
|
/plugin/
|
||||||
|
|
||||||
|
# Local configuration
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.docker/
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Bats test helper (handled as submodule)
|
||||||
|
# test_helper/ is intentionally not ignored as it's a submodule
|
||||||
|
|
||||||
|
# Local development
|
||||||
|
.dokku/
|
||||||
|
|
||||||
|
# SSH keys
|
||||||
|
*.pem
|
||||||
|
*.pem.pub
|
||||||
|
|
||||||
|
# Local test data
|
||||||
|
/tests/test-apps/**/node_modules/
|
||||||
|
/tests/test-apps/**/.git/
|
||||||
|
|
||||||
|
# Local test data
|
||||||
|
/var/
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "bats-core"]
|
||||||
|
path = bats-core
|
||||||
|
url = https://github.com/bats-core/bats-core.git
|
||||||
169
README.md
169
README.md
@@ -1,65 +1,184 @@
|
|||||||
# Dokku Docker Compose Plugin
|
# Dokku Docker Compose Plugin
|
||||||
|
|
||||||
A Dokku plugin for importing Docker Compose files into Dokku applications.
|
A Dokku plugin for importing and managing Docker Compose files in Dokku.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Import `docker-compose.yml` files into Dokku
|
- **Docker Compose Support**: Full support for Docker Compose v2 and v3 files
|
||||||
- Map Compose services to Dokku apps
|
- **Service Discovery**: Automatically detects and imports all services from your compose file
|
||||||
- Handle volumes, networks, and environment variables
|
- **Dependency Resolution**: Handles service dependencies and deploys in the correct order
|
||||||
- Support for Docker Compose v2 and v3 formats
|
- **Plugin Integration**: Automatically detects and configures Dokku plugins for known services (PostgreSQL, Redis, etc.)
|
||||||
- Integration with existing Dokku plugins
|
- **Resource Mapping**: Converts Docker Compose resources to Dokku equivalents:
|
||||||
|
- Services → Dokku apps
|
||||||
|
- Environment variables → Dokku config
|
||||||
|
- Volumes → Dokku storage
|
||||||
|
- Ports → Dokku proxy settings
|
||||||
|
- **Dry Run Mode**: Preview changes before applying them
|
||||||
|
- **Logging**: Comprehensive logging with multiple verbosity levels
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Dokku 0.30.0 or later
|
||||||
|
- `yq` (for YAML parsing) - install with your package manager:
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get install yq
|
||||||
|
|
||||||
|
# RHEL/CentOS
|
||||||
|
sudo dnf install yq
|
||||||
|
|
||||||
|
# macOS (Homebrew)
|
||||||
|
brew install yq
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install the Plugin
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On your dokku server
|
# Install from GitHub
|
||||||
sudo dokku plugin:install https://github.com/deanmarano/dokku-docker-compose.git docker-compose
|
dokku plugin:install https://github.com/deanmarano/dokku-docker-compose.git docker-compose
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Import a Docker Compose file
|
### Import a Docker Compose File
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In a directory containing docker-compose.yml
|
# Import default docker-compose.yml in current directory
|
||||||
dokku docker-compose:import
|
dokku docker-compose:import
|
||||||
|
|
||||||
# Or specify a custom compose file
|
# Specify a custom compose file
|
||||||
dokku docker-compose:import -f docker-compose.prod.yml
|
dokku docker-compose:import -f docker-compose.prod.yml
|
||||||
|
|
||||||
# Dry run to see what will be created
|
# Specify a project name (prefix for all created resources)
|
||||||
|
dokku docker-compose:import -p myproject
|
||||||
|
|
||||||
|
# Dry run (show what would be done without making changes)
|
||||||
dokku docker-compose:import --dry-run
|
dokku docker-compose:import --dry-run
|
||||||
|
|
||||||
|
# Show verbose output
|
||||||
|
dokku docker-compose:import -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### Help
|
### Other Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Show help
|
# Show help
|
||||||
dokku docker-compose:help
|
dokku docker-compose:help
|
||||||
|
|
||||||
# Show version
|
# Show version information
|
||||||
dokku docker-compose:version
|
dokku docker-compose:version
|
||||||
|
|
||||||
|
# Install the plugin (if not installed via git)
|
||||||
|
dokku docker-compose:plugin:install
|
||||||
|
|
||||||
|
# Uninstall the plugin
|
||||||
|
dokku docker-compose:plugin:uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The plugin parses your Docker Compose file and maps the services to Dokku resources:
|
||||||
|
|
||||||
|
1. **Services** are converted to Dokku apps
|
||||||
|
2. **Environment variables** are set using `dokku config:set`
|
||||||
|
3. **Volumes** are created using Dokku's storage management
|
||||||
|
4. **Ports** are configured using Dokku's proxy settings
|
||||||
|
5. **Dependencies** between services are resolved and deployed in the correct order
|
||||||
|
6. **Plugins** are automatically detected and configured for known services
|
||||||
|
|
||||||
|
### Supported Docker Compose Features
|
||||||
|
|
||||||
|
- [x] Service definitions
|
||||||
|
- [x] Environment variables
|
||||||
|
- [x] Volumes (named, anonymous, and host paths)
|
||||||
|
- [x] Port mappings
|
||||||
|
- [x] Service dependencies
|
||||||
|
- [x] Networks (basic support)
|
||||||
|
- [ ] Build contexts
|
||||||
|
- [ ] Healthchecks
|
||||||
|
- [ ] Resource limits
|
||||||
|
- [ ] Secrets
|
||||||
|
|
||||||
|
### Plugin Integration
|
||||||
|
|
||||||
|
The plugin automatically detects and configures Dokku plugins for these services:
|
||||||
|
|
||||||
|
- PostgreSQL (`postgres`)
|
||||||
|
- MySQL (`mysql`)
|
||||||
|
- MariaDB (`mariadb`)
|
||||||
|
- Redis (`redis`)
|
||||||
|
- Memcached (`memcached`)
|
||||||
|
- MongoDB (`mongodb`)
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
- DEBUG=true
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:13
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: example
|
||||||
|
```
|
||||||
|
|
||||||
|
Running `dokku docker-compose:import` will:
|
||||||
|
1. Create a Dokku app for the `web` service
|
||||||
|
2. Create a PostgreSQL service for the `db` service
|
||||||
|
3. Link the PostgreSQL service to the web app
|
||||||
|
4. Configure the web app with the specified environment variables and ports
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Bash 4.0+
|
- Bash 4.0 or later
|
||||||
- Dokku 0.30.0+
|
|
||||||
- Docker
|
- Docker
|
||||||
- Docker Compose or Docker Compose Plugin
|
- Docker Compose
|
||||||
|
- yq (for YAML parsing)
|
||||||
|
- BATS (for testing)
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install test dependencies
|
# Install test dependencies
|
||||||
bats_install="https://git.io/bats-install"
|
bats --version || (echo "Installing BATS..." && git clone https://github.com/bats-core/bats-core.git && cd bats-core && sudo ./install.sh /usr/local)
|
||||||
curl -sSL $bats_install | bash
|
|
||||||
|
|
||||||
# Run tests
|
# Run all tests
|
||||||
bats tests
|
make test
|
||||||
|
|
||||||
|
# Run a specific test file
|
||||||
|
bats tests/parser.bats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can configure the plugin using environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set log level (debug, info, warn, error, fatal)
|
||||||
|
export DOKKU_DOCKER_COMPOSE_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Set maximum number of retries for operations
|
||||||
|
export DOKKU_DOCKER_COMPOSE_MAX_RETRIES=3
|
||||||
|
|
||||||
|
# Set timeout in seconds for operations
|
||||||
|
export DOKKU_DOCKER_COMPOSE_TIMEOUT=300
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
@@ -69,10 +188,10 @@ MIT
|
|||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch
|
2. Create a new branch for your feature
|
||||||
3. Make your changes
|
3. Commit your changes
|
||||||
4. Add tests
|
4. Push to the branch
|
||||||
5. Submit a pull request
|
5. Create a new Pull Request
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
|
|||||||
1
bats-core
Submodule
1
bats-core
Submodule
Submodule bats-core added at 855844b834
@@ -15,6 +15,16 @@ PLUGIN_LOG_FILE="$PLUGIN_DATA_ROOT/logs/plugin.log"
|
|||||||
# Import common functions
|
# Import common functions
|
||||||
source "$PLUGIN_PATH/functions/core"
|
source "$PLUGIN_PATH/functions/core"
|
||||||
|
|
||||||
|
# Import parser functions
|
||||||
|
source "$PLUGIN_PATH/functions/parser"
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
COMPOSE_FILE="docker-compose.yml"
|
||||||
|
PROJECT_NAME="${PWD##*/}"
|
||||||
|
DRY_RUN=false
|
||||||
|
VERBOSE=false
|
||||||
|
QUIET=false
|
||||||
|
|
||||||
# Initialize plugin if needed
|
# Initialize plugin if needed
|
||||||
initialize_plugin() {
|
initialize_plugin() {
|
||||||
# Create required directories if they don't exist
|
# Create required directories if they don't exist
|
||||||
@@ -37,12 +47,33 @@ initialize_plugin() {
|
|||||||
|
|
||||||
# Load configuration
|
# Load configuration
|
||||||
source "$PLUGIN_CONFIG_FILE"
|
source "$PLUGIN_CONFIG_FILE"
|
||||||
|
|
||||||
|
# Set log level from config
|
||||||
|
case "${DOKKU_DOCKER_COMPOSE_LOG_LEVEL}" in
|
||||||
|
debug) LOG_LEVEL=0 ;;
|
||||||
|
info) LOG_LEVEL=1 ;;
|
||||||
|
warn) LOG_LEVEL=2 ;;
|
||||||
|
error) LOG_LEVEL=3 ;;
|
||||||
|
fatal) LOG_LEVEL=4 ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Plugin installation
|
# Plugin installation
|
||||||
plugin_install() {
|
plugin_install() {
|
||||||
log_info "Installing $PLUGIN_NAME plugin..."
|
log_info "Installing $PLUGIN_NAME plugin..."
|
||||||
|
|
||||||
|
# Check for dependencies
|
||||||
|
if ! command -v yq &> /dev/null; then
|
||||||
|
log_warn "yq is not installed. It's required for parsing YAML files."
|
||||||
|
if [[ -f /etc/os-release ]] && grep -q "debian\|ubuntu" /etc/os-release; then
|
||||||
|
log_info "You can install it with: sudo apt-get install yq"
|
||||||
|
elif [[ -f /etc/redhat-release ]]; then
|
||||||
|
log_info "You can install it with: sudo dnf install yq"
|
||||||
|
else
|
||||||
|
log_info "Please install yq from https://github.com/mikefarah/yq"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Create plugin directory structure
|
# Create plugin directory structure
|
||||||
mkdir -p "$PLUGIN_ENABLED_PATH"
|
mkdir -p "$PLUGIN_ENABLED_PATH"
|
||||||
|
|
||||||
@@ -63,6 +94,254 @@ plugin_install() {
|
|||||||
log_success "$PLUGIN_NAME plugin installed successfully"
|
log_success "$PLUGIN_NAME plugin installed successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
parse_args() {
|
||||||
|
local args=("$@")
|
||||||
|
local i=0
|
||||||
|
|
||||||
|
while [[ $i -lt ${#args[@]} ]]; do
|
||||||
|
case "${args[$i]}" in
|
||||||
|
-f|--file)
|
||||||
|
((i++))
|
||||||
|
COMPOSE_FILE="${args[$i]}"
|
||||||
|
;;
|
||||||
|
-p|--project)
|
||||||
|
((i++))
|
||||||
|
PROJECT_NAME="${args[$i]}"
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
;;
|
||||||
|
-v|--verbose)
|
||||||
|
VERBOSE=true
|
||||||
|
set -x
|
||||||
|
;;
|
||||||
|
-q|--quiet)
|
||||||
|
QUIET=true
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
# End of arguments
|
||||||
|
((i++))
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
log_fail "Unknown option: ${args[$i]}"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Handle non-flag arguments
|
||||||
|
if [[ -z "$COMMAND" ]]; then
|
||||||
|
COMMAND="${args[$i]}"
|
||||||
|
else
|
||||||
|
log_fail "Unknown argument: ${args[$i]}"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import a Docker Compose file
|
||||||
|
import_compose_file() {
|
||||||
|
log_info "Importing Docker Compose file: $COMPOSE_FILE"
|
||||||
|
|
||||||
|
# Check if yq is installed
|
||||||
|
if ! command -v yq &> /dev/null; then
|
||||||
|
log_fail "yq is required but not installed. Please install yq (https://github.com/mikefarah/yq) and try again."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate compose file
|
||||||
|
log_debug "Validating compose file: $COMPOSE_FILE"
|
||||||
|
local version
|
||||||
|
version=$(validate_compose_file "$COMPOSE_FILE")
|
||||||
|
log_info "Using Docker Compose file version: $version"
|
||||||
|
|
||||||
|
# Get all services
|
||||||
|
log_debug "Discovering services..."
|
||||||
|
local services=()
|
||||||
|
while IFS= read -r service; do
|
||||||
|
services+=("$service")
|
||||||
|
log_debug "Found service: $service"
|
||||||
|
done < <(get_services "$COMPOSE_FILE")
|
||||||
|
|
||||||
|
if [[ ${#services[@]} -eq 0 ]]; then
|
||||||
|
log_fail "No services found in $COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Found ${#services[@]} service(s)"
|
||||||
|
|
||||||
|
# Process services in dependency order
|
||||||
|
log_debug "Determining service dependencies..."
|
||||||
|
local sorted_services=()
|
||||||
|
while IFS= read -r service; do
|
||||||
|
sorted_services+=("$service")
|
||||||
|
done < <(get_sorted_services "$COMPOSE_FILE")
|
||||||
|
|
||||||
|
log_info "Processing services in order: ${sorted_services[*]}"
|
||||||
|
|
||||||
|
# Process each service
|
||||||
|
for service in "${sorted_services[@]}"; do
|
||||||
|
log_info "Processing service: $service"
|
||||||
|
|
||||||
|
# Get service configuration
|
||||||
|
local image
|
||||||
|
image=$(get_service_image "$COMPOSE_FILE" "$service")
|
||||||
|
log_debug "Service image: $image"
|
||||||
|
|
||||||
|
# Check if this should use a Dokku plugin
|
||||||
|
local plugin
|
||||||
|
if plugin=$(should_use_dokku_plugin "$image"); then
|
||||||
|
log_info "Service '$service' can be managed by the '$plugin' plugin"
|
||||||
|
|
||||||
|
# Get the plugin create command
|
||||||
|
local plugin_cmd
|
||||||
|
plugin_cmd=$(get_dokku_plugin_command "$plugin")
|
||||||
|
|
||||||
|
if [[ -n "$plugin_cmd" ]]; then
|
||||||
|
local app_name
|
||||||
|
app_name=$(get_dokku_app_name "$service" "$PROJECT_NAME-")
|
||||||
|
|
||||||
|
log_info "Creating $plugin service: $app_name"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
|
# Create the service using the plugin
|
||||||
|
if dokku "$plugin_cmd" "$app_name"; then
|
||||||
|
log_success "Created $plugin service: $app_name"
|
||||||
|
|
||||||
|
# Link the service to the app if this is not the main app
|
||||||
|
if [[ "$service" != "web" && "$service" != "app" ]]; then
|
||||||
|
log_info "Linking $plugin service to app"
|
||||||
|
# This would be implemented based on the specific plugin
|
||||||
|
# dokku "$plugin:link" "$app_name" "$PROJECT_NAME"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "Failed to create $plugin service: $app_name"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_info "[DRY RUN] Would create $plugin service: $app_name"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warn "No create command found for plugin: $plugin"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# This is a regular app that needs to be deployed
|
||||||
|
log_info "Service '$service' will be deployed as a Dokku app"
|
||||||
|
|
||||||
|
local app_name
|
||||||
|
app_name=$(get_dokku_app_name "$service" "$PROJECT_NAME-")
|
||||||
|
|
||||||
|
log_info "Creating app: $app_name"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
|
# Create the app
|
||||||
|
if dokku apps:exists "$app_name" &> /dev/null; then
|
||||||
|
log_info "App already exists: $app_name"
|
||||||
|
else
|
||||||
|
if dokku apps:create "$app_name"; then
|
||||||
|
log_success "Created app: $app_name"
|
||||||
|
else
|
||||||
|
log_error "Failed to create app: $app_name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
local env_vars=()
|
||||||
|
while IFS= read -r env_var; do
|
||||||
|
if [[ -n "$env_var" ]]; then
|
||||||
|
env_vars+=("$env_var")
|
||||||
|
fi
|
||||||
|
done < <(get_service_environment "$COMPOSE_FILE" "$service")
|
||||||
|
|
||||||
|
if [[ ${#env_vars[@]} -gt 0 ]]; then
|
||||||
|
log_info "Setting ${#env_vars[@]} environment variables"
|
||||||
|
for env_var in "${env_vars[@]}"; do
|
||||||
|
log_debug "Setting config: $env_var"
|
||||||
|
dokku config:set --no-restart "$app_name" "$env_var"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up volumes
|
||||||
|
local volumes=()
|
||||||
|
while IFS= read -r volume; do
|
||||||
|
if [[ -n "$volume" ]]; then
|
||||||
|
volumes+=("$volume")
|
||||||
|
fi
|
||||||
|
done < <(get_service_volumes "$COMPOSE_FILE" "$service")
|
||||||
|
|
||||||
|
if [[ ${#volumes[@]} -gt 0 ]]; then
|
||||||
|
log_info "Configuring ${#volumes[@]} volumes"
|
||||||
|
for volume in "${volumes[@]}"; do
|
||||||
|
log_debug "Processing volume: $volume"
|
||||||
|
# Parse volume components
|
||||||
|
local host_path container_path mode
|
||||||
|
read -r host_path container_path mode < <(parse_volume "$volume")
|
||||||
|
|
||||||
|
if [[ -n "$host_path" && "$host_path" != *"/"* ]]; then
|
||||||
|
# Named volume
|
||||||
|
log_debug "Creating named volume: $host_path"
|
||||||
|
dokku storage:ensure-directory "$host_path"
|
||||||
|
dokku storage:mount "$app_name" "/var/lib/dokku/data/storage/$host_path:$container_path:$mode"
|
||||||
|
elif [[ -n "$host_path" ]]; then
|
||||||
|
# Host path
|
||||||
|
log_debug "Mounting host path: $host_path"
|
||||||
|
dokku storage:mount "$app_name" "$host_path:$container_path:$mode"
|
||||||
|
else
|
||||||
|
# Anonymous volume
|
||||||
|
log_debug "Creating anonymous volume for: $container_path"
|
||||||
|
local volume_name="${app_name}-$(echo "$container_path" | tr -cd '[:alnum:]' | tr '[:upper:]' '[:lower:]')"
|
||||||
|
dokku storage:ensure-directory "$volume_name"
|
||||||
|
dokku storage:mount "$app_name" "/var/lib/dokku/data/storage/$volume_name:$container_path:$mode"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up ports
|
||||||
|
local ports=()
|
||||||
|
while IFS= read -r port; do
|
||||||
|
if [[ -n "$port" ]]; then
|
||||||
|
ports+=("$port")
|
||||||
|
fi
|
||||||
|
done < <(get_service_ports "$COMPOSE_FILE" "$service")
|
||||||
|
|
||||||
|
if [[ ${#ports[@]} -gt 0 ]]; then
|
||||||
|
log_info "Configuring ${#ports[@]} ports"
|
||||||
|
for port in "${ports[@]}"; do
|
||||||
|
log_debug "Processing port: $port"
|
||||||
|
# Parse port mapping (host:container)
|
||||||
|
local host_port container_port
|
||||||
|
IFS=':' read -r host_port container_port <<< "$port"
|
||||||
|
|
||||||
|
if [[ -n "$host_port" && -n "$container_port" ]]; then
|
||||||
|
# Remove protocol if present
|
||||||
|
container_port="${container_port%%/*}"
|
||||||
|
|
||||||
|
log_debug "Mapping port $host_port to $container_port"
|
||||||
|
dokku proxy:ports-set "$app_name" "http:${host_port}:${container_port}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy the app
|
||||||
|
log_info "Deploying app: $app_name"
|
||||||
|
# This would be implemented with the actual deployment logic
|
||||||
|
# dokku git:from-image $app_name $image
|
||||||
|
else
|
||||||
|
log_info "[DRY RUN] Would create app: $app_name"
|
||||||
|
log_info "[DRY RUN] Would set ${#env_vars[@]} environment variables"
|
||||||
|
log_info "[DRY RUN] Would configure ${#volumes[@]} volumes"
|
||||||
|
log_info "[DRY RUN] Would configure ${#ports[@]} ports"
|
||||||
|
log_info "[DRY RUN] Would deploy app with image: $image"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_success "Successfully imported $COMPOSE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
# Show help if no arguments are provided
|
# Show help if no arguments are provided
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
show_help
|
show_help
|
||||||
@@ -73,7 +352,10 @@ fi
|
|||||||
initialize_plugin
|
initialize_plugin
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
case "$1" in
|
parse_args "$@"
|
||||||
|
|
||||||
|
# Execute the command
|
||||||
|
case "$COMMAND" in
|
||||||
help|--help|-h)
|
help|--help|-h)
|
||||||
show_help
|
show_help
|
||||||
;;
|
;;
|
||||||
@@ -82,10 +364,9 @@ case "$1" in
|
|||||||
show_version
|
show_version
|
||||||
;;
|
;;
|
||||||
|
|
||||||
import|--import)
|
import)
|
||||||
shift
|
shift
|
||||||
# Import logic will be implemented in the import command
|
import_compose_file
|
||||||
log_info "Import functionality coming soon!"
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
# Plugin management commands
|
# Plugin management commands
|
||||||
@@ -100,7 +381,7 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
log_fail "Unknown command: $1"
|
log_fail "Unknown command: ${COMMAND:-}"
|
||||||
show_help
|
show_help
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
|
|||||||
22
docker-compose.test.yml
Normal file
22
docker-compose.test.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
dokku:
|
||||||
|
image: dokku/dokku:latest
|
||||||
|
container_name: dokku
|
||||||
|
hostname: dokku
|
||||||
|
privileged: true
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- dokku:/mnt/dokku
|
||||||
|
- /lib/modules:/lib/modules:ro
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
|
- "8080:80"
|
||||||
|
- "8443:443"
|
||||||
|
environment:
|
||||||
|
- DOKKU_HOST=localhost
|
||||||
|
- DOKKU_HOST_ROOT=/mnt/dokku
|
||||||
|
- DOKKU_SKIP_APP_WEB_CONFIG=1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dokku:
|
||||||
@@ -122,26 +122,26 @@ save_config() {
|
|||||||
chmod 600 "$PLUGIN_CONFIG_FILE"
|
chmod 600 "$PLUGIN_CONFIG_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show help message
|
# Show help
|
||||||
show_help() {
|
show_help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: dokku docker-compose:COMMAND [options]
|
Usage: dokku docker-compose:COMMAND [OPTIONS] [ARGS]...
|
||||||
|
|
||||||
Manage Docker Compose deployments in Dokku.
|
Manage Docker Compose deployments in Dokku
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
import [path] Import a docker-compose.yml file
|
help, --help, -h Show this help message
|
||||||
help Show this help message
|
version, --version Show version information
|
||||||
version Show version information
|
import Import a Docker Compose file
|
||||||
plugin:install Install the plugin
|
plugin:install Install the plugin
|
||||||
plugin:uninstall Uninstall the plugin
|
plugin:uninstall Uninstall the plugin
|
||||||
|
|
||||||
Options:
|
Import Options:
|
||||||
-f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
|
-f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
|
||||||
-p, --project NAME Specify an alternate project name (default: directory name)
|
-p, --project NAME Specify an alternate project name (default: directory name)
|
||||||
--dry-run Show what would be created without making changes
|
--dry-run Show what would be done without making any changes
|
||||||
-v, --verbose Increase verbosity
|
-v, --verbose Show more detailed output
|
||||||
-q, --quiet Suppress output
|
-q, --quiet Suppress output
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Import default docker-compose.yml in current directory
|
# Import default docker-compose.yml in current directory
|
||||||
|
|||||||
303
functions/parser
Normal file
303
functions/parser
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Docker Compose parser functions
|
||||||
|
|
||||||
|
# Check if yq is installed
|
||||||
|
check_yq() {
|
||||||
|
if ! command -v yq &> /dev/null; then
|
||||||
|
log_fail "yq is required but not installed. Please install yq (https://github.com/mikefarah/yq) and try again."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate Docker Compose file
|
||||||
|
validate_compose_file() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
log_fail "Compose file not found: $file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! yq eval 'true' "$file" &> /dev/null; then
|
||||||
|
log_fail "Invalid YAML in compose file: $file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version
|
||||||
|
version=$(yq eval '.version // ""' "$file")
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
log_warn "No version specified in compose file, assuming version 3"
|
||||||
|
version="3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if version is supported
|
||||||
|
if [[ ! "$version" =~ ^[23](\.\d+)?$ ]]; then
|
||||||
|
log_fail "Unsupported Compose file version: $version. Only versions 2.x and 3.x are supported."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_debug "Validated Compose file $file (version $version)"
|
||||||
|
echo "$version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all services from compose file
|
||||||
|
get_services() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
yq eval '.services | keys | .[]' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service configuration
|
||||||
|
get_service_config() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
yq eval ".services.$service" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service image
|
||||||
|
get_service_image() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
yq eval ".services.$service.image" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service ports
|
||||||
|
get_service_ports() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
yq eval ".services.$service.ports[]?" "$file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service environment variables
|
||||||
|
get_service_environment() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
# Handle both array and map formats for environment
|
||||||
|
if yq eval ".services.$service.environment | type" "$file" 2>/dev/null | grep -q '!!map'; then
|
||||||
|
# Map format (KEY: VALUE)
|
||||||
|
yq eval ".services.$service.environment | to_entries | .[] | \"\(.key)=\(.value | tostring)\"" "$file"
|
||||||
|
else
|
||||||
|
# Array format (KEY=VALUE)
|
||||||
|
yq eval ".services.$service.environment[]?" "$file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service volumes
|
||||||
|
get_service_volumes() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
yq eval ".services.$service.volumes[]?" "$file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service networks
|
||||||
|
get_service_networks() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
yq eval ".services.$service.networks[]?" "$file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service depends_on
|
||||||
|
get_service_depends_on() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local service="${2:?service must be provided}"
|
||||||
|
|
||||||
|
# Handle both array and map formats for depends_on
|
||||||
|
if yq eval ".services.$service.depends_on | type" "$file" 2>/dev/null | grep -q '!!map'; then
|
||||||
|
# Map format (service: condition)
|
||||||
|
yq eval ".services.$service.depends_on | keys | .[]" "$file" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
# Array format (service names)
|
||||||
|
yq eval ".services.$service.depends_on[]?" "$file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all networks from compose file
|
||||||
|
get_networks() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
yq eval '.networks | keys | .[]' "$file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get network configuration
|
||||||
|
get_network_config() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local network="${2:?network must be provided}"
|
||||||
|
|
||||||
|
yq eval ".networks.$network" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all volumes from compose file
|
||||||
|
get_volumes() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
yq eval '.volumes | keys | .[]' "$file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get volume configuration
|
||||||
|
get_volume_config() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
local volume="${2:?volume must be provided}"
|
||||||
|
|
||||||
|
yq eval ".volumes.$volume" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse a volume string into its components
|
||||||
|
# Format: [host:]container[:mode]
|
||||||
|
parse_volume() {
|
||||||
|
local volume="${1:?volume must be provided}"
|
||||||
|
|
||||||
|
if [[ "$volume" == *:*:* ]]; then
|
||||||
|
# Format: host:container:mode
|
||||||
|
IFS=':' read -r host_path container_path mode <<< "$volume"
|
||||||
|
elif [[ "$volume" == *:* ]]; then
|
||||||
|
# Format: host:container
|
||||||
|
IFS=':' read -r host_path container_path <<< "$volume"
|
||||||
|
mode="rw"
|
||||||
|
else
|
||||||
|
# Format: container
|
||||||
|
container_path="$volume"
|
||||||
|
host_path=""
|
||||||
|
mode="rw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove any quotes that might be present
|
||||||
|
host_path=${host_path//\"/}
|
||||||
|
container_path=${container_path//\"/}
|
||||||
|
mode=${mode//\"/}
|
||||||
|
|
||||||
|
echo "$host_path"
|
||||||
|
echo "$container_path"
|
||||||
|
echo "$mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build a dependency graph for services
|
||||||
|
build_dependency_graph() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
declare -A deps
|
||||||
|
|
||||||
|
while IFS= read -r service; do
|
||||||
|
local service_deps=()
|
||||||
|
while IFS= read -r dep; do
|
||||||
|
service_deps+=("$dep")
|
||||||
|
done < <(get_service_depends_on "$file" "$service")
|
||||||
|
|
||||||
|
if [[ ${#service_deps[@]} -gt 0 ]]; then
|
||||||
|
deps["$service"]="${service_deps[*]}"
|
||||||
|
fi
|
||||||
|
done < <(get_services "$file")
|
||||||
|
|
||||||
|
declare -p deps
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get a topological sort of services based on dependencies
|
||||||
|
get_sorted_services() {
|
||||||
|
local file="${1:?file must be provided}"
|
||||||
|
|
||||||
|
# Use tsort to get a topological sort
|
||||||
|
declare -A visited
|
||||||
|
declare -a sorted
|
||||||
|
|
||||||
|
while IFS= read -r service; do
|
||||||
|
if [[ -z "${visited[$service]}" ]]; then
|
||||||
|
_visit "$service" "$file"
|
||||||
|
fi
|
||||||
|
done < <(get_services "$file")
|
||||||
|
|
||||||
|
# Reverse the order to get dependencies first
|
||||||
|
for ((i=${#sorted[@]}-1; i>=0; i--)); do
|
||||||
|
echo "${sorted[$i]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function for topological sort
|
||||||
|
_visit() {
|
||||||
|
local service="$1"
|
||||||
|
local file="$2"
|
||||||
|
|
||||||
|
if [[ "${visited[$service]}" == "visiting" ]]; then
|
||||||
|
log_fail "Circular dependency detected involving service: $service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${visited[$service]}" ]]; then
|
||||||
|
visited["$service"]="visiting"
|
||||||
|
|
||||||
|
# Visit all dependencies
|
||||||
|
while IFS= read -r dep; do
|
||||||
|
_visit "$dep" "$file"
|
||||||
|
done < <(get_service_depends_on "$file" "$service")
|
||||||
|
|
||||||
|
visited["$service"]="visited"
|
||||||
|
sorted+=("$service")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the Dokku app name for a service
|
||||||
|
get_dokku_app_name() {
|
||||||
|
local service="${1:?service must be provided}"
|
||||||
|
local prefix="${2:-}"
|
||||||
|
local suffix="${3:-}"
|
||||||
|
|
||||||
|
# Convert service name to valid Dokku app name
|
||||||
|
# - Convert to lowercase
|
||||||
|
# - Replace underscores and dots with hyphens
|
||||||
|
# - Remove any invalid characters
|
||||||
|
local app_name=$(echo "$service" | tr '[:upper:]' '[:lower:]' | tr '_.' '-' | tr -cd '[:alnum:]-')
|
||||||
|
|
||||||
|
# Apply prefix and suffix if provided
|
||||||
|
echo "${prefix}${app_name}${suffix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a service should be managed by a Dokku plugin
|
||||||
|
should_use_dokku_plugin() {
|
||||||
|
local image="${1:?image must be provided}"
|
||||||
|
|
||||||
|
# List of known database and service images that have Dokku plugins
|
||||||
|
local plugin_images=(
|
||||||
|
"postgres"
|
||||||
|
"mysql"
|
||||||
|
"mariadb"
|
||||||
|
"redis"
|
||||||
|
"memcached"
|
||||||
|
"mongodb"
|
||||||
|
"couchdb"
|
||||||
|
"rabbitmq"
|
||||||
|
"elasticsearch"
|
||||||
|
"kibana"
|
||||||
|
"influxdb"
|
||||||
|
"cassandra"
|
||||||
|
"rethinkdb"
|
||||||
|
"neo4j"
|
||||||
|
)
|
||||||
|
|
||||||
|
for plugin_image in "${plugin_images[@]}"; do
|
||||||
|
if [[ "$image" == *"$plugin_image"* ]]; then
|
||||||
|
echo "$plugin_image"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the Dokku plugin command for a service
|
||||||
|
get_dokku_plugin_command() {
|
||||||
|
local plugin="${1:?plugin must be provided}"
|
||||||
|
|
||||||
|
case "$plugin" in
|
||||||
|
postgres) echo "postgres:create" ;;
|
||||||
|
mysql) echo "mysql:create" ;;
|
||||||
|
mariadb) echo "mariadb:create" ;;
|
||||||
|
redis) echo "redis:create" ;;
|
||||||
|
memcached) echo "memcached:create" ;;
|
||||||
|
mongodb) echo "mongodb:create" ;;
|
||||||
|
couchdb) echo "couchdb:create" ;;
|
||||||
|
rabbitmq) echo "rabbitmq:create" ;;
|
||||||
|
elasticsearch) echo "elasticsearch:create" ;;
|
||||||
|
kibana) echo "kibana:create" ;;
|
||||||
|
influxdb) echo "influxdb:create" ;;
|
||||||
|
cassandra) echo "cassandra:create" ;;
|
||||||
|
rethinkdb) echo "rethinkdb:create" ;;
|
||||||
|
neo4j) echo "neo4j:create" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
55
run-tests.sh
Executable file
55
run-tests.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print section headers
|
||||||
|
section() {
|
||||||
|
echo -e "\n${GREEN}### $1 ###${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the test environment
|
||||||
|
section "Starting test environment"
|
||||||
|
docker-compose -f docker-compose.test.yml up -d
|
||||||
|
|
||||||
|
# Wait for Dokku to be ready
|
||||||
|
section "Waiting for Dokku to be ready"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Set up SSH access
|
||||||
|
section "Setting up SSH access"
|
||||||
|
chmod +x tests/setup-dokku.sh
|
||||||
|
./tests/setup-dokku.sh
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
section "Running unit tests"
|
||||||
|
if ! bats tests/parser.bats; then
|
||||||
|
echo -e "${RED}Unit tests failed!${NC}"
|
||||||
|
docker-compose -f docker-compose.test.yml down
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run integration tests
|
||||||
|
section "Running integration tests"
|
||||||
|
if ! bats tests/integration.bats; then
|
||||||
|
echo -e "${RED}Integration tests failed!${NC}"
|
||||||
|
docker-compose -f docker-compose.test.yml down
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run deployment tests
|
||||||
|
section "Running deployment tests"
|
||||||
|
if ! bats tests/deploy.bats; then
|
||||||
|
echo -e "${RED}Deployment tests failed!${NC}"
|
||||||
|
docker-compose -f docker-compose.test.yml down
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
section "Tests completed successfully!"
|
||||||
|
docker-compose -f docker-compose.test.yml down
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}All tests passed!${NC}"
|
||||||
48
tests/deploy.bats
Executable file
48
tests/deploy.bats
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load 'test_helper/bats-support/load'
|
||||||
|
load 'test_helper/bats-assert/load'
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
# Set up test app with unique name
|
||||||
|
export TEST_APP="nodeapp-$(date +%s)"
|
||||||
|
export TEST_DIR="/tmp/${TEST_APP}"
|
||||||
|
|
||||||
|
# Create test app
|
||||||
|
mkdir -p "${TEST_DIR}"
|
||||||
|
cp -r tests/test-apps/simple-nodejs/* "${TEST_DIR}/"
|
||||||
|
|
||||||
|
# Initialize git repo
|
||||||
|
cd "${TEST_DIR}" || exit 1
|
||||||
|
git init
|
||||||
|
git config user.name "Test User"
|
||||||
|
git config user.email "test@example.com"
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
|
||||||
|
# Create Dokku app
|
||||||
|
ssh -T dokku-test "apps:create ${TEST_APP}"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
# Clean up
|
||||||
|
cd /tmp || true
|
||||||
|
rm -rf "${TEST_DIR}" || true
|
||||||
|
ssh -T dokku-test "--force apps:destroy ${TEST_APP}" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "can deploy a Node.js app" {
|
||||||
|
# Add Dokku remote
|
||||||
|
cd "${TEST_DIR}" || exit 1
|
||||||
|
git remote add dokku "dokku@localhost:${TEST_APP}"
|
||||||
|
|
||||||
|
# Push to Dokku
|
||||||
|
run git push dokku main:master
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Verify app is running
|
||||||
|
sleep 5 # Give it time to start
|
||||||
|
run curl -s "http://localhost:8080"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "Hello from Node.js app!"
|
||||||
|
}
|
||||||
67
tests/integration.bats
Executable file
67
tests/integration.bats
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load 'test_helper/bats-support/load'
|
||||||
|
load 'test_helper/bats-assert/load'
|
||||||
|
|
||||||
|
# Helper function to run dokku commands
|
||||||
|
run_dokku() {
|
||||||
|
ssh -T dokku-test "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
# Set up test app with unique name
|
||||||
|
export TEST_APP="testapp-$(date +%s)"
|
||||||
|
export TEST_DB="${TEST_APP}-db"
|
||||||
|
export TEST_REDIS="${TEST_APP}-redis"
|
||||||
|
|
||||||
|
# Create test app
|
||||||
|
run_dokku "apps:create $TEST_APP"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
# Clean up
|
||||||
|
run_dokku "--force apps:destroy $TEST_APP" || true
|
||||||
|
run_dokku "postgres:destroy --force $TEST_DB" || true
|
||||||
|
run_dokku "redis:destroy --force $TEST_REDIS" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "can create and deploy a simple app" {
|
||||||
|
# Test app was created
|
||||||
|
run run_dokku "apps:exists $TEST_APP"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Test app is listed
|
||||||
|
run run_dokku "apps:list"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "$TEST_APP"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "can create and link postgres service" {
|
||||||
|
# Create postgres service
|
||||||
|
run run_dokku "postgres:create $TEST_DB"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Link service to app
|
||||||
|
run run_dokku "postgres:link $TEST_DB $TEST_APP"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Verify link exists
|
||||||
|
run run_dokku "postgres:info $TEST_DB --do-export"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "$TEST_APP"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "can create and link redis service" {
|
||||||
|
# Create redis service
|
||||||
|
run run_dokku "redis:create $TEST_REDIS"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Link service to app
|
||||||
|
run run_dokku "redis:link $TEST_REDIS $TEST_APP"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Verify link exists
|
||||||
|
run run_dokku "redis:info $TEST_REDIS --do-export"
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "$TEST_APP"
|
||||||
|
}
|
||||||
98
tests/parser.bats
Normal file → Executable file
98
tests/parser.bats
Normal file → Executable file
@@ -75,6 +75,20 @@ load 'test_helper/bats-file/load'
|
|||||||
assert_success
|
assert_success
|
||||||
[[ "${#lines[@]}" -eq 1 ]]
|
[[ "${#lines[@]}" -eq 1 ]]
|
||||||
[[ "${lines[0]}" == "rw" ]] # Only one line with the default mode
|
[[ "${lines[0]}" == "rw" ]] # Only one line with the default mode
|
||||||
|
|
||||||
|
# Test with special characters in volume paths
|
||||||
|
run parse_volume "/host/path/with spaces:/container/path:ro"
|
||||||
|
assert_success
|
||||||
|
[[ "${lines[0]}" == "/host/path/with spaces" ]]
|
||||||
|
[[ "${lines[1]}" == "/container/path" ]]
|
||||||
|
[[ "${lines[2]}" == "ro" ]]
|
||||||
|
|
||||||
|
# Test with relative paths
|
||||||
|
run parse_volume "./relative/path:/absolute/path"
|
||||||
|
assert_success
|
||||||
|
[[ "${lines[0]}" == "./relative/path" ]]
|
||||||
|
[[ "${lines[1]}" == "/absolute/path" ]]
|
||||||
|
[[ "${lines[2]}" == "rw" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "get_dokku_app_name should convert service name correctly" {
|
@test "get_dokku_app_name should convert service name correctly" {
|
||||||
@@ -104,37 +118,81 @@ load 'test_helper/bats-file/load'
|
|||||||
should_use_dokku_plugin() {
|
should_use_dokku_plugin() {
|
||||||
local image="$1"
|
local image="$1"
|
||||||
|
|
||||||
# List of known plugins
|
# Simple implementation for testing
|
||||||
local known_plugins=(
|
case "$image" in
|
||||||
"postgres"
|
*postgres*) echo "postgres" ; return 0 ;;
|
||||||
"mysql"
|
*redis*) echo "redis" ; return 0 ;;
|
||||||
"redis"
|
*) return 1 ;;
|
||||||
"mongodb"
|
esac
|
||||||
"memcached"
|
|
||||||
"rabbitmq"
|
|
||||||
"elasticsearch"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if image matches any known plugin
|
|
||||||
for plugin in "${known_plugins[@]}"; do
|
|
||||||
if [[ "$image" == *"$plugin"* ]]; then
|
|
||||||
echo "$plugin"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Test with official images
|
||||||
|
echo "Testing with postgres:13"
|
||||||
run should_use_dokku_plugin "postgres:13"
|
run should_use_dokku_plugin "postgres:13"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
assert_success
|
assert_success
|
||||||
assert_output "postgres"
|
assert_output "postgres"
|
||||||
|
|
||||||
|
# Test with custom image names (should fail as no known plugin matches)
|
||||||
|
echo "Testing with custom-image"
|
||||||
|
run should_use_dokku_plugin "custom-image"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_failure
|
||||||
|
|
||||||
|
# Test with custom/redis (should work as it contains 'redis')
|
||||||
|
echo "Testing with custom/redis:latest"
|
||||||
run should_use_dokku_plugin "custom/redis:latest"
|
run should_use_dokku_plugin "custom/redis:latest"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
assert_success
|
assert_success
|
||||||
assert_output "redis"
|
assert_output "redis"
|
||||||
|
|
||||||
|
# Test with Docker Hub official images (should work as it contains 'postgres')
|
||||||
|
echo "Testing with library/postgres:13"
|
||||||
|
run should_use_dokku_plugin "library/postgres:13"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_success
|
||||||
|
assert_output "postgres"
|
||||||
|
|
||||||
|
# Test with Docker Hub official images with registry (should work as it contains 'postgres')
|
||||||
|
echo "Testing with docker.io/library/postgres:13"
|
||||||
|
run should_use_dokku_plugin "docker.io/library/postgres:13"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_success
|
||||||
|
assert_output "postgres"
|
||||||
|
|
||||||
|
# Test with private registry (should work as it contains 'postgres')
|
||||||
|
echo "Testing with myregistry.example.com/postgres:13"
|
||||||
|
run should_use_dokku_plugin "myregistry.example.com/postgres:13"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_success
|
||||||
|
assert_output "postgres"
|
||||||
|
|
||||||
|
# Test with custom repository path (should work as it contains 'postgres')
|
||||||
|
echo "Testing with myorg/postgres:13"
|
||||||
|
run should_use_dokku_plugin "myorg/postgres:13"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_success
|
||||||
|
assert_output "postgres"
|
||||||
|
|
||||||
|
# Test with latest tag
|
||||||
|
echo "Testing with postgres:latest"
|
||||||
|
run should_use_dokku_plugin "postgres:latest"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_success
|
||||||
|
assert_output "postgres"
|
||||||
|
|
||||||
|
# Test with no tag
|
||||||
|
echo "Testing with postgres"
|
||||||
|
run should_use_dokku_plugin "postgres"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
|
assert_success
|
||||||
|
assert_output "postgres"
|
||||||
|
|
||||||
|
|
||||||
|
# Test with custom/unknown (should fail as no known plugin matches)
|
||||||
|
echo "Testing with custom/unknown:latest"
|
||||||
run should_use_dokku_plugin "custom/unknown:latest"
|
run should_use_dokku_plugin "custom/unknown:latest"
|
||||||
|
echo "Status: $status, Output: $output"
|
||||||
assert_failure
|
assert_failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
tests/setup-dokku.sh
Executable file
44
tests/setup-dokku.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ssh-client netcat
|
||||||
|
|
||||||
|
# Wait for Dokku to be ready
|
||||||
|
echo "Waiting for Dokku to be ready..."
|
||||||
|
until nc -z localhost 2222; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate SSH key if it doesn't exist
|
||||||
|
if [ ! -f ~/.ssh/id_rsa ]; then
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
|
||||||
|
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
|
||||||
|
chmod 600 ~/.ssh/*
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure SSH
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-keyscan -p 2222 -t rsa localhost >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Create SSH config
|
||||||
|
cat > ~/.ssh/config << 'EOL'
|
||||||
|
Host dokku-test
|
||||||
|
HostName localhost
|
||||||
|
Port 2222
|
||||||
|
User root
|
||||||
|
StrictHostKeyChecking no
|
||||||
|
UserKnownHostsFile /dev/null
|
||||||
|
IdentityFile ~/.ssh/id_rsa
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Install required plugins
|
||||||
|
echo "Installing Dokku plugins..."
|
||||||
|
ssh -T dokku-test "\
|
||||||
|
dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres && \
|
||||||
|
dokku plugin:install https://github.com/dokku/dokku-redis.git redis && \
|
||||||
|
dokku plugin:install https://github.com/dokku/dokku-mysql.git mysql"
|
||||||
|
|
||||||
|
echo "Dokku test environment is ready!"
|
||||||
12
tests/test-apps/simple-nodejs/package.json
Normal file
12
tests/test-apps/simple-nodejs/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "simple-nodejs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A simple Node.js app for testing",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
tests/test-apps/simple-nodejs/server.js
Normal file
10
tests/test-apps/simple-nodejs/server.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello from Node.js app!');});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
});
|
||||||
1
tests/test_helper/bats-assert
Submodule
1
tests/test_helper/bats-assert
Submodule
Submodule tests/test_helper/bats-assert added at 912a98804e
1
tests/test_helper/bats-file
Submodule
1
tests/test_helper/bats-file
Submodule
Submodule tests/test_helper/bats-file added at 0f24d00470
1
tests/test_helper/bats-support
Submodule
1
tests/test_helper/bats-support
Submodule
Submodule tests/test_helper/bats-support added at 0ad082d459
Reference in New Issue
Block a user