diff --git a/DOCKER_COMPOSE_EXAMPLES.md b/DOCKER_COMPOSE_EXAMPLES.md new file mode 100644 index 000000000..dbe2b0e01 --- /dev/null +++ b/DOCKER_COMPOSE_EXAMPLES.md @@ -0,0 +1,214 @@ +# Docker Compose Examples for Testing Health Status Aggregation + +These example docker-compose files demonstrate different container health status scenarios to test the "unknown" health state aggregation fix. + +## Prerequisites + +```bash +# Make sure Docker is running +docker --version +``` + +## Test Cases + +### 1. **Healthy** - All containers with passing health checks + +**File:** `docker-compose.healthy.yml` + +```bash +docker-compose -f docker-compose.healthy.yml up -d +docker-compose -f docker-compose.healthy.yml ps +docker inspect $(docker-compose -f docker-compose.healthy.yml ps -q web) | grep -A 5 '"Health"' +``` + +**Expected Status:** `running (healthy)` +- Container has healthcheck that successfully connects to nginx on port 80 + +**Cleanup:** +```bash +docker-compose -f docker-compose.healthy.yml down +``` + +--- + +### 2. **Unknown** - Container without health check + +**File:** `docker-compose.unknown.yml` + +```bash +docker-compose -f docker-compose.unknown.yml up -d +docker-compose -f docker-compose.unknown.yml ps +docker inspect $(docker-compose -f docker-compose.unknown.yml ps -q web) | grep -A 5 '"Health"' +``` + +**Expected Status:** `running (unknown)` +- Container has NO healthcheck defined +- `State.Health` key is missing from Docker inspect output + +**Cleanup:** +```bash +docker-compose -f docker-compose.unknown.yml down +``` + +--- + +### 3. **Unhealthy** - Container with failing health check + +**File:** `docker-compose.unhealthy.yml` + +```bash +docker-compose -f docker-compose.unhealthy.yml up -d +# Wait 30 seconds for health check to fail +sleep 30 +docker-compose -f docker-compose.unhealthy.yml ps +docker inspect $(docker-compose -f docker-compose.unhealthy.yml ps -q web) | grep -A 5 '"Health"' +``` + +**Expected Status:** `running (unhealthy)` +- Container has healthcheck that tries to connect to port 9999 (which doesn't exist) +- Health check will fail after retries + +**Cleanup:** +```bash +docker-compose -f docker-compose.unhealthy.yml down +``` + +--- + +### 4. **Mixed: Healthy + Unknown** → Should show "unknown" + +**File:** `docker-compose.mixed-healthy-unknown.yml` + +```bash +docker-compose -f docker-compose.mixed-healthy-unknown.yml up -d +docker-compose -f docker-compose.mixed-healthy-unknown.yml ps +docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q web) | grep -A 5 '"Health"' +docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q worker) | grep -A 5 '"Health"' +``` + +**Expected Aggregated Status:** `running (unknown)` ← **This is the fix!** +- `web` container: `running (healthy)` - has passing healthcheck +- `worker` container: `running (unknown)` - no healthcheck +- **Before fix:** Would show `running (healthy)` ❌ +- **After fix:** Shows `running (unknown)` ✅ + +**Cleanup:** +```bash +docker-compose -f docker-compose.mixed-healthy-unknown.yml down +``` + +--- + +### 5. **Mixed: Unhealthy + Unknown** → Should show "unhealthy" + +**File:** `docker-compose.mixed-unhealthy-unknown.yml` + +```bash +docker-compose -f docker-compose.mixed-unhealthy-unknown.yml up -d +# Wait 30 seconds for health check to fail +sleep 30 +docker-compose -f docker-compose.mixed-unhealthy-unknown.yml ps +docker inspect $(docker-compose -f docker-compose.mixed-unhealthy-unknown.yml ps -q web) | grep -A 5 '"Health"' +docker inspect $(docker-compose -f docker-compose.mixed-unhealthy-unknown.yml ps -q worker) | grep -A 5 '"Health"' +``` + +**Expected Aggregated Status:** `running (unhealthy)` +- `web` container: `running (unhealthy)` - failing healthcheck +- `worker` container: `running (unknown)` - no healthcheck +- Unhealthy takes priority over unknown + +**Cleanup:** +```bash +docker-compose -f docker-compose.mixed-unhealthy-unknown.yml down +``` + +--- + +### 6. **Excluded Container** - Unhealthy container excluded from health checks + +**File:** `docker-compose.excluded.yml` + +```bash +docker-compose -f docker-compose.excluded.yml up -d +# Wait 30 seconds for health check to fail +sleep 30 +docker-compose -f docker-compose.excluded.yml ps +docker inspect $(docker-compose -f docker-compose.excluded.yml ps -q web) | grep -A 5 '"Health"' +docker inspect $(docker-compose -f docker-compose.excluded.yml ps -q backup) | grep -A 5 '"Health"' +``` + +**Expected Aggregated Status:** `running (healthy)` +- `web` container: `running (healthy)` - passing healthcheck +- `backup` container: `running (unhealthy)` - but has `exclude_from_hc: true` +- Excluded containers don't affect aggregation + +**Cleanup:** +```bash +docker-compose -f docker-compose.excluded.yml down +``` + +--- + +## Quick Test All Cases + +```bash +# Test healthy +echo "=== Testing HEALTHY ===" +docker-compose -f docker-compose.healthy.yml up -d && sleep 15 +docker inspect $(docker-compose -f docker-compose.healthy.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})' +docker-compose -f docker-compose.healthy.yml down + +# Test unknown +echo -e "\n=== Testing UNKNOWN ===" +docker-compose -f docker-compose.unknown.yml up -d && sleep 5 +docker inspect $(docker-compose -f docker-compose.unknown.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})' +docker-compose -f docker-compose.unknown.yml down + +# Test unhealthy +echo -e "\n=== Testing UNHEALTHY ===" +docker-compose -f docker-compose.unhealthy.yml up -d && sleep 35 +docker inspect $(docker-compose -f docker-compose.unhealthy.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})' +docker-compose -f docker-compose.unhealthy.yml down + +# Test mixed healthy + unknown +echo -e "\n=== Testing MIXED HEALTHY + UNKNOWN ===" +docker-compose -f docker-compose.mixed-healthy-unknown.yml up -d && sleep 15 +echo "Web: $(docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q web) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})')" +echo "Worker: $(docker inspect $(docker-compose -f docker-compose.mixed-healthy-unknown.yml ps -q worker) --format='{{.State.Status}} ({{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}})')" +echo "Expected Aggregated: running (unknown)" +docker-compose -f docker-compose.mixed-healthy-unknown.yml down + +# Cleanup all +echo -e "\n=== Cleaning up ===" +docker-compose -f docker-compose.healthy.yml down 2>/dev/null +docker-compose -f docker-compose.unknown.yml down 2>/dev/null +docker-compose -f docker-compose.unhealthy.yml down 2>/dev/null +docker-compose -f docker-compose.mixed-healthy-unknown.yml down 2>/dev/null +docker-compose -f docker-compose.mixed-unhealthy-unknown.yml down 2>/dev/null +docker-compose -f docker-compose.excluded.yml down 2>/dev/null +``` + +## Understanding the Output + +### Docker Inspect Health Status +```json +"Health": { + "Status": "healthy", // or "unhealthy", "starting" + "FailingStreak": 0, + "Log": [...] +} +``` + +If `"Health"` key is missing → Container has no healthcheck → Shows as `unknown` + +### Coolify Status Format +Individual containers: `" ()"` +- `"running (healthy)"` - Container running with passing healthcheck +- `"running (unhealthy)"` - Container running with failing healthcheck +- `"running (unknown)"` - Container running with no healthcheck +- `"running (starting)"` - Container running, healthcheck in initial grace period + +### Aggregation Priority (after fix) +1. **Unhealthy** (highest) - If ANY container is unhealthy +2. **Unknown** (medium) - If no unhealthy, but ≥1 has no healthcheck +3. **Healthy** (lowest) - Only when ALL containers explicitly healthy diff --git a/database/migrations/2025_11_19_115504_create_github_runner_sources_table.php b/database/migrations/2025_11_19_115504_create_github_runner_sources_table.php new file mode 100644 index 000000000..02ae2b77f --- /dev/null +++ b/database/migrations/2025_11_19_115504_create_github_runner_sources_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('github_runner_sources'); + } +}; diff --git a/docker-compose.excluded.yml b/docker-compose.excluded.yml new file mode 100644 index 000000000..d285e86cd --- /dev/null +++ b/docker-compose.excluded.yml @@ -0,0 +1,25 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8085:80" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + + backup: + image: nginx:alpine + exclude_from_hc: true + healthcheck: + # Even though this will fail, it's excluded from health checks + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9999/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + # Should still show "running (healthy)" because backup is excluded diff --git a/docker-compose.healthy.yml b/docker-compose.healthy.yml new file mode 100644 index 000000000..d6928e6d1 --- /dev/null +++ b/docker-compose.healthy.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8080:80" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s diff --git a/docker-compose.mixed-healthy-unknown.yml b/docker-compose.mixed-healthy-unknown.yml new file mode 100644 index 000000000..77da46e09 --- /dev/null +++ b/docker-compose.mixed-healthy-unknown.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8083:80" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + + worker: + image: nginx:alpine + # No healthcheck - will show as "running (unknown)" + # This should make the aggregated status "running (unknown)" diff --git a/docker-compose.mixed-unhealthy-unknown.yml b/docker-compose.mixed-unhealthy-unknown.yml new file mode 100644 index 000000000..3184b3c83 --- /dev/null +++ b/docker-compose.mixed-unhealthy-unknown.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8084:80" + healthcheck: + # This will fail + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9999/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + + worker: + image: nginx:alpine + # No healthcheck - will show as "running (unknown)" + # Since one is unhealthy, aggregated status should be "running (unhealthy)" diff --git a/docker-compose.unhealthy.yml b/docker-compose.unhealthy.yml new file mode 100644 index 000000000..4cb1a2806 --- /dev/null +++ b/docker-compose.unhealthy.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8082:80" + healthcheck: + # This will always fail because port 9999 doesn't exist + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9999/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s diff --git a/docker-compose.unknown.yml b/docker-compose.unknown.yml new file mode 100644 index 000000000..6002b3492 --- /dev/null +++ b/docker-compose.unknown.yml @@ -0,0 +1,8 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8081:80" + # No healthcheck defined - will show as "running (unknown)" diff --git a/resources/views/livewire/project/service/heading.blade.php b/resources/views/livewire/project/service/heading.blade.php index c611231a2..bb1bfb0fc 100644 --- a/resources/views/livewire/project/service/heading.blade.php +++ b/resources/views/livewire/project/service/heading.blade.php @@ -29,7 +29,7 @@ @if ($service->isDeployable)
- @if (str($service->status)->contains('running') || (str($service->status)->startsWith('running:') && !str($service->status)->contains('exited'))) + @if (str($service->status)->contains('running'))