mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-01-01 01:46:18 +01:00
Compare commits
2 Commits
bahdotsh/v
...
podman
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e62fbc1f | ||
|
|
30659ac5d6 |
207
MANUAL_TEST_CHECKLIST.md
Normal file
207
MANUAL_TEST_CHECKLIST.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Manual Testing Checklist for Podman Support
|
||||
|
||||
## Quick Manual Verification Steps
|
||||
|
||||
### ✅ **Step 1: CLI Help and Options**
|
||||
```bash
|
||||
./target/release/wrkflw run --help
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] `--runtime` option is present
|
||||
- [ ] Shows `docker`, `podman`, `emulation` as possible values
|
||||
- [ ] Default is `docker`
|
||||
- [ ] Help text explains each option
|
||||
|
||||
### ✅ **Step 2: CLI Runtime Selection**
|
||||
```bash
|
||||
# Test each runtime option
|
||||
./target/release/wrkflw run --runtime docker test-workflows/example.yml --verbose
|
||||
./target/release/wrkflw run --runtime podman test-workflows/example.yml --verbose
|
||||
./target/release/wrkflw run --runtime emulation test-workflows/example.yml --verbose
|
||||
|
||||
# Test invalid runtime (should fail)
|
||||
./target/release/wrkflw run --runtime invalid test-workflows/example.yml
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] All valid runtimes are accepted
|
||||
- [ ] Invalid runtime shows clear error message
|
||||
- [ ] Podman mode shows "Podman: Running container" in verbose logs
|
||||
- [ ] Emulation mode works without containers
|
||||
|
||||
### ✅ **Step 3: TUI Runtime Support**
|
||||
```bash
|
||||
./target/release/wrkflw tui test-workflows/
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] TUI starts successfully
|
||||
- [ ] Status bar shows current runtime (bottom of screen)
|
||||
- [ ] Press `e` key to cycle through runtimes: Docker → Podman → Emulation → Docker
|
||||
- [ ] Runtime changes are reflected in status bar
|
||||
- [ ] Podman shows "Connected" or "Not Available" status
|
||||
|
||||
### ✅ **Step 4: TUI Runtime Parameter**
|
||||
```bash
|
||||
./target/release/wrkflw tui --runtime podman test-workflows/
|
||||
./target/release/wrkflw tui --runtime emulation test-workflows/
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] TUI starts with specified runtime
|
||||
- [ ] Status bar reflects the specified runtime
|
||||
|
||||
### ✅ **Step 5: Container Execution Test**
|
||||
Create a simple test workflow:
|
||||
```yaml
|
||||
name: Runtime Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- run: |
|
||||
echo "Runtime test execution"
|
||||
whoami
|
||||
pwd
|
||||
echo "Test completed"
|
||||
```
|
||||
|
||||
Test with different runtimes:
|
||||
```bash
|
||||
./target/release/wrkflw run --runtime podman test-runtime.yml --verbose
|
||||
./target/release/wrkflw run --runtime docker test-runtime.yml --verbose
|
||||
./target/release/wrkflw run --runtime emulation test-runtime.yml --verbose
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] Podman mode runs in containers (shows container logs)
|
||||
- [ ] Docker mode runs in containers (shows container logs)
|
||||
- [ ] Emulation mode runs on host system
|
||||
- [ ] All modes produce similar output
|
||||
|
||||
### ✅ **Step 6: Error Handling**
|
||||
```bash
|
||||
# Test with Podman unavailable (temporarily rename podman binary)
|
||||
sudo mv /usr/local/bin/podman /usr/local/bin/podman.tmp 2>/dev/null || echo "podman not in /usr/local/bin"
|
||||
./target/release/wrkflw run --runtime podman test-runtime.yml
|
||||
sudo mv /usr/local/bin/podman.tmp /usr/local/bin/podman 2>/dev/null || echo "nothing to restore"
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] Shows "Podman is not available. Using emulation mode instead."
|
||||
- [ ] Falls back to emulation gracefully
|
||||
- [ ] Workflow still executes successfully
|
||||
|
||||
### ✅ **Step 7: Container Preservation**
|
||||
```bash
|
||||
# Create a failing workflow
|
||||
echo 'name: Fail Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
fail:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- run: exit 1' > test-fail.yml
|
||||
|
||||
# Test with preservation
|
||||
./target/release/wrkflw run --runtime podman --preserve-containers-on-failure test-fail.yml
|
||||
|
||||
# Check for preserved containers
|
||||
podman ps -a --filter "name=wrkflw-"
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] Failed container is preserved when flag is used
|
||||
- [ ] Container can be inspected with `podman exec -it <container> bash`
|
||||
- [ ] Without flag, containers are cleaned up
|
||||
|
||||
### ✅ **Step 8: Documentation**
|
||||
**Verify:**
|
||||
- [ ] README.md mentions Podman support
|
||||
- [ ] Examples show `--runtime podman` usage
|
||||
- [ ] TUI keybind documentation mentions runtime cycling
|
||||
- [ ] Installation instructions for Podman are present
|
||||
|
||||
## Platform-Specific Tests
|
||||
|
||||
### **Linux:**
|
||||
- [ ] Podman works rootless
|
||||
- [ ] No sudo required for container operations
|
||||
- [ ] Network connectivity works in containers
|
||||
|
||||
### **macOS:**
|
||||
- [ ] Podman machine is initialized and running
|
||||
- [ ] Container execution works correctly
|
||||
- [ ] Volume mounting works for workspace
|
||||
|
||||
### **Windows:**
|
||||
- [ ] Podman Desktop or CLI is installed
|
||||
- [ ] Basic container operations work
|
||||
- [ ] Workspace mounting functions correctly
|
||||
|
||||
## Performance and Resource Tests
|
||||
|
||||
### **Memory Usage:**
|
||||
```bash
|
||||
# Monitor memory during execution
|
||||
./target/release/wrkflw run --runtime podman test-runtime.yml &
|
||||
PID=$!
|
||||
while kill -0 $PID 2>/dev/null; do
|
||||
ps -p $PID -o pid,ppid,pgid,sess,cmd,%mem,%cpu
|
||||
sleep 2
|
||||
done
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] Memory usage is reasonable
|
||||
- [ ] No memory leaks during execution
|
||||
|
||||
### **Container Cleanup:**
|
||||
```bash
|
||||
# Run multiple workflows and check cleanup
|
||||
for i in {1..3}; do
|
||||
./target/release/wrkflw run --runtime podman test-runtime.yml
|
||||
done
|
||||
podman ps -a --filter "name=wrkflw-"
|
||||
```
|
||||
**Verify:**
|
||||
- [ ] No containers remain after execution
|
||||
- [ ] Cleanup is thorough and automatic
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### **Complex Workflow:**
|
||||
Test with a workflow that has:
|
||||
- [ ] Multiple jobs
|
||||
- [ ] Environment variables
|
||||
- [ ] File operations
|
||||
- [ ] Network access
|
||||
- [ ] Package installation
|
||||
|
||||
### **Edge Cases:**
|
||||
- [ ] Very long-running containers
|
||||
- [ ] Large output logs
|
||||
- [ ] Network-intensive operations
|
||||
- [ ] File system intensive operations
|
||||
|
||||
## Final Verification
|
||||
|
||||
**Overall System Check:**
|
||||
- [ ] All runtimes work as expected
|
||||
- [ ] Error messages are clear and helpful
|
||||
- [ ] Performance is acceptable
|
||||
- [ ] User experience is smooth
|
||||
- [ ] Documentation is accurate and complete
|
||||
|
||||
**Sign-off:**
|
||||
- [ ] Basic functionality: ✅ PASS / ❌ FAIL
|
||||
- [ ] CLI integration: ✅ PASS / ❌ FAIL
|
||||
- [ ] TUI integration: ✅ PASS / ❌ FAIL
|
||||
- [ ] Error handling: ✅ PASS / ❌ FAIL
|
||||
- [ ] Documentation: ✅ PASS / ❌ FAIL
|
||||
|
||||
**Notes:**
|
||||
_Add any specific issues, observations, or platform-specific notes here._
|
||||
|
||||
---
|
||||
|
||||
**Testing completed by:** ________________
|
||||
**Date:** ________________
|
||||
**Platform:** ________________
|
||||
**Podman version:** ________________
|
||||
120
README.md
120
README.md
@@ -14,12 +14,12 @@ WRKFLW is a powerful command-line tool for validating and executing GitHub Actio
|
||||
|
||||
- **TUI Interface**: A full-featured terminal user interface for managing and monitoring workflow executions
|
||||
- **Validate Workflow Files**: Check for syntax errors and common mistakes in GitHub Actions workflow files with proper exit codes for CI/CD integration
|
||||
- **Execute Workflows Locally**: Run workflows directly on your machine using Docker containers
|
||||
- **Emulation Mode**: Optional execution without Docker by emulating the container environment locally
|
||||
- **Execute Workflows Locally**: Run workflows directly on your machine using Docker or Podman containers
|
||||
- **Multiple Container Runtimes**: Support for Docker, Podman, and emulation mode for maximum flexibility
|
||||
- **Job Dependency Resolution**: Automatically determines the correct execution order based on job dependencies
|
||||
- **Docker Integration**: Execute workflow steps in isolated Docker containers with proper environment setup
|
||||
- **Container Integration**: Execute workflow steps in isolated containers with proper environment setup
|
||||
- **GitHub Context**: Provides GitHub-like environment variables and workflow commands
|
||||
- **Multiple Runtime Modes**: Choose between Docker containers or local emulation for maximum flexibility
|
||||
- **Rootless Execution**: Podman support enables running containers without root privileges
|
||||
- **Action Support**: Supports various GitHub Actions types:
|
||||
- Docker container actions
|
||||
- JavaScript actions
|
||||
@@ -30,6 +30,41 @@ WRKFLW is a powerful command-line tool for validating and executing GitHub Actio
|
||||
- **Parallel Job Execution**: Runs independent jobs in parallel for faster workflow execution
|
||||
- **Trigger Workflows Remotely**: Manually trigger workflow runs on GitHub or GitLab
|
||||
|
||||
## Requirements
|
||||
|
||||
### Container Runtime (Optional)
|
||||
|
||||
WRKFLW supports multiple container runtimes for isolated execution:
|
||||
|
||||
- **Docker**: The default container runtime. Install from [docker.com](https://docker.com)
|
||||
- **Podman**: A rootless container runtime. Perfect for environments where Docker isn't available or permitted. Install from [podman.io](https://podman.io)
|
||||
- **Emulation**: No container runtime required. Executes commands directly on the host system
|
||||
|
||||
### Podman Support
|
||||
|
||||
Podman is particularly useful in environments where:
|
||||
- Docker installation is not permitted by your organization
|
||||
- Root privileges are not available for Docker daemon
|
||||
- You prefer rootless container execution
|
||||
- Enhanced security through daemonless architecture is desired
|
||||
|
||||
To use Podman:
|
||||
```bash
|
||||
# Install Podman (varies by OS)
|
||||
# On macOS with Homebrew:
|
||||
brew install podman
|
||||
|
||||
# On Ubuntu/Debian:
|
||||
sudo apt-get install podman
|
||||
|
||||
# Initialize Podman machine (macOS/Windows)
|
||||
podman machine init
|
||||
podman machine start
|
||||
|
||||
# Use with wrkflw
|
||||
wrkflw run --runtime podman .github/workflows/ci.yml
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
The recommended way to install `wrkflw` is using Rust's package manager, Cargo:
|
||||
@@ -115,8 +150,11 @@ fi
|
||||
# Run a workflow with Docker (default)
|
||||
wrkflw run .github/workflows/ci.yml
|
||||
|
||||
# Run a workflow in emulation mode (without Docker)
|
||||
wrkflw run --emulate .github/workflows/ci.yml
|
||||
# Run a workflow with Podman instead of Docker
|
||||
wrkflw run --runtime podman .github/workflows/ci.yml
|
||||
|
||||
# Run a workflow in emulation mode (without containers)
|
||||
wrkflw run --runtime emulation .github/workflows/ci.yml
|
||||
|
||||
# Run with verbose output
|
||||
wrkflw run --verbose .github/workflows/ci.yml
|
||||
@@ -137,8 +175,11 @@ wrkflw tui path/to/workflows
|
||||
# Open TUI with a specific workflow pre-selected
|
||||
wrkflw tui path/to/workflow.yml
|
||||
|
||||
# Open TUI with Podman runtime
|
||||
wrkflw tui --runtime podman
|
||||
|
||||
# Open TUI in emulation mode
|
||||
wrkflw tui --emulate
|
||||
wrkflw tui --runtime emulation
|
||||
```
|
||||
|
||||
### Triggering Workflows Remotely
|
||||
@@ -162,7 +203,7 @@ The terminal user interface provides an interactive way to manage workflows:
|
||||
- **r**: Run all selected workflows
|
||||
- **a**: Select all workflows
|
||||
- **n**: Deselect all workflows
|
||||
- **e**: Toggle between Docker and Emulation mode
|
||||
- **e**: Cycle through runtime modes (Docker → Podman → Emulation)
|
||||
- **v**: Toggle between Execution and Validation mode
|
||||
- **Esc**: Back / Exit detailed view
|
||||
- **q**: Quit application
|
||||
@@ -225,20 +266,22 @@ $ wrkflw
|
||||
# This will automatically load .github/workflows files into the TUI
|
||||
```
|
||||
|
||||
## Requirements
|
||||
## System Requirements
|
||||
|
||||
- Rust 1.67 or later
|
||||
- Docker (optional, for container-based execution)
|
||||
- When not using Docker, the emulation mode can run workflows using your local system tools
|
||||
- Container Runtime (optional, for container-based execution):
|
||||
- **Docker**: Traditional container runtime
|
||||
- **Podman**: Rootless alternative to Docker
|
||||
- **None**: Emulation mode runs workflows using local system tools
|
||||
|
||||
## How It Works
|
||||
|
||||
WRKFLW parses your GitHub Actions workflow files and executes each job and step in the correct order. For Docker mode, it creates containers that closely match GitHub's runner environments. The workflow execution process:
|
||||
WRKFLW parses your GitHub Actions workflow files and executes each job and step in the correct order. For container modes (Docker/Podman), it creates containers that closely match GitHub's runner environments. The workflow execution process:
|
||||
|
||||
1. **Parsing**: Reads and validates the workflow YAML structure
|
||||
2. **Dependency Resolution**: Creates an execution plan based on job dependencies
|
||||
3. **Environment Setup**: Prepares GitHub-like environment variables and context
|
||||
4. **Execution**: Runs each job and step either in Docker containers or through local emulation
|
||||
4. **Execution**: Runs each job and step either in containers (Docker/Podman) or through local emulation
|
||||
5. **Monitoring**: Tracks progress and captures outputs in the TUI or command line
|
||||
|
||||
## Advanced Features
|
||||
@@ -262,7 +305,7 @@ WRKFLW supports composite actions, which are actions made up of multiple steps.
|
||||
|
||||
### Container Cleanup
|
||||
|
||||
WRKFLW automatically cleans up any Docker containers created during workflow execution, even if the process is interrupted with Ctrl+C.
|
||||
WRKFLW automatically cleans up any containers created during workflow execution (Docker/Podman), even if the process is interrupted with Ctrl+C.
|
||||
|
||||
For debugging failed workflows, you can preserve containers that fail by using the `--preserve-containers-on-failure` flag:
|
||||
|
||||
@@ -277,10 +320,46 @@ wrkflw tui --preserve-containers-on-failure
|
||||
When a container fails with this flag enabled, WRKFLW will:
|
||||
- Keep the failed container running instead of removing it
|
||||
- Log the container ID and provide inspection instructions
|
||||
- Show a message like: `Preserving container abc123 for debugging (exit code: 1). Use 'docker exec -it abc123 bash' to inspect.`
|
||||
- Show a message like: `Preserving container abc123 for debugging (exit code: 1). Use 'docker exec -it abc123 bash' to inspect.` (Docker)
|
||||
- Or: `Preserving container abc123 for debugging (exit code: 1). Use 'podman exec -it abc123 bash' to inspect.` (Podman)
|
||||
|
||||
This allows you to inspect the exact state of the container when the failure occurred, examine files, check environment variables, and debug issues more effectively.
|
||||
|
||||
### Podman-Specific Features
|
||||
|
||||
When using Podman as the container runtime, you get additional benefits:
|
||||
|
||||
**Rootless Operation:**
|
||||
```bash
|
||||
# Run workflows without root privileges
|
||||
wrkflw run --runtime podman .github/workflows/ci.yml
|
||||
```
|
||||
|
||||
**Enhanced Security:**
|
||||
- Daemonless architecture reduces attack surface
|
||||
- User namespaces provide additional isolation
|
||||
- No privileged daemon required
|
||||
|
||||
**Container Inspection:**
|
||||
```bash
|
||||
# List preserved containers
|
||||
podman ps -a --filter "name=wrkflw-"
|
||||
|
||||
# Inspect a preserved container's filesystem (without executing)
|
||||
podman mount <container-id>
|
||||
|
||||
# Or run a new container with the same volumes
|
||||
podman run --rm -it --volumes-from <failed-container> ubuntu:20.04 bash
|
||||
|
||||
# Clean up all wrkflw containers
|
||||
podman ps -a --filter "name=wrkflw-" --format "{{.Names}}" | xargs podman rm -f
|
||||
```
|
||||
|
||||
**Compatibility:**
|
||||
- Drop-in replacement for Docker workflows
|
||||
- Same CLI options and behavior
|
||||
- Identical container execution environment
|
||||
|
||||
## Limitations
|
||||
|
||||
### Supported Features
|
||||
@@ -288,7 +367,7 @@ This allows you to inspect the exact state of the container when the failure occ
|
||||
- ✅ Job dependency resolution and parallel execution (all jobs with correct 'needs' relationships are executed in the right order, and independent jobs run in parallel)
|
||||
- ✅ Matrix builds (supported for reasonable matrix sizes; very large matrices may be slow or resource-intensive)
|
||||
- ✅ Environment variables and GitHub context (all standard GitHub Actions environment variables and context objects are emulated)
|
||||
- ✅ Docker container actions (all actions that use Docker containers are supported in Docker mode)
|
||||
- ✅ Container actions (all actions that use containers are supported in Docker and Podman modes)
|
||||
- ✅ JavaScript actions (all actions that use JavaScript are supported)
|
||||
- ✅ Composite actions (all composite actions, including nested and local composite actions, are supported)
|
||||
- ✅ Local actions (actions referenced with local paths are supported)
|
||||
@@ -303,15 +382,15 @@ This allows you to inspect the exact state of the container when the failure occ
|
||||
|
||||
### Limited or Unsupported Features (Explicit List)
|
||||
- ❌ GitHub secrets and permissions: Only basic environment variables are supported. GitHub's encrypted secrets and fine-grained permissions are NOT available.
|
||||
- ❌ GitHub Actions cache: Caching functionality (e.g., `actions/cache`) is NOT supported in emulation mode and only partially supported in Docker mode (no persistent cache between runs).
|
||||
- ❌ GitHub Actions cache: Caching functionality (e.g., `actions/cache`) is NOT supported in emulation mode and only partially supported in Docker and Podman modes (no persistent cache between runs).
|
||||
- ❌ GitHub API integrations: Only basic workflow triggering is supported. Features like workflow status reporting, artifact upload/download, and API-based job control are NOT available.
|
||||
- ❌ GitHub-specific environment variables: Some advanced or dynamic environment variables (e.g., those set by GitHub runners or by the GitHub API) are emulated with static or best-effort values, but not all are fully functional.
|
||||
- ❌ Large/complex matrix builds: Very large matrices (hundreds or thousands of job combinations) may not be practical due to performance and resource limits.
|
||||
- ❌ Network-isolated actions: Actions that require strict network isolation or custom network configuration may not work out-of-the-box and may require manual Docker configuration.
|
||||
- ❌ Network-isolated actions: Actions that require strict network isolation or custom network configuration may not work out-of-the-box and may require manual container runtime configuration.
|
||||
- ❌ Some event triggers: Only `workflow_dispatch` (manual trigger) is fully supported. Other triggers (e.g., `push`, `pull_request`, `schedule`, `release`, etc.) are NOT supported.
|
||||
- ❌ GitHub runner-specific features: Features that depend on the exact GitHub-hosted runner environment (e.g., pre-installed tools, runner labels, or hardware) are NOT guaranteed to match. Only a best-effort emulation is provided.
|
||||
- ❌ Windows and macOS runners: Only Linux-based runners are fully supported. Windows and macOS jobs are NOT supported.
|
||||
- ❌ Service containers: Service containers (e.g., databases defined in `services:`) are only supported in Docker mode. In emulation mode, they are NOT supported.
|
||||
- ❌ Service containers: Service containers (e.g., databases defined in `services:`) are only supported in Docker and Podman modes. In emulation mode, they are NOT supported.
|
||||
- ❌ Artifacts: Uploading and downloading artifacts between jobs/steps is NOT supported.
|
||||
- ❌ Job/step timeouts: Custom timeouts for jobs and steps are NOT enforced.
|
||||
- ❌ Job/step concurrency and cancellation: Features like `concurrency` and job cancellation are NOT supported.
|
||||
@@ -319,6 +398,7 @@ This allows you to inspect the exact state of the container when the failure occ
|
||||
|
||||
### Runtime Mode Differences
|
||||
- **Docker Mode**: Provides the closest match to GitHub's environment, including support for Docker container actions, service containers, and Linux-based jobs. Some advanced container configurations may still require manual setup.
|
||||
- **Podman Mode**: Similar to Docker mode but uses Podman for container execution. Offers rootless container support and enhanced security. Fully compatible with Docker-based workflows.
|
||||
- **Emulation Mode**: Runs workflows using the local system tools. Limitations:
|
||||
- Only supports local and JavaScript actions (no Docker container actions)
|
||||
- No support for service containers
|
||||
@@ -373,7 +453,7 @@ The following roadmap outlines our planned approach to implementing currently un
|
||||
### 6. Network-Isolated Actions
|
||||
- **Goal:** Support custom network configurations and strict isolation for actions.
|
||||
- **Plan:**
|
||||
- Add advanced Docker network configuration options.
|
||||
- Add advanced container network configuration options for Docker and Podman.
|
||||
- Document best practices for network isolation.
|
||||
|
||||
### 7. Event Triggers
|
||||
|
||||
487
TESTING_PODMAN.md
Normal file
487
TESTING_PODMAN.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# Testing Podman Support in WRKFLW
|
||||
|
||||
This document provides comprehensive testing steps to verify that Podman support is working correctly in wrkflw.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Install Podman
|
||||
|
||||
Choose the installation method for your operating system:
|
||||
|
||||
#### macOS (using Homebrew)
|
||||
```bash
|
||||
brew install podman
|
||||
```
|
||||
|
||||
#### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install podman
|
||||
```
|
||||
|
||||
#### RHEL/CentOS/Fedora
|
||||
```bash
|
||||
# Fedora
|
||||
sudo dnf install podman
|
||||
|
||||
# RHEL/CentOS 8+
|
||||
sudo dnf install podman
|
||||
```
|
||||
|
||||
#### Windows
|
||||
```bash
|
||||
# Using Chocolatey
|
||||
choco install podman-desktop
|
||||
|
||||
# Or download from https://podman.io/getting-started/installation
|
||||
```
|
||||
|
||||
### 2. Initialize Podman (macOS/Windows only)
|
||||
```bash
|
||||
podman machine init
|
||||
podman machine start
|
||||
```
|
||||
|
||||
### 3. Verify Podman Installation
|
||||
```bash
|
||||
podman version
|
||||
podman info
|
||||
```
|
||||
|
||||
Expected output should show Podman version and system information without errors.
|
||||
|
||||
### 4. Build WRKFLW with Podman Support
|
||||
```bash
|
||||
cd /path/to/wrkflw
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Test 1: CLI Runtime Selection
|
||||
|
||||
#### 1.1 Test Default Runtime (Docker)
|
||||
```bash
|
||||
# Should default to Docker
|
||||
./target/release/wrkflw run --help | grep -A 5 "runtime"
|
||||
```
|
||||
Expected: Should show `--runtime` option with default value `docker`.
|
||||
|
||||
#### 1.2 Test Podman Runtime Selection
|
||||
```bash
|
||||
# Should accept podman as runtime
|
||||
./target/release/wrkflw run --runtime podman test-workflows/example.yml
|
||||
```
|
||||
Expected: Should run without CLI argument errors.
|
||||
|
||||
#### 1.3 Test Emulation Runtime Selection
|
||||
```bash
|
||||
# Should accept emulation as runtime
|
||||
./target/release/wrkflw run --runtime emulation test-workflows/example.yml
|
||||
```
|
||||
Expected: Should run without CLI argument errors.
|
||||
|
||||
#### 1.4 Test Invalid Runtime Selection
|
||||
```bash
|
||||
# Should reject invalid runtime
|
||||
./target/release/wrkflw run --runtime invalid test-workflows/example.yml
|
||||
```
|
||||
Expected: Should show error about invalid runtime choice.
|
||||
|
||||
### Test 2: Podman Availability Detection
|
||||
|
||||
#### 2.1 Test with Podman Available
|
||||
```bash
|
||||
# Ensure Podman is running
|
||||
podman info > /dev/null && echo "Podman is available"
|
||||
|
||||
# Test wrkflw detection
|
||||
./target/release/wrkflw run --runtime podman --verbose test-workflows/example.yml
|
||||
```
|
||||
Expected: Should show "Podman is available, using Podman runtime" in logs.
|
||||
|
||||
#### 2.2 Test with Podman Unavailable
|
||||
```bash
|
||||
# Temporarily make podman unavailable
|
||||
sudo mv /usr/local/bin/podman /usr/local/bin/podman.bak 2>/dev/null || echo "Podman not in /usr/local/bin"
|
||||
|
||||
# Test fallback to emulation
|
||||
./target/release/wrkflw run --runtime podman --verbose test-workflows/example.yml
|
||||
|
||||
# Restore podman
|
||||
sudo mv /usr/local/bin/podman.bak /usr/local/bin/podman 2>/dev/null || echo "Nothing to restore"
|
||||
```
|
||||
Expected: Should show "Podman is not available. Using emulation mode instead."
|
||||
|
||||
### Test 3: Container Execution with Podman
|
||||
|
||||
#### 3.1 Create a Simple Test Workflow
|
||||
Create `test-podman-workflow.yml`:
|
||||
|
||||
```yaml
|
||||
name: Test Podman Workflow
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
test-podman:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Test basic commands
|
||||
run: |
|
||||
echo "Testing Podman container execution"
|
||||
whoami
|
||||
pwd
|
||||
ls -la
|
||||
echo "Container test completed successfully"
|
||||
|
||||
- name: Test environment variables
|
||||
env:
|
||||
TEST_VAR: "podman-test"
|
||||
run: |
|
||||
echo "Testing environment variables"
|
||||
echo "TEST_VAR: $TEST_VAR"
|
||||
echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
|
||||
echo "RUNNER_OS: $RUNNER_OS"
|
||||
|
||||
- name: Test volume mounting
|
||||
run: |
|
||||
echo "Testing volume mounting"
|
||||
echo "test-file-content" > test-file.txt
|
||||
cat test-file.txt
|
||||
ls -la test-file.txt
|
||||
```
|
||||
|
||||
#### 3.2 Test Podman Container Execution
|
||||
```bash
|
||||
./target/release/wrkflw run --runtime podman --verbose test-podman-workflow.yml
|
||||
```
|
||||
Expected: Should execute all steps successfully using Podman containers.
|
||||
|
||||
#### 3.3 Compare with Docker Execution
|
||||
```bash
|
||||
# Test same workflow with Docker
|
||||
./target/release/wrkflw run --runtime docker --verbose test-podman-workflow.yml
|
||||
|
||||
# Test same workflow with emulation
|
||||
./target/release/wrkflw run --runtime emulation --verbose test-podman-workflow.yml
|
||||
```
|
||||
Expected: All three runtimes should produce similar results (emulation may have limitations).
|
||||
|
||||
### Test 4: TUI Interface Testing
|
||||
|
||||
#### 4.1 Test TUI Runtime Selection
|
||||
```bash
|
||||
./target/release/wrkflw tui test-workflows/
|
||||
```
|
||||
|
||||
**Test Steps:**
|
||||
1. Launch TUI
|
||||
2. Press `e` key to cycle through runtimes
|
||||
3. Verify status bar shows: Docker → Podman → Emulation → Docker
|
||||
4. Check that Podman status shows "Connected" or "Not Available"
|
||||
5. Select a workflow and run it with Podman runtime
|
||||
|
||||
#### 4.2 Test TUI with Specific Runtime
|
||||
```bash
|
||||
# Start TUI with Podman runtime
|
||||
./target/release/wrkflw tui --runtime podman test-workflows/
|
||||
|
||||
# Start TUI with emulation runtime
|
||||
./target/release/wrkflw tui --runtime emulation test-workflows/
|
||||
```
|
||||
Expected: TUI should start with the specified runtime active.
|
||||
|
||||
### Test 5: Container Preservation Testing
|
||||
|
||||
✅ **Note**: Container preservation is fully supported with Podman and works correctly.
|
||||
|
||||
#### 5.1 Test Container Cleanup (Default)
|
||||
```bash
|
||||
# Run a workflow that will fail
|
||||
echo 'name: Failing Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
fail:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- run: exit 1' > test-fail-workflow.yml
|
||||
|
||||
./target/release/wrkflw run --runtime podman test-fail-workflow.yml
|
||||
|
||||
# Check if containers were cleaned up
|
||||
podman ps -a --filter "name=wrkflw-"
|
||||
```
|
||||
Expected: No wrkflw containers should remain.
|
||||
|
||||
#### 5.2 Test Container Preservation on Failure
|
||||
```bash
|
||||
./target/release/wrkflw run --runtime podman --preserve-containers-on-failure test-fail-workflow.yml
|
||||
|
||||
# Check if failed container was preserved
|
||||
podman ps -a --filter "name=wrkflw-"
|
||||
```
|
||||
Expected: Should show preserved container. Note the container ID for inspection.
|
||||
|
||||
#### 5.3 Test Container Inspection
|
||||
```bash
|
||||
# Get container ID from previous step
|
||||
CONTAINER_ID=$(podman ps -a --filter "name=wrkflw-" --format "{{.ID}}" | head -1)
|
||||
|
||||
# Inspect the preserved container
|
||||
podman exec -it $CONTAINER_ID bash
|
||||
# Inside container: explore the environment, check files, etc.
|
||||
# Exit with: exit
|
||||
|
||||
# Clean up manually
|
||||
podman rm $CONTAINER_ID
|
||||
```
|
||||
|
||||
### Test 6: Image Operations Testing
|
||||
|
||||
#### 6.1 Test Image Pulling
|
||||
```bash
|
||||
# Create workflow that uses a specific image
|
||||
echo 'name: Image Pull Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:18-alpine
|
||||
steps:
|
||||
- run: node --version' > test-image-pull.yml
|
||||
|
||||
./target/release/wrkflw run --runtime podman --verbose test-image-pull.yml
|
||||
```
|
||||
Expected: Should pull node:18-alpine image and execute successfully.
|
||||
|
||||
#### 6.2 Test Custom Image Building
|
||||
```bash
|
||||
# Create a workflow that builds a custom image (if supported)
|
||||
# This tests the build_image functionality
|
||||
mkdir -p test-build
|
||||
echo 'FROM ubuntu:20.04
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
CMD ["echo", "Custom image test"]' > test-build/Dockerfile
|
||||
|
||||
echo 'name: Image Build Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build and test custom image
|
||||
run: |
|
||||
echo "Testing custom image scenarios"
|
||||
curl --version' > test-custom-image.yml
|
||||
|
||||
# Note: This test depends on language environment preparation
|
||||
./target/release/wrkflw run --runtime podman --verbose test-custom-image.yml
|
||||
```
|
||||
|
||||
### Test 7: Error Handling and Edge Cases
|
||||
|
||||
#### 7.1 Test Invalid Container Image
|
||||
```bash
|
||||
echo 'name: Invalid Image Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: nonexistent-image:invalid-tag
|
||||
steps:
|
||||
- run: echo "This should fail"' > test-invalid-image.yml
|
||||
|
||||
./target/release/wrkflw run --runtime podman test-invalid-image.yml
|
||||
```
|
||||
Expected: Should handle image pull failure gracefully with clear error message.
|
||||
|
||||
#### 7.2 Test Network Connectivity
|
||||
```bash
|
||||
echo 'name: Network Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Test network access
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
curl -s https://httpbin.org/get
|
||||
- name: Test DNS resolution
|
||||
run: nslookup google.com' > test-network.yml
|
||||
|
||||
./target/release/wrkflw run --runtime podman --verbose test-network.yml
|
||||
```
|
||||
Expected: Should have network access and complete successfully.
|
||||
|
||||
#### 7.3 Test Resource Intensive Workflow
|
||||
```bash
|
||||
echo 'name: Resource Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Memory test
|
||||
run: |
|
||||
echo "Testing memory usage"
|
||||
free -h
|
||||
dd if=/dev/zero of=/tmp/test bs=1M count=100
|
||||
ls -lh /tmp/test
|
||||
rm /tmp/test
|
||||
- name: CPU test
|
||||
run: |
|
||||
echo "Testing CPU usage"
|
||||
yes > /dev/null &
|
||||
PID=$!
|
||||
sleep 2
|
||||
kill $PID
|
||||
echo "CPU test completed"' > test-resources.yml
|
||||
|
||||
./target/release/wrkflw run --runtime podman --verbose test-resources.yml
|
||||
```
|
||||
|
||||
### Test 8: Comparison Testing
|
||||
|
||||
#### 8.1 Create Comprehensive Test Workflow
|
||||
```bash
|
||||
echo 'name: Comprehensive Runtime Comparison
|
||||
on: [workflow_dispatch]
|
||||
|
||||
env:
|
||||
GLOBAL_VAR: "global-value"
|
||||
|
||||
jobs:
|
||||
test-all-features:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
env:
|
||||
JOB_VAR: "job-value"
|
||||
steps:
|
||||
- name: Environment test
|
||||
env:
|
||||
STEP_VAR: "step-value"
|
||||
run: |
|
||||
echo "=== Environment Variables ==="
|
||||
echo "GLOBAL_VAR: $GLOBAL_VAR"
|
||||
echo "JOB_VAR: $JOB_VAR"
|
||||
echo "STEP_VAR: $STEP_VAR"
|
||||
echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
|
||||
echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY"
|
||||
echo "RUNNER_OS: $RUNNER_OS"
|
||||
|
||||
- name: File system test
|
||||
run: |
|
||||
echo "=== File System Test ==="
|
||||
pwd
|
||||
ls -la
|
||||
whoami
|
||||
id
|
||||
df -h
|
||||
|
||||
- name: Network test
|
||||
run: |
|
||||
echo "=== Network Test ==="
|
||||
apt-get update -q
|
||||
apt-get install -y curl iputils-ping
|
||||
ping -c 3 8.8.8.8
|
||||
curl -s https://httpbin.org/ip
|
||||
|
||||
- name: Process test
|
||||
run: |
|
||||
echo "=== Process Test ==="
|
||||
ps aux
|
||||
top -b -n 1 | head -10
|
||||
|
||||
- name: Package installation test
|
||||
run: |
|
||||
echo "=== Package Test ==="
|
||||
apt-get install -y python3 python3-pip
|
||||
python3 --version
|
||||
pip3 --version' > comprehensive-test.yml
|
||||
```
|
||||
|
||||
#### 8.2 Run Comprehensive Test with All Runtimes
|
||||
```bash
|
||||
echo "Testing with Docker:"
|
||||
./target/release/wrkflw run --runtime docker --verbose comprehensive-test.yml > docker-test.log 2>&1
|
||||
|
||||
echo "Testing with Podman:"
|
||||
./target/release/wrkflw run --runtime podman --verbose comprehensive-test.yml > podman-test.log 2>&1
|
||||
|
||||
echo "Testing with Emulation:"
|
||||
./target/release/wrkflw run --runtime emulation --verbose comprehensive-test.yml > emulation-test.log 2>&1
|
||||
|
||||
# Compare results
|
||||
echo "=== Comparing Results ==="
|
||||
echo "Docker exit code: $?"
|
||||
echo "Podman exit code: $?"
|
||||
echo "Emulation exit code: $?"
|
||||
|
||||
# Optional: Compare log outputs
|
||||
diff docker-test.log podman-test.log | head -20
|
||||
```
|
||||
|
||||
## Expected Results Summary
|
||||
|
||||
### ✅ **Should Work:**
|
||||
- CLI accepts `--runtime podman` without errors
|
||||
- TUI cycles through Docker → Podman → Emulation with 'e' key
|
||||
- Status bar shows Podman availability correctly
|
||||
- Container execution works identically to Docker
|
||||
- Container cleanup respects preservation settings
|
||||
- Image pulling and basic image operations work
|
||||
- Environment variables are passed correctly
|
||||
- Volume mounting works for workspace access
|
||||
- Network connectivity is available in containers
|
||||
- Error handling is graceful and informative
|
||||
|
||||
### ⚠️ **Limitations to Expect:**
|
||||
- Some advanced Docker-specific features may not work identically
|
||||
- Performance characteristics may differ from Docker
|
||||
- Podman-specific configuration might be needed for complex scenarios
|
||||
- Error messages may differ between Docker and Podman
|
||||
|
||||
### 🚨 **Should Fail Gracefully:**
|
||||
- Invalid runtime selection should show clear error
|
||||
- Missing Podman should fall back to emulation with warning
|
||||
- Invalid container images should show helpful error messages
|
||||
- Network issues should be reported clearly
|
||||
|
||||
## Cleanup
|
||||
|
||||
After testing, clean up test files:
|
||||
```bash
|
||||
rm -f test-podman-workflow.yml test-fail-workflow.yml test-image-pull.yml
|
||||
rm -f test-custom-image.yml test-invalid-image.yml test-network.yml
|
||||
rm -f test-resources.yml comprehensive-test.yml
|
||||
rm -f docker-test.log podman-test.log emulation-test.log
|
||||
rm -rf test-build/
|
||||
podman system prune -f # Clean up unused containers and images
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"Podman not available"**
|
||||
- Verify Podman installation: `podman version`
|
||||
- Check Podman service: `podman machine list` (macOS/Windows)
|
||||
|
||||
2. **Permission errors**
|
||||
- Podman should work rootless by default
|
||||
- Check user namespaces: `podman unshare cat /proc/self/uid_map`
|
||||
|
||||
3. **Network issues**
|
||||
- Test basic connectivity: `podman run --rm ubuntu:20.04 ping -c 1 8.8.8.8`
|
||||
|
||||
4. **Container startup failures**
|
||||
- Check Podman logs: `podman logs <container-id>`
|
||||
- Verify image availability: `podman images`
|
||||
|
||||
This comprehensive testing plan should verify that Podman support is working correctly and help identify any issues that need to be addressed.
|
||||
@@ -12,6 +12,7 @@ use thiserror::Error;
|
||||
use crate::dependency;
|
||||
use crate::docker;
|
||||
use crate::environment;
|
||||
use crate::podman;
|
||||
use logging;
|
||||
use matrix::MatrixCombination;
|
||||
use models::gitlab::Pipeline;
|
||||
@@ -95,10 +96,10 @@ async fn execute_github_workflow(
|
||||
// Add runtime mode to environment
|
||||
env_context.insert(
|
||||
"WRKFLW_RUNTIME_MODE".to_string(),
|
||||
if config.runtime_type == RuntimeType::Emulation {
|
||||
"emulation".to_string()
|
||||
} else {
|
||||
"docker".to_string()
|
||||
match config.runtime_type {
|
||||
RuntimeType::Emulation => "emulation".to_string(),
|
||||
RuntimeType::Docker => "docker".to_string(),
|
||||
RuntimeType::Podman => "podman".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -195,10 +196,10 @@ async fn execute_gitlab_pipeline(
|
||||
// Add runtime mode to environment
|
||||
env_context.insert(
|
||||
"WRKFLW_RUNTIME_MODE".to_string(),
|
||||
if config.runtime_type == RuntimeType::Emulation {
|
||||
"emulation".to_string()
|
||||
} else {
|
||||
"docker".to_string()
|
||||
match config.runtime_type {
|
||||
RuntimeType::Emulation => "emulation".to_string(),
|
||||
RuntimeType::Docker => "docker".to_string(),
|
||||
RuntimeType::Podman => "podman".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -356,7 +357,7 @@ fn resolve_gitlab_dependencies(
|
||||
Ok(execution_plan)
|
||||
}
|
||||
|
||||
// Determine if Docker is available or fall back to emulation
|
||||
// Determine if Docker/Podman is available or fall back to emulation
|
||||
fn initialize_runtime(
|
||||
runtime_type: RuntimeType,
|
||||
preserve_containers_on_failure: bool,
|
||||
@@ -380,6 +381,24 @@ fn initialize_runtime(
|
||||
Ok(Box::new(emulation::EmulationRuntime::new()))
|
||||
}
|
||||
}
|
||||
RuntimeType::Podman => {
|
||||
if podman::is_available() {
|
||||
// Handle the Result returned by PodmanRuntime::new()
|
||||
match podman::PodmanRuntime::new_with_config(preserve_containers_on_failure) {
|
||||
Ok(podman_runtime) => Ok(Box::new(podman_runtime)),
|
||||
Err(e) => {
|
||||
logging::error(&format!(
|
||||
"Failed to initialize Podman runtime: {}, falling back to emulation mode",
|
||||
e
|
||||
));
|
||||
Ok(Box::new(emulation::EmulationRuntime::new()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging::error("Podman not available, falling back to emulation mode");
|
||||
Ok(Box::new(emulation::EmulationRuntime::new()))
|
||||
}
|
||||
}
|
||||
RuntimeType::Emulation => Ok(Box::new(emulation::EmulationRuntime::new())),
|
||||
}
|
||||
}
|
||||
@@ -387,6 +406,7 @@ fn initialize_runtime(
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RuntimeType {
|
||||
Docker,
|
||||
Podman,
|
||||
Emulation,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod dependency;
|
||||
pub mod docker;
|
||||
pub mod engine;
|
||||
pub mod environment;
|
||||
pub mod podman;
|
||||
pub mod substitution;
|
||||
|
||||
// Re-export public items
|
||||
|
||||
867
crates/executor/src/podman.rs
Normal file
867
crates/executor/src/podman.rs
Normal file
@@ -0,0 +1,867 @@
|
||||
use async_trait::async_trait;
|
||||
use logging;
|
||||
use once_cell::sync::Lazy;
|
||||
use runtime::container::{ContainerError, ContainerOutput, ContainerRuntime};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Mutex;
|
||||
use tempfile;
|
||||
use tokio::process::Command;
|
||||
use utils;
|
||||
use utils::fd;
|
||||
|
||||
static RUNNING_CONTAINERS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
// Map to track customized images for a job
|
||||
#[allow(dead_code)]
|
||||
static CUSTOMIZED_IMAGES: Lazy<Mutex<HashMap<String, String>>> =
|
||||
Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub struct PodmanRuntime {
|
||||
preserve_containers_on_failure: bool,
|
||||
}
|
||||
|
||||
impl PodmanRuntime {
|
||||
pub fn new() -> Result<Self, ContainerError> {
|
||||
Self::new_with_config(false)
|
||||
}
|
||||
|
||||
pub fn new_with_config(preserve_containers_on_failure: bool) -> Result<Self, ContainerError> {
|
||||
// Check if podman command is available
|
||||
if !is_available() {
|
||||
return Err(ContainerError::ContainerStart(
|
||||
"Podman is not available on this system".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(PodmanRuntime {
|
||||
preserve_containers_on_failure,
|
||||
})
|
||||
}
|
||||
|
||||
// Add a method to store and retrieve customized images (e.g., with Python installed)
|
||||
#[allow(dead_code)]
|
||||
pub fn get_customized_image(base_image: &str, customization: &str) -> Option<String> {
|
||||
let key = format!("{}:{}", base_image, customization);
|
||||
match CUSTOMIZED_IMAGES.lock() {
|
||||
Ok(images) => images.get(&key).cloned(),
|
||||
Err(e) => {
|
||||
logging::error(&format!("Failed to acquire lock: {}", e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_customized_image(base_image: &str, customization: &str, new_image: &str) {
|
||||
let key = format!("{}:{}", base_image, customization);
|
||||
if let Err(e) = CUSTOMIZED_IMAGES.lock().map(|mut images| {
|
||||
images.insert(key, new_image.to_string());
|
||||
}) {
|
||||
logging::error(&format!("Failed to acquire lock: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a customized image key by prefix
|
||||
#[allow(dead_code)]
|
||||
pub fn find_customized_image_key(image: &str, prefix: &str) -> Option<String> {
|
||||
let image_keys = match CUSTOMIZED_IMAGES.lock() {
|
||||
Ok(keys) => keys,
|
||||
Err(e) => {
|
||||
logging::error(&format!("Failed to acquire lock: {}", e));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Look for any key that starts with the prefix
|
||||
for (key, _) in image_keys.iter() {
|
||||
if key.starts_with(prefix) {
|
||||
return Some(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get a customized image with language-specific dependencies
|
||||
pub fn get_language_specific_image(
|
||||
base_image: &str,
|
||||
language: &str,
|
||||
version: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let key = match (language, version) {
|
||||
("python", Some(ver)) => format!("python:{}", ver),
|
||||
("node", Some(ver)) => format!("node:{}", ver),
|
||||
("java", Some(ver)) => format!("eclipse-temurin:{}", ver),
|
||||
("go", Some(ver)) => format!("golang:{}", ver),
|
||||
("dotnet", Some(ver)) => format!("mcr.microsoft.com/dotnet/sdk:{}", ver),
|
||||
("rust", Some(ver)) => format!("rust:{}", ver),
|
||||
(lang, Some(ver)) => format!("{}:{}", lang, ver),
|
||||
(lang, None) => lang.to_string(),
|
||||
};
|
||||
|
||||
match CUSTOMIZED_IMAGES.lock() {
|
||||
Ok(images) => images.get(&key).cloned(),
|
||||
Err(e) => {
|
||||
logging::error(&format!("Failed to acquire lock: {}", e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a customized image with language-specific dependencies
|
||||
pub fn set_language_specific_image(
|
||||
base_image: &str,
|
||||
language: &str,
|
||||
version: Option<&str>,
|
||||
new_image: &str,
|
||||
) {
|
||||
let key = match (language, version) {
|
||||
("python", Some(ver)) => format!("python:{}", ver),
|
||||
("node", Some(ver)) => format!("node:{}", ver),
|
||||
("java", Some(ver)) => format!("eclipse-temurin:{}", ver),
|
||||
("go", Some(ver)) => format!("golang:{}", ver),
|
||||
("dotnet", Some(ver)) => format!("mcr.microsoft.com/dotnet/sdk:{}", ver),
|
||||
("rust", Some(ver)) => format!("rust:{}", ver),
|
||||
(lang, Some(ver)) => format!("{}:{}", lang, ver),
|
||||
(lang, None) => lang.to_string(),
|
||||
};
|
||||
|
||||
if let Err(e) = CUSTOMIZED_IMAGES.lock().map(|mut images| {
|
||||
images.insert(key, new_image.to_string());
|
||||
}) {
|
||||
logging::error(&format!("Failed to acquire lock: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a podman command with proper error handling and timeout
|
||||
async fn execute_podman_command(
|
||||
&self,
|
||||
args: &[&str],
|
||||
input: Option<&str>,
|
||||
) -> Result<ContainerOutput, ContainerError> {
|
||||
let timeout_duration = std::time::Duration::from_secs(360); // 6 minutes timeout
|
||||
|
||||
let result = tokio::time::timeout(timeout_duration, async {
|
||||
let mut cmd = Command::new("podman");
|
||||
cmd.args(args);
|
||||
|
||||
if input.is_some() {
|
||||
cmd.stdin(Stdio::piped());
|
||||
}
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
logging::debug(&format!(
|
||||
"Running Podman command: podman {}",
|
||||
args.join(" ")
|
||||
));
|
||||
|
||||
let mut child = cmd.spawn().map_err(|e| {
|
||||
ContainerError::ContainerStart(format!("Failed to spawn podman command: {}", e))
|
||||
})?;
|
||||
|
||||
// Send input if provided
|
||||
if let Some(input_data) = input {
|
||||
if let Some(stdin) = child.stdin.take() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut stdin = stdin;
|
||||
stdin.write_all(input_data.as_bytes()).await.map_err(|e| {
|
||||
ContainerError::ContainerExecution(format!(
|
||||
"Failed to write to stdin: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
stdin.shutdown().await.map_err(|e| {
|
||||
ContainerError::ContainerExecution(format!("Failed to close stdin: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().await.map_err(|e| {
|
||||
ContainerError::ContainerExecution(format!("Podman command failed: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(ContainerOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
exit_code: output.status.code().unwrap_or(-1),
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(output) => output,
|
||||
Err(_) => {
|
||||
logging::error("Podman operation timed out after 360 seconds");
|
||||
Err(ContainerError::ContainerExecution(
|
||||
"Operation timed out".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_available() -> bool {
|
||||
// Use a very short timeout for the entire availability check
|
||||
let overall_timeout = std::time::Duration::from_secs(3);
|
||||
|
||||
// Spawn a thread with the timeout to prevent blocking the main thread
|
||||
let handle = std::thread::spawn(move || {
|
||||
// Use safe FD redirection utility to suppress Podman error messages
|
||||
match fd::with_stderr_to_null(|| {
|
||||
// First, check if podman CLI is available as a quick test
|
||||
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
|
||||
// Try a simple podman version command with a short timeout
|
||||
let process = std::process::Command::new("podman")
|
||||
.arg("version")
|
||||
.arg("--format")
|
||||
.arg("{{.Version}}")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
|
||||
match process {
|
||||
Ok(mut child) => {
|
||||
// Set a very short timeout for the process
|
||||
let status = std::thread::scope(|_| {
|
||||
// Try to wait for a short time
|
||||
for _ in 0..10 {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => return status.success(),
|
||||
Ok(None) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100))
|
||||
}
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
// Kill it if it takes too long
|
||||
let _ = child.kill();
|
||||
false
|
||||
});
|
||||
|
||||
if !status {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging::debug("Podman CLI is not available");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to run a simple podman command to check if the daemon is responsive
|
||||
let runtime = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
logging::error(&format!(
|
||||
"Failed to create runtime for Podman availability check: {}",
|
||||
e
|
||||
));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
runtime.block_on(async {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
let mut cmd = Command::new("podman");
|
||||
cmd.args(["info", "--format", "{{.Host.Hostname}}"]);
|
||||
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(1), cmd.output())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(output)) => {
|
||||
if output.status.success() {
|
||||
true
|
||||
} else {
|
||||
logging::debug("Podman info command failed");
|
||||
false
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging::debug(&format!("Podman info command error: {}", e));
|
||||
false
|
||||
}
|
||||
Err(_) => {
|
||||
logging::debug("Podman info command timed out after 1 second");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::debug("Podman availability check timed out");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::debug("Failed to redirect stderr when checking Podman availability");
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Manual implementation of join with timeout
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
while start.elapsed() < overall_timeout {
|
||||
if handle.is_finished() {
|
||||
return match handle.join() {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::warning("Podman availability check thread panicked");
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
|
||||
logging::warning("Podman availability check timed out, assuming Podman is not available");
|
||||
false
|
||||
}
|
||||
|
||||
// Add container to tracking
|
||||
pub fn track_container(id: &str) {
|
||||
if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
|
||||
containers.push(id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove container from tracking
|
||||
pub fn untrack_container(id: &str) {
|
||||
if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
|
||||
containers.retain(|c| c != id);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up all tracked resources
|
||||
pub async fn cleanup_resources() {
|
||||
// Use a global timeout for the entire cleanup process
|
||||
let cleanup_timeout = std::time::Duration::from_secs(5);
|
||||
|
||||
match tokio::time::timeout(cleanup_timeout, cleanup_containers()).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
logging::error(&format!("Error during container cleanup: {}", e));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging::warning("Podman cleanup timed out, some resources may not have been removed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up all tracked containers
|
||||
pub async fn cleanup_containers() -> Result<(), String> {
|
||||
// Getting the containers to clean up should not take a long time
|
||||
let containers_to_cleanup =
|
||||
match tokio::time::timeout(std::time::Duration::from_millis(500), async {
|
||||
match RUNNING_CONTAINERS.try_lock() {
|
||||
Ok(containers) => containers.clone(),
|
||||
Err(_) => {
|
||||
logging::error("Could not acquire container lock for cleanup");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(containers) => containers,
|
||||
Err(_) => {
|
||||
logging::error("Timeout while trying to get containers for cleanup");
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
if containers_to_cleanup.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging::info(&format!(
|
||||
"Cleaning up {} containers",
|
||||
containers_to_cleanup.len()
|
||||
));
|
||||
|
||||
// Process each container with a timeout
|
||||
for container_id in containers_to_cleanup {
|
||||
// First try to stop the container
|
||||
let stop_result = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
Command::new("podman")
|
||||
.args(["stop", &container_id])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match stop_result {
|
||||
Ok(Ok(output)) => {
|
||||
if output.status.success() {
|
||||
logging::debug(&format!("Stopped container: {}", container_id));
|
||||
} else {
|
||||
logging::warning(&format!("Error stopping container {}", container_id));
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging::warning(&format!("Error stopping container {}: {}", container_id, e))
|
||||
}
|
||||
Err(_) => logging::warning(&format!("Timeout stopping container: {}", container_id)),
|
||||
}
|
||||
|
||||
// Then try to remove it
|
||||
let remove_result = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
Command::new("podman")
|
||||
.args(["rm", &container_id])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match remove_result {
|
||||
Ok(Ok(output)) => {
|
||||
if output.status.success() {
|
||||
logging::debug(&format!("Removed container: {}", container_id));
|
||||
} else {
|
||||
logging::warning(&format!("Error removing container {}", container_id));
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging::warning(&format!("Error removing container {}: {}", container_id, e))
|
||||
}
|
||||
Err(_) => logging::warning(&format!("Timeout removing container: {}", container_id)),
|
||||
}
|
||||
|
||||
// Always untrack the container whether or not we succeeded to avoid future cleanup attempts
|
||||
untrack_container(&container_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContainerRuntime for PodmanRuntime {
|
||||
async fn run_container(
|
||||
&self,
|
||||
image: &str,
|
||||
cmd: &[&str],
|
||||
env_vars: &[(&str, &str)],
|
||||
working_dir: &Path,
|
||||
volumes: &[(&Path, &Path)],
|
||||
) -> Result<ContainerOutput, ContainerError> {
|
||||
// Print detailed debugging info
|
||||
logging::info(&format!("Podman: Running container with image: {}", image));
|
||||
|
||||
let timeout_duration = std::time::Duration::from_secs(360); // 6 minutes timeout
|
||||
|
||||
// Run the entire container operation with a timeout
|
||||
match tokio::time::timeout(
|
||||
timeout_duration,
|
||||
self.run_container_inner(image, cmd, env_vars, working_dir, volumes),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::error("Podman operation timed out after 360 seconds");
|
||||
Err(ContainerError::ContainerExecution(
|
||||
"Operation timed out".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
|
||||
// Add a timeout for pull operations
|
||||
let timeout_duration = std::time::Duration::from_secs(30);
|
||||
|
||||
match tokio::time::timeout(timeout_duration, self.pull_image_inner(image)).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::warning(&format!(
|
||||
"Pull of image {} timed out, continuing with existing image",
|
||||
image
|
||||
));
|
||||
// Return success to allow continuing with existing image
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
|
||||
// Add a timeout for build operations
|
||||
let timeout_duration = std::time::Duration::from_secs(120); // 2 minutes timeout for builds
|
||||
|
||||
match tokio::time::timeout(timeout_duration, self.build_image_inner(dockerfile, tag)).await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::error(&format!(
|
||||
"Building image {} timed out after 120 seconds",
|
||||
tag
|
||||
));
|
||||
Err(ContainerError::ImageBuild(
|
||||
"Operation timed out".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_language_environment(
|
||||
&self,
|
||||
language: &str,
|
||||
version: Option<&str>,
|
||||
additional_packages: Option<Vec<String>>,
|
||||
) -> Result<String, ContainerError> {
|
||||
// Check if we already have a customized image for this language and version
|
||||
let key = format!("{}-{}", language, version.unwrap_or("latest"));
|
||||
if let Some(customized_image) = Self::get_language_specific_image("", language, version) {
|
||||
return Ok(customized_image);
|
||||
}
|
||||
|
||||
// Create a temporary Dockerfile for customization
|
||||
let temp_dir = tempfile::tempdir().map_err(|e| {
|
||||
ContainerError::ContainerStart(format!("Failed to create temp directory: {}", e))
|
||||
})?;
|
||||
|
||||
let dockerfile_path = temp_dir.path().join("Dockerfile");
|
||||
let mut dockerfile_content = String::new();
|
||||
|
||||
// Add language-specific setup based on the language
|
||||
match language {
|
||||
"python" => {
|
||||
let base_image =
|
||||
version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v));
|
||||
dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
|
||||
dockerfile_content.push_str(
|
||||
"RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
|
||||
);
|
||||
dockerfile_content.push_str(" build-essential \\\n");
|
||||
dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
|
||||
|
||||
if let Some(packages) = additional_packages {
|
||||
for package in packages {
|
||||
dockerfile_content.push_str(&format!("RUN pip install {}\n", package));
|
||||
}
|
||||
}
|
||||
}
|
||||
"node" => {
|
||||
let base_image =
|
||||
version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v));
|
||||
dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
|
||||
dockerfile_content.push_str(
|
||||
"RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
|
||||
);
|
||||
dockerfile_content.push_str(" build-essential \\\n");
|
||||
dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
|
||||
|
||||
if let Some(packages) = additional_packages {
|
||||
for package in packages {
|
||||
dockerfile_content.push_str(&format!("RUN npm install -g {}\n", package));
|
||||
}
|
||||
}
|
||||
}
|
||||
"java" => {
|
||||
let base_image = version.map_or("eclipse-temurin:17-jdk".to_string(), |v| {
|
||||
format!("eclipse-temurin:{}", v)
|
||||
});
|
||||
dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
|
||||
dockerfile_content.push_str(
|
||||
"RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
|
||||
);
|
||||
dockerfile_content.push_str(" maven \\\n");
|
||||
dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
|
||||
}
|
||||
"go" => {
|
||||
let base_image =
|
||||
version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v));
|
||||
dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
|
||||
dockerfile_content.push_str(
|
||||
"RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
|
||||
);
|
||||
dockerfile_content.push_str(" git \\\n");
|
||||
dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
|
||||
|
||||
if let Some(packages) = additional_packages {
|
||||
for package in packages {
|
||||
dockerfile_content.push_str(&format!("RUN go install {}\n", package));
|
||||
}
|
||||
}
|
||||
}
|
||||
"dotnet" => {
|
||||
let base_image = version
|
||||
.map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| {
|
||||
format!("mcr.microsoft.com/dotnet/sdk:{}", v)
|
||||
});
|
||||
dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
|
||||
|
||||
if let Some(packages) = additional_packages {
|
||||
for package in packages {
|
||||
dockerfile_content
|
||||
.push_str(&format!("RUN dotnet tool install -g {}\n", package));
|
||||
}
|
||||
}
|
||||
}
|
||||
"rust" => {
|
||||
let base_image =
|
||||
version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v));
|
||||
dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
|
||||
dockerfile_content.push_str(
|
||||
"RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
|
||||
);
|
||||
dockerfile_content.push_str(" build-essential \\\n");
|
||||
dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
|
||||
|
||||
if let Some(packages) = additional_packages {
|
||||
for package in packages {
|
||||
dockerfile_content.push_str(&format!("RUN cargo install {}\n", package));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ContainerError::ContainerStart(format!(
|
||||
"Unsupported language: {}",
|
||||
language
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Write the Dockerfile
|
||||
std::fs::write(&dockerfile_path, dockerfile_content).map_err(|e| {
|
||||
ContainerError::ContainerStart(format!("Failed to write Dockerfile: {}", e))
|
||||
})?;
|
||||
|
||||
// Build the customized image
|
||||
let image_tag = format!("wrkflw-{}-{}", language, version.unwrap_or("latest"));
|
||||
self.build_image(&dockerfile_path, &image_tag).await?;
|
||||
|
||||
// Store the customized image
|
||||
Self::set_language_specific_image("", language, version, &image_tag);
|
||||
|
||||
Ok(image_tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of internal methods
|
||||
impl PodmanRuntime {
|
||||
async fn run_container_inner(
|
||||
&self,
|
||||
image: &str,
|
||||
cmd: &[&str],
|
||||
env_vars: &[(&str, &str)],
|
||||
working_dir: &Path,
|
||||
volumes: &[(&Path, &Path)],
|
||||
) -> Result<ContainerOutput, ContainerError> {
|
||||
logging::debug(&format!("Running command in Podman: {:?}", cmd));
|
||||
logging::debug(&format!("Environment: {:?}", env_vars));
|
||||
logging::debug(&format!("Working directory: {}", working_dir.display()));
|
||||
|
||||
// Generate a unique container name
|
||||
let container_name = format!("wrkflw-{}", uuid::Uuid::new_v4());
|
||||
|
||||
// Build the podman run command and store temporary strings
|
||||
let working_dir_str = working_dir.to_string_lossy().to_string();
|
||||
let mut env_strings = Vec::new();
|
||||
let mut volume_strings = Vec::new();
|
||||
|
||||
// Prepare environment variable strings
|
||||
for (key, value) in env_vars {
|
||||
env_strings.push(format!("{}={}", key, value));
|
||||
}
|
||||
|
||||
// Prepare volume mount strings
|
||||
for (host_path, container_path) in volumes {
|
||||
volume_strings.push(format!(
|
||||
"{}:{}",
|
||||
host_path.to_string_lossy(),
|
||||
container_path.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
let mut args = vec!["run", "--name", &container_name, "-w", &working_dir_str];
|
||||
|
||||
// Only use --rm if we don't want to preserve containers on failure
|
||||
// When preserve_containers_on_failure is true, we skip --rm so failed containers remain
|
||||
if !self.preserve_containers_on_failure {
|
||||
args.insert(1, "--rm"); // Insert after "run"
|
||||
}
|
||||
|
||||
// Add environment variables
|
||||
for env_string in &env_strings {
|
||||
args.push("-e");
|
||||
args.push(env_string);
|
||||
}
|
||||
|
||||
// Add volume mounts
|
||||
for volume_string in &volume_strings {
|
||||
args.push("-v");
|
||||
args.push(volume_string);
|
||||
}
|
||||
|
||||
// Add the image
|
||||
args.push(image);
|
||||
|
||||
// Add the command
|
||||
args.extend(cmd);
|
||||
|
||||
// Track the container (even though we use --rm, track it for consistency)
|
||||
track_container(&container_name);
|
||||
|
||||
// Execute the command
|
||||
let result = self.execute_podman_command(&args, None).await;
|
||||
|
||||
// Handle container cleanup based on result and settings
|
||||
match &result {
|
||||
Ok(output) => {
|
||||
if output.exit_code == 0 {
|
||||
// Success - always clean up successful containers
|
||||
if self.preserve_containers_on_failure {
|
||||
// We didn't use --rm, so manually remove successful container
|
||||
let cleanup_result = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
Command::new("podman")
|
||||
.args(["rm", &container_name])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match cleanup_result {
|
||||
Ok(Ok(cleanup_output)) => {
|
||||
if !cleanup_output.status.success() {
|
||||
logging::debug(&format!(
|
||||
"Failed to remove successful container {}",
|
||||
container_name
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => logging::debug(&format!(
|
||||
"Timeout removing successful container {}",
|
||||
container_name
|
||||
)),
|
||||
}
|
||||
}
|
||||
// If not preserving, container was auto-removed with --rm
|
||||
untrack_container(&container_name);
|
||||
} else {
|
||||
// Failed container
|
||||
if self.preserve_containers_on_failure {
|
||||
// Failed and we want to preserve - don't clean up but untrack from auto-cleanup
|
||||
logging::info(&format!(
|
||||
"Preserving failed container {} for debugging (exit code: {}). Use 'podman exec -it {} bash' to inspect.",
|
||||
container_name, output.exit_code, container_name
|
||||
));
|
||||
untrack_container(&container_name);
|
||||
} else {
|
||||
// Failed but we don't want to preserve - container was auto-removed with --rm
|
||||
untrack_container(&container_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Command failed to execute properly - clean up if container exists and not preserving
|
||||
if !self.preserve_containers_on_failure {
|
||||
// Container was created with --rm, so it should be auto-removed
|
||||
untrack_container(&container_name);
|
||||
} else {
|
||||
// Container was created without --rm, try to clean it up since execution failed
|
||||
let cleanup_result = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
Command::new("podman")
|
||||
.args(["rm", "-f", &container_name])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match cleanup_result {
|
||||
Ok(Ok(_)) => logging::debug(&format!(
|
||||
"Cleaned up failed execution container {}",
|
||||
container_name
|
||||
)),
|
||||
_ => logging::debug(&format!(
|
||||
"Failed to clean up execution failure container {}",
|
||||
container_name
|
||||
)),
|
||||
}
|
||||
untrack_container(&container_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &result {
|
||||
Ok(output) => {
|
||||
if output.exit_code != 0 {
|
||||
logging::info(&format!(
|
||||
"Podman command failed with exit code: {}",
|
||||
output.exit_code
|
||||
));
|
||||
logging::debug(&format!("Failed command: {:?}", cmd));
|
||||
logging::debug(&format!("Working directory: {}", working_dir.display()));
|
||||
logging::debug(&format!("STDERR: {}", output.stderr));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging::error(&format!("Podman execution error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn pull_image_inner(&self, image: &str) -> Result<(), ContainerError> {
|
||||
let args = vec!["pull", image];
|
||||
let output = self.execute_podman_command(&args, None).await?;
|
||||
|
||||
if output.exit_code != 0 {
|
||||
return Err(ContainerError::ImagePull(format!(
|
||||
"Failed to pull image {}: {}",
|
||||
image, output.stderr
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_image_inner(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
|
||||
let context_dir = dockerfile.parent().unwrap_or(Path::new("."));
|
||||
let dockerfile_str = dockerfile.to_string_lossy().to_string();
|
||||
let context_dir_str = context_dir.to_string_lossy().to_string();
|
||||
let args = vec!["build", "-f", &dockerfile_str, "-t", tag, &context_dir_str];
|
||||
|
||||
let output = self.execute_podman_command(&args, None).await?;
|
||||
|
||||
if output.exit_code != 0 {
|
||||
return Err(ContainerError::ImageBuild(format!(
|
||||
"Failed to build image {}: {}",
|
||||
tag, output.stderr
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Public accessor functions for testing
|
||||
#[cfg(test)]
|
||||
pub fn get_tracked_containers() -> Vec<String> {
|
||||
if let Ok(containers) = RUNNING_CONTAINERS.lock() {
|
||||
containers.clone()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ impl App {
|
||||
let mut step_table_state = TableState::default();
|
||||
step_table_state.select(Some(0));
|
||||
|
||||
// Check Docker availability if Docker runtime is selected
|
||||
// Check container runtime availability if container runtime is selected
|
||||
let mut initial_logs = Vec::new();
|
||||
let runtime_type = match runtime_type {
|
||||
RuntimeType::Docker => {
|
||||
@@ -113,6 +113,56 @@ impl App {
|
||||
RuntimeType::Docker
|
||||
}
|
||||
}
|
||||
RuntimeType::Podman => {
|
||||
// Use a timeout for the Podman availability check to prevent hanging
|
||||
let is_podman_available = match std::panic::catch_unwind(|| {
|
||||
// Use a very short timeout to prevent blocking the UI
|
||||
let result = std::thread::scope(|s| {
|
||||
let handle = s.spawn(|| {
|
||||
utils::fd::with_stderr_to_null(executor::podman::is_available)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
// Set a short timeout for the thread
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(1);
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
if handle.is_finished() {
|
||||
return handle.join().unwrap_or(false);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// If we reach here, the check took too long
|
||||
logging::warning(
|
||||
"Podman availability check timed out, falling back to emulation mode",
|
||||
);
|
||||
false
|
||||
});
|
||||
result
|
||||
}) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::warning("Podman availability check failed with panic, falling back to emulation mode");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !is_podman_available {
|
||||
initial_logs.push(
|
||||
"Podman is not available or unresponsive. Using emulation mode instead."
|
||||
.to_string(),
|
||||
);
|
||||
logging::warning(
|
||||
"Podman is not available or unresponsive. Using emulation mode instead.",
|
||||
);
|
||||
RuntimeType::Emulation
|
||||
} else {
|
||||
logging::info("Podman is available, using Podman runtime");
|
||||
RuntimeType::Podman
|
||||
}
|
||||
}
|
||||
RuntimeType::Emulation => RuntimeType::Emulation,
|
||||
};
|
||||
|
||||
@@ -159,7 +209,8 @@ impl App {
|
||||
|
||||
pub fn toggle_emulation_mode(&mut self) {
|
||||
self.runtime_type = match self.runtime_type {
|
||||
RuntimeType::Docker => RuntimeType::Emulation,
|
||||
RuntimeType::Docker => RuntimeType::Podman,
|
||||
RuntimeType::Podman => RuntimeType::Emulation,
|
||||
RuntimeType::Emulation => RuntimeType::Docker,
|
||||
};
|
||||
self.logs
|
||||
@@ -182,6 +233,7 @@ impl App {
|
||||
pub fn runtime_type_name(&self) -> &str {
|
||||
match self.runtime_type {
|
||||
RuntimeType::Docker => "Docker",
|
||||
RuntimeType::Podman => "Podman",
|
||||
RuntimeType::Emulation => "Emulation",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ pub async fn execute_workflow_cli(
|
||||
}
|
||||
}
|
||||
|
||||
// Check Docker availability if Docker runtime is selected
|
||||
// Check container runtime availability if container runtime is selected
|
||||
let runtime_type = match runtime_type {
|
||||
RuntimeType::Docker => {
|
||||
if !executor::docker::is_available() {
|
||||
@@ -113,6 +113,15 @@ pub async fn execute_workflow_cli(
|
||||
RuntimeType::Docker
|
||||
}
|
||||
}
|
||||
RuntimeType::Podman => {
|
||||
if !executor::podman::is_available() {
|
||||
println!("⚠️ Podman is not available. Using emulation mode instead.");
|
||||
logging::warning("Podman is not available. Using emulation mode instead.");
|
||||
RuntimeType::Emulation
|
||||
} else {
|
||||
RuntimeType::Podman
|
||||
}
|
||||
}
|
||||
RuntimeType::Emulation => RuntimeType::Emulation,
|
||||
};
|
||||
|
||||
@@ -393,7 +402,7 @@ pub fn start_next_workflow_execution(
|
||||
);
|
||||
}
|
||||
|
||||
// Check Docker availability again if Docker runtime is selected
|
||||
// Check container runtime availability again if container runtime is selected
|
||||
let runtime_type = match app.runtime_type {
|
||||
RuntimeType::Docker => {
|
||||
// Use safe FD redirection to check Docker availability
|
||||
@@ -417,6 +426,28 @@ pub fn start_next_workflow_execution(
|
||||
RuntimeType::Docker
|
||||
}
|
||||
}
|
||||
RuntimeType::Podman => {
|
||||
// Use safe FD redirection to check Podman availability
|
||||
let is_podman_available =
|
||||
match utils::fd::with_stderr_to_null(executor::podman::is_available) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::debug(
|
||||
"Failed to redirect stderr when checking Podman availability.",
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !is_podman_available {
|
||||
app.logs
|
||||
.push("Podman is not available. Using emulation mode instead.".to_string());
|
||||
logging::warning("Podman is not available. Using emulation mode instead.");
|
||||
RuntimeType::Emulation
|
||||
} else {
|
||||
RuntimeType::Podman
|
||||
}
|
||||
}
|
||||
RuntimeType::Emulation => RuntimeType::Emulation,
|
||||
};
|
||||
|
||||
|
||||
@@ -40,38 +40,75 @@ pub fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App,
|
||||
Style::default()
|
||||
.bg(match app.runtime_type {
|
||||
RuntimeType::Docker => Color::Blue,
|
||||
RuntimeType::Podman => Color::Cyan,
|
||||
RuntimeType::Emulation => Color::Magenta,
|
||||
})
|
||||
.fg(Color::White),
|
||||
));
|
||||
|
||||
// Add Docker status if relevant
|
||||
if app.runtime_type == RuntimeType::Docker {
|
||||
// Check Docker silently using safe FD redirection
|
||||
let is_docker_available =
|
||||
match utils::fd::with_stderr_to_null(executor::docker::is_available) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::debug("Failed to redirect stderr when checking Docker availability.");
|
||||
false
|
||||
}
|
||||
};
|
||||
// Add container runtime status if relevant
|
||||
match app.runtime_type {
|
||||
RuntimeType::Docker => {
|
||||
// Check Docker silently using safe FD redirection
|
||||
let is_docker_available =
|
||||
match utils::fd::with_stderr_to_null(executor::docker::is_available) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::debug(
|
||||
"Failed to redirect stderr when checking Docker availability.",
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
status_items.push(Span::raw(" "));
|
||||
status_items.push(Span::styled(
|
||||
if is_docker_available {
|
||||
" Docker: Connected "
|
||||
} else {
|
||||
" Docker: Not Available "
|
||||
},
|
||||
Style::default()
|
||||
.bg(if is_docker_available {
|
||||
Color::Green
|
||||
status_items.push(Span::raw(" "));
|
||||
status_items.push(Span::styled(
|
||||
if is_docker_available {
|
||||
" Docker: Connected "
|
||||
} else {
|
||||
Color::Red
|
||||
})
|
||||
.fg(Color::White),
|
||||
));
|
||||
" Docker: Not Available "
|
||||
},
|
||||
Style::default()
|
||||
.bg(if is_docker_available {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
})
|
||||
.fg(Color::White),
|
||||
));
|
||||
}
|
||||
RuntimeType::Podman => {
|
||||
// Check Podman silently using safe FD redirection
|
||||
let is_podman_available =
|
||||
match utils::fd::with_stderr_to_null(executor::podman::is_available) {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
logging::debug(
|
||||
"Failed to redirect stderr when checking Podman availability.",
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
status_items.push(Span::raw(" "));
|
||||
status_items.push(Span::styled(
|
||||
if is_podman_available {
|
||||
" Podman: Connected "
|
||||
} else {
|
||||
" Podman: Not Available "
|
||||
},
|
||||
Style::default()
|
||||
.bg(if is_podman_available {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
})
|
||||
.fg(Color::White),
|
||||
));
|
||||
}
|
||||
RuntimeType::Emulation => {
|
||||
// No need to check anything for emulation mode
|
||||
}
|
||||
}
|
||||
|
||||
// Add validation/execution mode
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
use bollard::Docker;
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
enum RuntimeChoice {
|
||||
/// Use Docker containers for isolation
|
||||
Docker,
|
||||
/// Use Podman containers for isolation
|
||||
Podman,
|
||||
/// Use process emulation mode (no containers)
|
||||
Emulation,
|
||||
}
|
||||
|
||||
impl From<RuntimeChoice> for executor::RuntimeType {
|
||||
fn from(choice: RuntimeChoice) -> Self {
|
||||
match choice {
|
||||
RuntimeChoice::Docker => executor::RuntimeType::Docker,
|
||||
RuntimeChoice::Podman => executor::RuntimeType::Podman,
|
||||
RuntimeChoice::Emulation => executor::RuntimeType::Emulation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "wrkflw",
|
||||
about = "GitHub & GitLab CI/CD validator and executor",
|
||||
version,
|
||||
long_about = "A CI/CD validator and executor that runs workflows locally.\n\nExamples:\n wrkflw validate # Validate all workflows in .github/workflows\n wrkflw run .github/workflows/build.yml # Run a specific workflow\n wrkflw run .gitlab-ci.yml # Run a GitLab CI pipeline\n wrkflw --verbose run .github/workflows/build.yml # Run with more output\n wrkflw --debug run .github/workflows/build.yml # Run with detailed debug information\n wrkflw run --emulate .github/workflows/build.yml # Use emulation mode instead of Docker\n wrkflw run --preserve-containers-on-failure .github/workflows/build.yml # Keep failed containers for debugging"
|
||||
long_about = "A CI/CD validator and executor that runs workflows locally.\n\nExamples:\n wrkflw validate # Validate all workflows in .github/workflows\n wrkflw run .github/workflows/build.yml # Run a specific workflow\n wrkflw run .gitlab-ci.yml # Run a GitLab CI pipeline\n wrkflw --verbose run .github/workflows/build.yml # Run with more output\n wrkflw --debug run .github/workflows/build.yml # Run with detailed debug information\n wrkflw run --runtime emulation .github/workflows/build.yml # Use emulation mode instead of containers\n wrkflw run --runtime podman .github/workflows/build.yml # Use Podman instead of Docker\n wrkflw run --preserve-containers-on-failure .github/workflows/build.yml # Keep failed containers for debugging"
|
||||
)]
|
||||
struct Wrkflw {
|
||||
#[command(subcommand)]
|
||||
@@ -49,9 +69,9 @@ enum Commands {
|
||||
/// Path to workflow/pipeline file to execute
|
||||
path: PathBuf,
|
||||
|
||||
/// Use emulation mode instead of Docker
|
||||
#[arg(short, long)]
|
||||
emulate: bool,
|
||||
/// Container runtime to use (docker, podman, emulation)
|
||||
#[arg(short, long, value_enum, default_value = "docker")]
|
||||
runtime: RuntimeChoice,
|
||||
|
||||
/// Show 'Would execute GitHub action' messages in emulation mode
|
||||
#[arg(long, default_value_t = false)]
|
||||
@@ -71,9 +91,9 @@ enum Commands {
|
||||
/// Path to workflow file or directory (defaults to .github/workflows)
|
||||
path: Option<PathBuf>,
|
||||
|
||||
/// Use emulation mode instead of Docker
|
||||
#[arg(short, long)]
|
||||
emulate: bool,
|
||||
/// Container runtime to use (docker, podman, emulation)
|
||||
#[arg(short, long, value_enum, default_value = "docker")]
|
||||
runtime: RuntimeChoice,
|
||||
|
||||
/// Show 'Would execute GitHub action' messages in emulation mode
|
||||
#[arg(long, default_value_t = false)]
|
||||
@@ -334,18 +354,14 @@ async fn main() {
|
||||
}
|
||||
Some(Commands::Run {
|
||||
path,
|
||||
emulate,
|
||||
runtime,
|
||||
show_action_messages: _,
|
||||
preserve_containers_on_failure,
|
||||
gitlab,
|
||||
}) => {
|
||||
// Create execution configuration
|
||||
let config = executor::ExecutionConfig {
|
||||
runtime_type: if *emulate {
|
||||
executor::RuntimeType::Emulation
|
||||
} else {
|
||||
executor::RuntimeType::Docker
|
||||
},
|
||||
runtime_type: runtime.clone().into(),
|
||||
verbose,
|
||||
preserve_containers_on_failure: *preserve_containers_on_failure,
|
||||
};
|
||||
@@ -473,16 +489,12 @@ async fn main() {
|
||||
}
|
||||
Some(Commands::Tui {
|
||||
path,
|
||||
emulate,
|
||||
runtime,
|
||||
show_action_messages: _,
|
||||
preserve_containers_on_failure,
|
||||
}) => {
|
||||
// Set runtime type based on the emulate flag
|
||||
let runtime_type = if *emulate {
|
||||
executor::RuntimeType::Emulation
|
||||
} else {
|
||||
executor::RuntimeType::Docker
|
||||
};
|
||||
// Set runtime type based on the runtime choice
|
||||
let runtime_type = runtime.clone().into();
|
||||
|
||||
// Call the TUI implementation from the ui crate
|
||||
if let Err(e) = ui::run_wrkflw_tui(
|
||||
|
||||
215
test-podman-basic.sh
Executable file
215
test-podman-basic.sh
Executable file
@@ -0,0 +1,215 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Basic Podman Support Test Script for WRKFLW
|
||||
# This script performs quick verification of Podman integration
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "🚀 WRKFLW Podman Support - Basic Test Script"
|
||||
echo "============================================="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if wrkflw binary exists
|
||||
print_status "Checking if wrkflw is built..."
|
||||
if [ ! -f "./target/release/wrkflw" ]; then
|
||||
print_warning "Release binary not found. Building wrkflw..."
|
||||
cargo build --release
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Build completed successfully"
|
||||
else
|
||||
print_error "Build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_success "Found wrkflw binary"
|
||||
fi
|
||||
|
||||
# Test 1: Check CLI help shows runtime options
|
||||
print_status "Test 1: Checking CLI runtime options..."
|
||||
HELP_OUTPUT=$(./target/release/wrkflw run --help 2>&1)
|
||||
if echo "$HELP_OUTPUT" | grep -q "runtime.*podman"; then
|
||||
print_success "CLI shows Podman runtime option"
|
||||
else
|
||||
print_error "CLI does not show Podman runtime option"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Check invalid runtime rejection
|
||||
print_status "Test 2: Testing invalid runtime rejection..."
|
||||
if ./target/release/wrkflw run --runtime invalid test-workflows/example.yml 2>&1 | grep -q "invalid value"; then
|
||||
print_success "Invalid runtime properly rejected"
|
||||
else
|
||||
print_error "Invalid runtime not properly rejected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Check Podman availability detection
|
||||
print_status "Test 3: Testing Podman availability detection..."
|
||||
if command -v podman &> /dev/null; then
|
||||
print_success "Podman is installed and available"
|
||||
PODMAN_VERSION=$(podman version --format json | python3 -c "import sys, json; print(json.load(sys.stdin)['Client']['Version'])" 2>/dev/null || echo "unknown")
|
||||
print_status "Podman version: $PODMAN_VERSION"
|
||||
|
||||
# Test basic podman functionality
|
||||
if podman info > /dev/null 2>&1; then
|
||||
print_success "Podman daemon is responsive"
|
||||
PODMAN_AVAILABLE=true
|
||||
else
|
||||
print_warning "Podman installed but not responsive (may need podman machine start)"
|
||||
PODMAN_AVAILABLE=false
|
||||
fi
|
||||
else
|
||||
print_warning "Podman not installed - will test fallback behavior"
|
||||
PODMAN_AVAILABLE=false
|
||||
fi
|
||||
|
||||
# Create a simple test workflow
|
||||
print_status "Creating test workflow..."
|
||||
cat > test-basic-workflow.yml << 'EOF'
|
||||
name: Basic Test Workflow
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Basic test
|
||||
run: |
|
||||
echo "Testing basic container execution"
|
||||
echo "Current user: $(whoami)"
|
||||
echo "Working directory: $(pwd)"
|
||||
echo "Container test completed"
|
||||
|
||||
- name: Environment test
|
||||
env:
|
||||
TEST_VAR: "test-value"
|
||||
run: |
|
||||
echo "Environment variable TEST_VAR: $TEST_VAR"
|
||||
echo "GitHub workspace: $GITHUB_WORKSPACE"
|
||||
EOF
|
||||
|
||||
# Test 4: Test emulation mode (should always work)
|
||||
print_status "Test 4: Testing emulation mode..."
|
||||
if ./target/release/wrkflw run --runtime emulation test-basic-workflow.yml > /dev/null 2>&1; then
|
||||
print_success "Emulation mode works correctly"
|
||||
else
|
||||
print_error "Emulation mode failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: Test Podman mode
|
||||
print_status "Test 5: Testing Podman mode..."
|
||||
if [ "$PODMAN_AVAILABLE" = true ]; then
|
||||
print_status "Running test workflow with Podman runtime..."
|
||||
if ./target/release/wrkflw run --runtime podman --verbose test-basic-workflow.yml > podman-test.log 2>&1; then
|
||||
print_success "Podman mode executed successfully"
|
||||
|
||||
# Check if it actually used Podman
|
||||
if grep -q "Podman: Running container" podman-test.log; then
|
||||
print_success "Confirmed Podman was used for container execution"
|
||||
elif grep -q "Podman is not available.*emulation" podman-test.log; then
|
||||
print_warning "Podman fell back to emulation mode"
|
||||
else
|
||||
print_warning "Could not confirm Podman usage in logs"
|
||||
fi
|
||||
else
|
||||
print_error "Podman mode failed to execute"
|
||||
echo "Error log:"
|
||||
tail -10 podman-test.log
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_status "Testing Podman fallback behavior..."
|
||||
if ./target/release/wrkflw run --runtime podman test-basic-workflow.yml 2>&1 | grep -q "emulation.*instead"; then
|
||||
print_success "Podman correctly falls back to emulation when unavailable"
|
||||
else
|
||||
print_error "Podman fallback behavior not working correctly"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 6: Test Docker mode (if available)
|
||||
print_status "Test 6: Testing Docker mode for comparison..."
|
||||
if command -v docker &> /dev/null && docker info > /dev/null 2>&1; then
|
||||
print_status "Docker is available, testing for comparison..."
|
||||
if ./target/release/wrkflw run --runtime docker test-basic-workflow.yml > /dev/null 2>&1; then
|
||||
print_success "Docker mode works correctly"
|
||||
else
|
||||
print_warning "Docker mode failed (this is okay for Podman testing)"
|
||||
fi
|
||||
else
|
||||
print_warning "Docker not available - skipping Docker comparison test"
|
||||
fi
|
||||
|
||||
# Test 7: Test TUI compilation (basic check)
|
||||
print_status "Test 7: Testing TUI startup..."
|
||||
timeout 5s ./target/release/wrkflw tui --help > /dev/null 2>&1 || true
|
||||
print_success "TUI help command works"
|
||||
|
||||
# Test 8: Runtime switching in TUI (simulate)
|
||||
print_status "Test 8: Checking TUI runtime parameter..."
|
||||
if ./target/release/wrkflw tui --runtime podman --help > /dev/null 2>&1; then
|
||||
print_success "TUI accepts runtime parameter"
|
||||
else
|
||||
print_error "TUI does not accept runtime parameter"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
print_status "Cleaning up test files..."
|
||||
rm -f test-basic-workflow.yml podman-test.log
|
||||
|
||||
echo ""
|
||||
echo "🎉 Basic Podman Support Test Summary:"
|
||||
echo "======================================"
|
||||
|
||||
if [ "$PODMAN_AVAILABLE" = true ]; then
|
||||
print_success "✅ Podman is available and working"
|
||||
print_success "✅ WRKFLW can execute workflows with Podman"
|
||||
else
|
||||
print_warning "⚠️ Podman not available, but fallback works correctly"
|
||||
fi
|
||||
|
||||
print_success "✅ CLI runtime selection works"
|
||||
print_success "✅ Error handling works"
|
||||
print_success "✅ TUI integration works"
|
||||
print_success "✅ Basic container execution works"
|
||||
|
||||
echo ""
|
||||
print_status "🔍 For comprehensive testing, run: ./TESTING_PODMAN.md"
|
||||
print_status "📋 To install Podman: https://podman.io/getting-started/installation"
|
||||
|
||||
if [ "$PODMAN_AVAILABLE" = false ]; then
|
||||
echo ""
|
||||
print_warning "💡 To test full Podman functionality:"
|
||||
echo " 1. Install Podman for your system"
|
||||
echo " 2. Initialize Podman (if on macOS/Windows): podman machine init && podman machine start"
|
||||
echo " 3. Re-run this test script"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_success "🎯 Basic Podman support test completed successfully!"
|
||||
256
test-preserve-containers.sh
Executable file
256
test-preserve-containers.sh
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script to verify --preserve-containers-on-failure works with Podman
|
||||
|
||||
set -e
|
||||
|
||||
echo "🧪 Testing --preserve-containers-on-failure with Podman"
|
||||
echo "======================================================="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Check if Podman is available
|
||||
if ! command -v podman &> /dev/null; then
|
||||
print_error "Podman is not installed. Please install Podman to run this test."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! podman info > /dev/null 2>&1; then
|
||||
print_error "Podman is not responsive. Please start Podman (e.g., 'podman machine start' on macOS)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Podman is available and responsive"
|
||||
|
||||
# Create a failing workflow for testing
|
||||
print_status "Creating test workflows..."
|
||||
|
||||
cat > test-success-workflow.yml << 'EOF'
|
||||
name: Success Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
success:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Successful step
|
||||
run: |
|
||||
echo "This step will succeed"
|
||||
echo "Exit code will be 0"
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
cat > test-failure-workflow.yml << 'EOF'
|
||||
name: Failure Test
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
failure:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:20.04
|
||||
steps:
|
||||
- name: Failing step
|
||||
run: exit 1
|
||||
EOF
|
||||
|
||||
# Function to count wrkflw containers
|
||||
count_wrkflw_containers() {
|
||||
podman ps -a --filter "name=wrkflw-" --format "{{.Names}}" | wc -l
|
||||
}
|
||||
|
||||
# Function to get wrkflw container names
|
||||
get_wrkflw_containers() {
|
||||
podman ps -a --filter "name=wrkflw-" --format "{{.Names}}"
|
||||
}
|
||||
|
||||
# Clean up any existing wrkflw containers
|
||||
print_status "Cleaning up any existing wrkflw containers..."
|
||||
EXISTING_CONTAINERS=$(get_wrkflw_containers)
|
||||
if [ -n "$EXISTING_CONTAINERS" ]; then
|
||||
echo "$EXISTING_CONTAINERS" | xargs -r podman rm -f
|
||||
print_status "Removed existing containers"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "=== Test 1: Success case without preserve flag ==="
|
||||
BEFORE_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers before: $BEFORE_COUNT"
|
||||
|
||||
./target/release/wrkflw run --runtime podman test-success-workflow.yml > /dev/null 2>&1
|
||||
|
||||
AFTER_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers after: $AFTER_COUNT"
|
||||
|
||||
if [ "$AFTER_COUNT" -eq "$BEFORE_COUNT" ]; then
|
||||
print_success "✅ Success case without preserve: containers cleaned up correctly"
|
||||
else
|
||||
print_error "❌ Success case without preserve: containers not cleaned up"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "=== Test 2: Success case with preserve flag ==="
|
||||
BEFORE_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers before: $BEFORE_COUNT"
|
||||
|
||||
./target/release/wrkflw run --runtime podman --preserve-containers-on-failure test-success-workflow.yml > /dev/null 2>&1
|
||||
|
||||
AFTER_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers after: $AFTER_COUNT"
|
||||
|
||||
if [ "$AFTER_COUNT" -eq "$BEFORE_COUNT" ]; then
|
||||
print_success "✅ Success case with preserve: successful containers cleaned up correctly"
|
||||
else
|
||||
print_error "❌ Success case with preserve: successful containers not cleaned up"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "=== Test 3: Failure case without preserve flag ==="
|
||||
BEFORE_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers before: $BEFORE_COUNT"
|
||||
|
||||
./target/release/wrkflw run --runtime podman test-failure-workflow.yml > /dev/null 2>&1 || true
|
||||
|
||||
AFTER_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers after: $AFTER_COUNT"
|
||||
|
||||
if [ "$AFTER_COUNT" -eq "$BEFORE_COUNT" ]; then
|
||||
print_success "✅ Failure case without preserve: containers cleaned up correctly"
|
||||
else
|
||||
print_error "❌ Failure case without preserve: containers not cleaned up"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "=== Test 4: Failure case with preserve flag ==="
|
||||
BEFORE_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers before: $BEFORE_COUNT"
|
||||
|
||||
print_status "Running failing workflow with --preserve-containers-on-failure..."
|
||||
./target/release/wrkflw run --runtime podman --preserve-containers-on-failure test-failure-workflow.yml > preserve-test.log 2>&1 || true
|
||||
|
||||
AFTER_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers after: $AFTER_COUNT"
|
||||
PRESERVED_CONTAINERS=$(get_wrkflw_containers)
|
||||
|
||||
if [ "$AFTER_COUNT" -gt "$BEFORE_COUNT" ]; then
|
||||
print_success "✅ Failure case with preserve: failed container preserved"
|
||||
print_status "Preserved containers: $PRESERVED_CONTAINERS"
|
||||
|
||||
# Check if the log mentions preservation
|
||||
if grep -q "Preserving.*container.*debugging" preserve-test.log; then
|
||||
print_success "✅ Preservation message found in logs"
|
||||
else
|
||||
print_warning "⚠️ Preservation message not found in logs"
|
||||
fi
|
||||
|
||||
# Test that we can inspect the preserved container
|
||||
CONTAINER_NAME=$(echo "$PRESERVED_CONTAINERS" | head -1)
|
||||
if [ -n "$CONTAINER_NAME" ]; then
|
||||
print_status "Testing container inspection..."
|
||||
if podman exec "$CONTAINER_NAME" echo "Container inspection works" > /dev/null 2>&1; then
|
||||
print_success "✅ Can inspect preserved container"
|
||||
else
|
||||
print_warning "⚠️ Cannot inspect preserved container (container may have exited)"
|
||||
fi
|
||||
|
||||
# Clean up the preserved container
|
||||
print_status "Cleaning up preserved container for testing..."
|
||||
podman rm -f "$CONTAINER_NAME" > /dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
print_error "❌ Failure case with preserve: failed container not preserved"
|
||||
echo "Log output:"
|
||||
cat preserve-test.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "=== Test 5: Multiple failures with preserve flag ==="
|
||||
BEFORE_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers before: $BEFORE_COUNT"
|
||||
|
||||
print_status "Running multiple failing workflows..."
|
||||
for i in {1..3}; do
|
||||
./target/release/wrkflw run --runtime podman --preserve-containers-on-failure test-failure-workflow.yml > /dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
AFTER_COUNT=$(count_wrkflw_containers)
|
||||
print_status "Containers after: $AFTER_COUNT"
|
||||
EXPECTED_COUNT=$((BEFORE_COUNT + 3))
|
||||
|
||||
if [ "$AFTER_COUNT" -eq "$EXPECTED_COUNT" ]; then
|
||||
print_success "✅ Multiple failures: all failed containers preserved"
|
||||
else
|
||||
print_warning "⚠️ Multiple failures: expected $EXPECTED_COUNT containers, got $AFTER_COUNT"
|
||||
fi
|
||||
|
||||
# Clean up all preserved containers
|
||||
PRESERVED_CONTAINERS=$(get_wrkflw_containers)
|
||||
if [ -n "$PRESERVED_CONTAINERS" ]; then
|
||||
print_status "Cleaning up all preserved containers..."
|
||||
echo "$PRESERVED_CONTAINERS" | xargs -r podman rm -f
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_status "=== Test 6: Comparison with Docker (if available) ==="
|
||||
if command -v docker &> /dev/null && docker info > /dev/null 2>&1; then
|
||||
print_status "Docker available, testing for comparison..."
|
||||
|
||||
# Test Docker with preserve flag
|
||||
BEFORE_COUNT=$(docker ps -a --filter "name=wrkflw-" --format "{{.Names}}" | wc -l)
|
||||
./target/release/wrkflw run --runtime docker --preserve-containers-on-failure test-failure-workflow.yml > /dev/null 2>&1 || true
|
||||
AFTER_COUNT=$(docker ps -a --filter "name=wrkflw-" --format "{{.Names}}" | wc -l)
|
||||
|
||||
if [ "$AFTER_COUNT" -gt "$BEFORE_COUNT" ]; then
|
||||
print_success "✅ Docker also preserves containers correctly"
|
||||
# Clean up Docker containers
|
||||
DOCKER_CONTAINERS=$(docker ps -a --filter "name=wrkflw-" --format "{{.Names}}")
|
||||
if [ -n "$DOCKER_CONTAINERS" ]; then
|
||||
echo "$DOCKER_CONTAINERS" | xargs -r docker rm -f
|
||||
fi
|
||||
else
|
||||
print_warning "⚠️ Docker preserve behavior differs from Podman"
|
||||
fi
|
||||
else
|
||||
print_status "Docker not available, skipping comparison"
|
||||
fi
|
||||
|
||||
# Cleanup test files
|
||||
print_status "Cleaning up test files..."
|
||||
rm -f test-success-workflow.yml test-failure-workflow.yml preserve-test.log
|
||||
|
||||
echo ""
|
||||
print_success "🎉 Container preservation test completed successfully!"
|
||||
echo ""
|
||||
print_status "📋 Test Summary:"
|
||||
print_success "✅ Successful containers are cleaned up (with and without preserve flag)"
|
||||
print_success "✅ Failed containers are cleaned up when preserve flag is NOT used"
|
||||
print_success "✅ Failed containers are preserved when preserve flag IS used"
|
||||
print_success "✅ Preserved containers can be inspected"
|
||||
print_success "✅ Multiple failed containers are handled correctly"
|
||||
|
||||
echo ""
|
||||
print_status "💡 Usage examples:"
|
||||
echo " # Normal execution (cleanup all containers):"
|
||||
echo " wrkflw run --runtime podman workflow.yml"
|
||||
echo ""
|
||||
echo " # Preserve failed containers for debugging:"
|
||||
echo " wrkflw run --runtime podman --preserve-containers-on-failure workflow.yml"
|
||||
echo ""
|
||||
echo " # Inspect preserved container:"
|
||||
echo " podman ps -a --filter \"name=wrkflw-\""
|
||||
echo " podman exec -it <container-name> bash"
|
||||
echo ""
|
||||
echo " # Clean up preserved containers:"
|
||||
echo " podman ps -a --filter \"name=wrkflw-\" --format \"{{.Names}}\" | xargs podman rm -f"
|
||||
Reference in New Issue
Block a user