Compare commits

..

12 Commits

Author SHA1 Message Date
reachableceo
1628b1dfea fix(demo): add HOMEPAGE_ALLOWED_HOSTS, harden Playwright tests
- Set HOMEPAGE_ALLOWED_HOSTS=* so Homepage accepts requests from
  localhost, LAN IPs, and Tailscale FQDNs (appropriate for demo)
- Add host validation to docker-compose.yml.template and demo.env.template
- Bootstrap HOMEPAGE_ALLOWED_HOSTS in ensure_env() for existing installs
- Harden Playwright tests: check for "host validation failed" and
  "internal server error" text, verify page titles, use stronger
  content assertions based on actual rendered content
- Pin @playwright/test to exact 1.52.0 (no caret) to prevent npm
  resolving to a version incompatible with the Docker image
- Gitignore additional Homepage auto-generated files (custom.css/js,
  proxmox.yaml)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 13:31:42 -05:00
reachableceo
b03f4b2ba2 feat(demo): add Playwright browser tests, fix Homepage config mount
- Add Playwright E2E test suite covering all 13 user-facing services
- Fix Homepage HTTP 500 by removing read-only bind mount (:ro) so it
  can create its required logs/ directory
- Pin @playwright/test to exact 1.52.0 to match Docker image browsers
- Add .gitignore entries for auto-generated Homepage files and
  Playwright artifacts
- All 13 Playwright tests passing (Chromium headless)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 11:24:59 -05:00
reachableceo
50206dce6b fix(demo): resolve duplicate deploy key and env var bootstrapping
- Remove duplicate `deploy:` block in atomictracker service that
  caused YAML parse failure on docker compose up
- Fix yamllint errors: wrap long lines in socket proxy label and
  Elasticsearch health check
- Add MAILHOG_SMTP_PORT migration to ensure_env() so older demo.env
  files get the new variable appended automatically
- Verified: full stack deploys, 91/91 tests pass (52 unit + 39 e2e),
  all 16 services healthy, 13/13 smoke ports accessible

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 10:12:32 -05:00
reachableceo
8362e1ce51 docs: synchronize documentation with current implementation
- Root README.md: proper project overview with quick start
- Root AGENTS.md: add MAILHOG_SMTP_PORT, update env config note
- demo/README.md: add MailHog SMTP port (4019) to service table
- demo/scripts/validate-all.sh: fall back to demo.env.template
  when demo.env not present, add MAILHOG_SMTP_PORT to required vars,
  mask variable values in validation output

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:56:24 -05:00
reachableceo
190b0aff3e docs: write root README, finalize PRD.md
Root README.md:
- Replace 2-line stub with proper project overview
- Add quick start, requirements, documentation index, testing section

PRD.md:
- Change status from Draft to Final, version 1.0 to 2.0
- Fix test script name from test-stack.sh to demo-test.sh
- Fix impossible NFRs: deployment <60s to <5min, setup <30s to <2min
  (Elasticsearch alone needs 60s start_period)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:53:01 -05:00
reachableceo
be03c95929 fix(demo): harden deployment scripts, remove duplicate fix-and-ship.sh
demo-stack.sh:
- Add ensure_env() to create demo.env from template if missing
- Add envsubst prerequisite check
- Fix wait_healthy() to use docker inspect instead of fragile
  sed/awk parsing of docker ps output
- Fix smoke_test() to use env vars instead of hardcoded ports
- Remove fix_env() which overwrote TA_HOST with wrong value
- Add MailHog SMTP port to display_summary()
- Add service names to smoke test output

demo-test.sh:
- Fix security compliance test to expect only 1 socket mount
  (proxy only, now that Dockhand uses DOCKER_HOST)
- Add Dockhand proxy routing check
- Fix arithmetic increment operators for set -e compatibility

- Remove scripts/fix-and-ship.sh (was identical copy of demo-stack.sh)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:50:40 -05:00
reachableceo
9f40e16b25 test(demo): rewrite test suite with meaningful assertions
Unit tests (test_env_validation.sh):
- Validate docker-compose.yml.template has all 16 services
- Verify every exposed service has healthcheck, restart policy, labels
- Verify Dockhand routes through socket proxy (not direct mount)
- Verify only docker-socket-proxy mounts /var/run/docker.sock
- Validate demo.env.template has all 28 required variables
- Verify all port values are in 4000-4099 range
- Verify Homepage and Grafana config files exist
- Verify all scripts use strict mode (set -euo pipefail)
- 53 assertions, all passing

Integration tests (test_service_communication.sh):
- Remove || true suppression on test failures
- Add require_stack_running guard with clear error message
- Add test for Dockhand proxy integration (DOCKER_HOST env check)
- Add network isolation test (container count on network)
- Proper pass/fail counting with exit code

Previous unit test was a tautology (id -u == id -u) that could
never fail. Previous integration tests suppressed all failures.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:48:25 -05:00
reachableceo
0c13069304 feat(demo): add Grafana dashboard and populate empty config directories
- Add Grafana Docker Infrastructure Overview dashboard (CPU, memory,
  container count, image count panels querying InfluxDB)
- Move dashboard JSON to config/grafana/dashboards/ for proper
  provisioning by Grafana's file provider
- Add .gitkeep to 10 empty config directories (pihole, drawio, kroki,
  atomictracker, archivebox, tubearchivist, wakapi, mailhog,
  influxdb, atuin) so git tracks the directory structure

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:43:26 -05:00
reachableceo
088a4cba07 feat(demo): add Homepage dashboard configuration files
- services.yaml: all 13 user-facing services organized by category
  with Pi-hole and Grafana widgets for live stats
- widgets.yaml: greeting, datetime, search, and Pi-hole glances widget
- bookmarks.yaml: developer resource links (GitHub, Stack Overflow,
  Docker Hub, Grafana Docs, InfluxDB Docs)
- settings.yaml: layout configuration (row style, column counts),
  Docker provider via socket proxy, and branding

Previously only docker.yaml existed, resulting in a bare-bones
dashboard with no widgets, bookmarks, or layout.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:42:02 -05:00
reachableceo
265d146bd3 fix(demo): route Dockhand through socket proxy, add resource limits
- Route Dockhand Docker access through docker-socket-proxy via
  DOCKER_HOST=tcp://docker-socket-proxy:2375 instead of direct
  socket mount, enforcing the security model documented in AGENTS.md
- Add POST, DELETE, ALLOW_START, ALLOW_STOP, ALLOW_RESTARTS
  permissions to socket proxy for Dockhand container management
- Add deploy.resources.limits.memory to all 16 services
  (128M-1024M depending on service needs)
- Add MailHog SMTP port 4019 mapping (1025 internal) so applications
  can actually send test emails to MailHog
- Remove stale config/portainer/ directory

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:41:08 -05:00
reachableceo
904fc6d727 chore: add .gitignore and env template, untrack generated files
- Add .gitignore excluding generated docker-compose.yml, demo.env,
  editor files, and temporary files
- Remove demo/docker-compose.yml from tracking (generated by envsubst)
- Remove demo/demo.env from tracking (contains per-machine values)
- Add demo/demo.env.template as reference for required configuration
- Remove stale config/portainer/ directory (Portainer not in stack)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 09:35:49 -05:00
reachableceo
6a70131f9c fix(demo): correct docs, env config, and health checks for production readiness
- Fix DrawIO/Kroki health checks from wget to curl (DrawIO has no wget,
  Kroki /health endpoint unreliable with wget)
- Fix script paths in demo/AGENTS.md (./demo-test.sh → ./scripts/demo-test.sh)
- Fix script paths in demo/README.md (./demo-stack.sh → ./scripts/demo-stack.sh)
- Fix all service URLs from 192.168.3.6 to localhost in demo/README.md
- Fix hardcoded variable references to actual port values in demo/README.md
- Fix root AGENTS.md doc paths (docs/ → demo/docs/)
- Reorganize demo.env: group related vars, fix TA_HOST to container DNS,
  fix ES_JAVA_OPTS quoting, move service credentials with their configs
- Add CWD guidance note to troubleshooting guide
- Regenerate docker-compose.yml with corrected TA_HOST

All 16 services healthy, 38/38 tests passing.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 13:28:03 -05:00
34 changed files with 1171 additions and 911 deletions

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Generated files
demo/docker-compose.yml
# Environment with secrets
demo/demo.env
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
.vscode/
.idea/
# Temporary files
*.tmp
*.bak
tmp_template.yml
# Homepage auto-generated files
demo/config/homepage/logs/
demo/config/homepage/kubernetes.yaml
demo/config/homepage/custom.css
demo/config/homepage/custom.js
demo/config/homepage/proxmox.yaml
# Playwright
node_modules/
test-results/
package-lock.json

View File

@@ -139,7 +139,7 @@ docker run --rm -v "$(pwd):/workdir" hadolint/hadolint <path-to-dockerfile>
- ArchiveBox (4013) - Web archiving solution
- Tube Archivist (4014) - YouTube video archiving (requires ta-redis + ta-elasticsearch)
- Wakapi (4015) - Open-source WakaTime alternative (time tracking)
- MailHog (4017) - Web and API based SMTP testing
- MailHog (4017 Web, 4019 SMTP) - Web and API based SMTP testing
- Atuin (4018) - Magical shell history synchronization
5. **Companion Services** (internal only, no host ports)
@@ -147,7 +147,7 @@ docker run --rm -v "$(pwd):/workdir" hadolint/hadolint <path-to-dockerfile>
- ta-elasticsearch - Elasticsearch index for Tube Archivist
### Configuration Management
- **Environment Variables**: All configuration via `demo/demo.env`
- **Environment Variables**: All configuration via `demo/demo.env` (copy from `demo/demo.env.template`)
- **Template-Based**: `docker-compose.yml` generated from `docker-compose.yml.template` using `envsubst`
- **Dynamic User Detection**: UID/GID automatically detected and applied
- **Service Discovery**: Automatic via Homepage labels in docker-compose.yml
@@ -342,6 +342,7 @@ ARCHIVEBOX_PORT=4013
TUBE_ARCHIVIST_PORT=4014
WAKAPI_PORT=4015
MAILHOG_PORT=4017
MAILHOG_SMTP_PORT=4019
ATUIN_PORT=4018
# Demo Credentials (NOT FOR PRODUCTION)
@@ -383,9 +384,9 @@ DEMO_ADMIN_PASSWORD=demo_password
- **demo/AGENTS.md**: Detailed development guidelines and standards
- **demo/PRD.md**: Product Requirements Document
- **demo/README.md**: Demo-specific documentation and quick start
- **docs/service-guides/**: Service-specific guides
- **docs/troubleshooting/**: Detailed troubleshooting procedures
- **docs/api-docs/**: API documentation
- **demo/docs/service-guides/**: Service-specific guides
- **demo/docs/troubleshooting/**: Detailed troubleshooting procedures
- **demo/docs/api-docs/**: API documentation
---

View File

@@ -1,3 +1,56 @@
# TSYSDevStack-SupportStack-LocalWorkstation
# TSYS Developer Support Stack
Off the shelf applications running local to developer workstations
A Docker Compose-based multi-service stack of FOSS applications that run locally on developer workstations to enhance productivity and quality of life.
## What It Does
Deploys 16 services across 4 categories via a single command:
| Category | Services |
|----------|----------|
| **Infrastructure** | Homepage (dashboard), Pi-hole (DNS), Dockhand (Docker management), Docker Socket Proxy |
| **Monitoring** | InfluxDB (time series), Grafana (visualization) |
| **Documentation** | Draw.io (diagramming), Kroki (diagrams as code) |
| **Developer Tools** | Atomic Tracker, ArchiveBox, Tube Archivist, Wakapi, MailHog, Atuin |
## Quick Start
```bash
cd demo
cp demo.env.template demo.env
./scripts/demo-stack.sh deploy
```
Access the dashboard at **http://localhost:4000**
Credentials: `admin` / `demo_password` (demo only)
## Requirements
- Docker Engine + Docker Compose
- 8GB RAM minimum
- 10GB disk space
- Linux (tested on Ubuntu)
## Documentation
| Document | Purpose |
|----------|---------|
| [demo/PRD.md](demo/PRD.md) | Product requirements (the source of truth) |
| [demo/README.md](demo/README.md) | Full deployment and service documentation |
| [demo/AGENTS.md](demo/AGENTS.md) | Development guidelines |
| [AGENTS.md](AGENTS.md) | Quick reference for contributors |
## Testing
```bash
# Unit tests (no Docker required)
bash demo/tests/unit/test_env_validation.sh
# Full test suite (requires running stack)
./demo/scripts/demo-test.sh full
```
## License
See [LICENSE](LICENSE).

View File

@@ -248,11 +248,11 @@ screen -ls
ps aux | grep demo-stack
# Dynamic deployment and testing (use unique session names)
screen -S demo-deploy-$(date +%Y%m%d-%H%M%S) -dm -L -Logfile deploy-$(date +%Y%m%d-%H%M%S).log ./demo-stack.sh deploy
./demo-test.sh full # Comprehensive QA/validation
./demo-test.sh security # Security compliance validation
./demo-test.sh permissions # File ownership validation
./demo-test.sh network # Network isolation validation
screen -S demo-deploy-$(date +%Y%m%d-%H%M%S) -dm -L -Logfile deploy-$(date +%Y%m%d-%H%M%S).log ./scripts/demo-stack.sh deploy
./scripts/demo-test.sh full # Comprehensive QA/validation
./scripts/demo-test.sh security # Security compliance validation
./scripts/demo-test.sh permissions # File ownership validation
./scripts/demo-test.sh network # Network isolation validation
```
### Automated Validation Suite
@@ -338,13 +338,13 @@ screen -ls
ps aux | grep demo-stack
# Start development stack with unique session name
screen -S demo-deploy-$(date +%Y%m%d-%H%M%S) -dm -L -Logfile deploy-$(date +%Y%m%d-%H%M%S).log ./demo-stack.sh deploy
screen -S demo-deploy-$(date +%Y%m%d-%H%M%S) -dm -L -Logfile deploy-$(date +%Y%m%d-%H%M%S).log ./scripts/demo-stack.sh deploy
# Monitor startup
docker compose logs -f
# Validate deployment
./test-stack.sh
./scripts/demo-test.sh full
```
### Demo Preparation

View File

@@ -4,8 +4,8 @@
[![Document ID: PRD-SUPPORT-DEMO-001](https://img.shields.io/badge/ID-PRD--SUPPORT--DEMO--001-blue.svg)](#)
[![Version: 1.0](https://img.shields.io/badge/Version-1.0-green.svg)](#)
[![Status: Draft](https://img.shields.io/badge/Status-Draft-orange.svg)](#)
[![Date: 2025-11-13](https://img.shields.io/badge/Date-2025--11--13-lightgrey.svg)](#)
[![Status: Final](https://img.shields.io/badge/Status-Final-green.svg)](#)
[![Date: 2026-05-01](https://img.shields.io/badge/Date-2026--05--01-lightgrey.svg)](#)
[![Author: TSYS Development Team](https://img.shields.io/badge/Author-TSYS%20Dev%20Team-purple.svg)](#)
**Demo Version - Product Requirements Document**
@@ -445,7 +445,7 @@ graph LR
| Requirement | Description | Success Metric |
|-------------|-------------|----------------|
| **🌐 Browser Access** | Immediate web interface availability | 100% browser compatibility |
| **🚫 No Manual Setup** | Eliminate configuration steps | Setup time < 30 seconds |
| **🚫 No Manual Setup** | Eliminate configuration steps | Setup time < 2 minutes |
| **🔐 Pre-configured Auth** | Default authentication where needed | Login success rate > 95% |
| **💡 Clear Error Messages** | Intuitive troubleshooting guidance | Issue resolution < 2 minutes |
@@ -453,8 +453,8 @@ graph LR
| Requirement | Description | Success Metric |
|-------------|-------------|----------------|
| **⚡ Single Command** | One-command deployment | Deployment time < 60 seconds |
| **🚀 Rapid Initialization** | Fast service startup | All services ready < 60 seconds |
| **⚡ Single Command** | One-command deployment | Deployment time < 5 minutes |
| **🚀 Rapid Initialization** | Fast service startup | All services ready < 5 minutes |
| **🎯 Immediate Features** | No setup delays for functionality | Feature availability = 100% |
| **🔄 Clean Sessions** | Fresh state between demos | Data reset success = 100% |
@@ -539,7 +539,7 @@ graph TD
| Test Type | Description | Tool/Script |
|-----------|-------------|-------------|
| **❤️ Health Validation** | Service health check verification | `test-stack.sh` |
| **❤️ Health Validation** | Service health check verification | `demo-test.sh` |
| **🔌 Port Accessibility** | Port availability and response testing | `test-stack.sh` |
| **🔍 Service Discovery** | Dashboard integration verification | `test-stack.sh` |
| **📊 Resource Monitoring** | Memory and CPU usage validation | `test-stack.sh` |
@@ -754,10 +754,10 @@ gantt
## 📄 Document Information
**Document ID**: PRD-SUPPORT-DEMO-001
**Version**: 1.0
**Date**: 2025-11-13
**Version**: 2.0
**Date**: 2026-05-01
**Author**: TSYS Development Team
**Status**: Draft
**Status**: Final
---

View File

@@ -36,15 +36,15 @@
```bash
# 🎯 Demo deployment with dynamic user detection
./demo-stack.sh deploy
./scripts/demo-stack.sh deploy
# 🔧 Comprehensive testing and validation
./demo-test.sh full
./scripts/demo-test.sh full
```
</div>
🎉 **Access all services via the Homepage dashboard at** **[http://localhost:${HOMEPAGE_PORT}](http://localhost:${HOMEPAGE_PORT})**
🎉 **Access all services via the Homepage dashboard at** **[http://localhost:4000](http://localhost:4000)**
> ⚠️ **Demo Configuration Only** - This stack is designed for demonstration purposes with no data persistence.
@@ -68,8 +68,8 @@ All configuration is managed through `demo.env` and dynamic detection:
| Script | Purpose | Usage |
|---------|---------|--------|
| **demo-stack.sh** | Dynamic deployment with user detection | `./demo-stack.sh [deploy|stop|restart]` |
| **demo-test.sh** | Comprehensive QA and validation | `./demo-test.sh [full|security|permissions]` |
| **demo-stack.sh** | Dynamic deployment with user detection | `./scripts/demo-stack.sh [deploy|stop|restart]` |
| **demo-test.sh** | Comprehensive QA and validation | `./scripts/demo-test.sh [full|security|permissions]` |
| **demo.env** | All environment variables | Source of configuration |
---
@@ -79,35 +79,35 @@ All configuration is managed through `demo.env` and dynamic detection:
### 🛠️ Developer Tools
| Service | Port | Description | 🌐 Access |
|---------|------|-------------|-----------|
| **Homepage** | 4000 | Central dashboard for service discovery | [Open](http://192.168.3.6:4000) |
| **Atomic Tracker** | 4012 | Habit tracking and personal dashboard | [Open](http://192.168.3.6:4012) |
| **Wakapi** | 4015 | Open-source WakaTime alternative for time tracking | [Open](http://192.168.3.6:4015) |
| **MailHog** | 4017 | Web and API based SMTP testing tool | [Open](http://192.168.3.6:4017) |
| **Atuin** | 4018 | Magical shell history synchronization | [Open](http://192.168.3.6:4018) |
| **Homepage** | 4000 | Central dashboard for service discovery | [Open](http://localhost:4000) |
| **Atomic Tracker** | 4012 | Habit tracking and personal dashboard | [Open](http://localhost:4012) |
| **Wakapi** | 4015 | Open-source WakaTime alternative for time tracking | [Open](http://localhost:4015) |
| **MailHog** | 4017 (Web), 4019 (SMTP) | Web and API based SMTP testing tool | [Open](http://localhost:4017) |
| **Atuin** | 4018 | Magical shell history synchronization | [Open](http://localhost:4018) |
### 📚 Archival & Content Management
| Service | Port | Description | 🌐 Access |
|---------|------|-------------|-----------|
| **ArchiveBox** | 4013 | Web archiving solution | [Open](http://192.168.3.6:4013) |
| **Tube Archivist** | 4014 | YouTube video archiving | [Open](http://192.168.3.6:4014) |
| **ArchiveBox** | 4013 | Web archiving solution | [Open](http://localhost:4013) |
| **Tube Archivist** | 4014 | YouTube video archiving | [Open](http://localhost:4014) |
### 🏗️ Infrastructure Services
| Service | Port | Description | 🌐 Access |
|---------|------|-------------|-----------|
| **Pi-hole** | 4006 | DNS-based ad blocking and monitoring | [Open](http://192.168.3.6:4006) |
| **Dockhand** | 4007 | Modern Docker management UI | [Open](http://192.168.3.6:4007) |
| **Pi-hole** | 4006 | DNS-based ad blocking and monitoring | [Open](http://localhost:4006) |
| **Dockhand** | 4007 | Modern Docker management UI | [Open](http://localhost:4007) |
### 📊 Monitoring & Observability
| Service | Port | Description | 🌐 Access |
|---------|------|-------------|-----------|
| **InfluxDB** | 4008 | Time series database for metrics | [Open](http://192.168.3.6:4008) |
| **Grafana** | 4009 | Analytics and visualization platform | [Open](http://192.168.3.6:4009) |
| **InfluxDB** | 4008 | Time series database for metrics | [Open](http://localhost:4008) |
| **Grafana** | 4009 | Analytics and visualization platform | [Open](http://localhost:4009) |
### 📚 Documentation & Diagramming
| Service | Port | Description | 🌐 Access |
|---------|------|-------------|-----------|
| **Draw.io** | 4010 | Web-based diagramming application | [Open](http://192.168.3.6:4010) |
| **Kroki** | 4011 | Diagrams as a service | [Open](http://192.168.3.6:4011) |
| **Draw.io** | 4010 | Web-based diagramming application | [Open](http://localhost:4010) |
| **Kroki** | 4011 | Diagrams as a service | [Open](http://localhost:4011) |
---
@@ -222,16 +222,16 @@ graph TD
```bash
# 🎯 Full deployment and validation
./demo-stack.sh deploy && ./demo-test.sh full
./scripts/demo-stack.sh deploy && ./scripts/demo-test.sh full
# 🔍 Security compliance validation
./demo-test.sh security
./scripts/demo-test.sh security
# 👤 File ownership validation
./demo-test.sh permissions
./scripts/demo-test.sh permissions
# 🌐 Network isolation validation
./demo-test.sh network
./scripts/demo-test.sh network
```
</div>
@@ -246,12 +246,12 @@ docker compose ps
docker compose logs {service-name}
# 🌐 Test individual endpoints with variables
curl -f http://localhost:${HOMEPAGE_PORT}/
curl -f http://localhost:${INFLUXDB_PORT}/ping
curl -f http://localhost:${GRAFANA_PORT}/api/health
curl -f http://localhost:4000/
curl -f http://localhost:4008/ping
curl -f http://localhost:4009/api/health
# 🔍 Validate user permissions
ls -la /var/lib/docker/volumes/${COMPOSE_PROJECT_NAME}_*/
ls -la /var/lib/docker/volumes/kneldevstack-supportstack-demo_*/
```
---

View File

View File

View File

View File

View File

@@ -0,0 +1,229 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Docker container resource monitoring via InfluxDB",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": "InfluxDB",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "red", "value": 80 }
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
"id": 1,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{
"datasource": "InfluxDB",
"query": "from(bucket: \"demo_metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"docker_container_cpu\")\n |> filter(fn: (r) => r._field == \"usage_percent\")",
"refId": "A"
}
],
"title": "Container CPU Usage",
"type": "timeseries"
},
{
"datasource": "InfluxDB",
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "auto",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "red", "value": 80 }
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"id": 2,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{
"datasource": "InfluxDB",
"query": "from(bucket: \"demo_metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"docker_container_mem\")\n |> filter(fn: (r) => r._field == \"usage\")",
"refId": "A"
}
],
"title": "Container Memory Usage",
"type": "timeseries"
},
{
"datasource": "InfluxDB",
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 10 },
{ "color": "red", "value": 14 }
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 },
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{
"datasource": "InfluxDB",
"query": "from(bucket: \"demo_metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"docker\")\n |> filter(fn: (r) => r._field == \"containers_running\")\n |> last()",
"refId": "A"
}
],
"title": "Running Containers",
"type": "stat"
},
{
"datasource": "InfluxDB",
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 15 },
{ "color": "red", "value": 20 }
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 8 },
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{
"datasource": "InfluxDB",
"query": "from(bucket: \"demo_metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"docker\")\n |> filter(fn: (r) => r._field == \"images\")\n |> last()",
"refId": "A"
}
],
"title": "Docker Images",
"type": "stat"
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["docker", "infrastructure"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "utc",
"title": "Docker Infrastructure Overview",
"uid": "docker-overview",
"version": 1
}

View File

@@ -0,0 +1,24 @@
---
# Homepage Bookmarks
- Developer Resources:
- GitHub:
- abbr: GH
href: https://github.com
- Stack Overflow:
- abbr: SO
href: https://stackoverflow.com
- Docker Hub:
- abbr: DH
href: https://hub.docker.com
- Documentation:
- Docker Docs:
- abbr: DD
href: https://docs.docker.com
- Grafana Docs:
- abbr: GF
href: https://grafana.com/docs
- InfluxDB Docs:
- abbr: IF
href: https://docs.influxdata.com

View File

@@ -0,0 +1,77 @@
---
# Homepage Services Configuration
# Services are auto-discovered via Docker labels, but this provides
# the manual layout and widget configuration.
- Infrastructure:
- Pi-hole:
href: http://localhost:4006/admin
description: DNS management with ad blocking
icon: pihole.png
widget:
type: pihole
url: http://localhost:4006
password: demo_password
- Dockhand:
href: http://localhost:4007
description: Modern Docker management UI
icon: dockhand.png
- Monitoring:
- InfluxDB:
href: http://localhost:4008
description: Time series database for metrics
icon: influxdb.png
- Grafana:
href: http://localhost:4009
description: Analytics and visualization platform
icon: grafana.png
widget:
type: grafana
url: http://localhost:4009
username: admin
password: demo_password
- Documentation:
- Draw.io:
href: http://localhost:4010
description: Web-based diagramming application
icon: drawio.png
- Kroki:
href: http://localhost:4011
description: Diagrams as a service
icon: kroki.png
- Developer Tools:
- Atomic Tracker:
href: http://localhost:4012
description: Habit tracking and personal dashboard
icon: atomic-tracker.png
- ArchiveBox:
href: http://localhost:4013
description: Web archiving solution
icon: archivebox.png
- Tube Archivist:
href: http://localhost:4014
description: YouTube video archiving
icon: tube-archivist.png
- Wakapi:
href: http://localhost:4015
description: Open-source WakaTime alternative
icon: wakapi.png
- MailHog:
href: http://localhost:4017
description: Web and API based SMTP testing
icon: mailhog.png
- Atuin:
href: http://localhost:4018
description: Magical shell history synchronization
icon: atuin.png

View File

@@ -0,0 +1,33 @@
---
# Homepage Settings
title: TSYS Developer Support Stack
favicon: https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/docker.png
headerStyle: boxed
layout:
Infrastructure:
style: row
columns: 2
Monitoring:
style: row
columns: 2
Documentation:
style: row
columns: 2
Developer Tools:
style: row
columns: 3
providers:
docker:
socket: docker-socket-proxy:2375
quicklaunch:
searchDescriptions: true
hideInternetSearch: false
hideVisitURL: false
showStats: true
hideVersion: false

View File

@@ -0,0 +1,21 @@
---
# Homepage Widgets Configuration
- greeting:
text_size: xl
text: TSYS Developer Support Stack
- datetime:
text_size: l
format:
dateStyle: long
timeStyle: short
- search:
provider: duckduckgo
target: _blank
- glances:
url: http://localhost:4006
type: pihole
password: demo_password

View File

View File

View File

View File

View File

View File

View File

@@ -1,15 +1,18 @@
# TSYS Developer Support Stack - Demo Environment Configuration
# FOR DEMONSTRATION PURPOSES ONLY - NOT FOR PRODUCTION
# Project Identification
COMPOSE_PROJECT_NAME=kneldevstack-supportstack-demo
COMPOSE_NETWORK_NAME=kneldevstack-supportstack-demo-network
# Dynamic User Detection (to be auto-populated by scripts)
# Dynamic User Detection (auto-populated by demo-stack.sh)
DEMO_UID=1000
DEMO_GID=1000
DEMO_DOCKER_GID=986
# Port Assignments (4000-4099 range)
HOMEPAGE_PORT=4000
HOMEPAGE_ALLOWED_HOSTS=*
DOCKER_SOCKET_PROXY_PORT=4005
PIHOLE_PORT=4006
DOCKHAND_PORT=4007
@@ -22,22 +25,13 @@ ARCHIVEBOX_PORT=4013
TUBE_ARCHIVIST_PORT=4014
WAKAPI_PORT=4015
MAILHOG_PORT=4017
MAILHOG_SMTP_PORT=4019
ATUIN_PORT=4018
# Demo Credentials (CLEARLY MARKED AS DEMO ONLY)
DEMO_ADMIN_USER=admin
DEMO_ADMIN_PASSWORD=demo_password
DEMO_GRAFANA_ADMIN_PASSWORD=demo_password
DEMO_DOCKHAND_PASSWORD=demo_password
# Network Configuration
NETWORK_SUBNET=192.168.3.0/24
NETWORK_GATEWAY=192.168.3.1
# Resource Limits
MEMORY_LIMIT=512m
CPU_LIMIT=0.25
# Health Check Timeouts
HEALTH_CHECK_TIMEOUT=10s
HEALTH_CHECK_INTERVAL=30s
@@ -74,11 +68,15 @@ WEBTHEME=default-darker
# ArchiveBox Configuration
ARCHIVEBOX_SECRET_KEY=demo_secret_replace_in_production
ARCHIVEBOX_ADMIN_USER=admin
ARCHIVEBOX_ADMIN_PASSWORD=demo_password
# Tube Archivist Configuration
TA_HOST=http://localhost:4014
TA_PORT=4014
TA_DEBUG=false
TA_HOST=http://tubearchivist:8000
TA_USERNAME=admin
TA_PASSWORD=demo_password
ELASTIC_PASSWORD=demo_password
ES_JAVA_OPTS="-Xms512m -Xmx512m"
# Wakapi Configuration
WAKAPI_PASSWORD_SALT=demo_salt_replace_in_production
@@ -86,9 +84,3 @@ WAKAPI_PASSWORD_SALT=demo_salt_replace_in_production
# Atuin Configuration
ATUIN_HOST=0.0.0.0
ATUIN_OPEN_REGISTRATION=true
TA_PASSWORD=demo_password
ELASTIC_PASSWORD=demo_password
ES_JAVA_OPTS="-Xms512m -Xmx512m"
ARCHIVEBOX_ADMIN_USER=admin
ARCHIVEBOX_ADMIN_PASSWORD=demo_password
TA_USERNAME=admin

View File

@@ -1,511 +0,0 @@
---
# TSYS Developer Support Stack - Docker Compose Template
# Version: 2.0
# Purpose: Demo deployment with dynamic configuration
# DEMO CONFIGURATION ONLY - NOT FOR PRODUCTION
networks:
kneldevstack-supportstack-demo-network:
driver: bridge
ipam:
config:
- subnet: 192.168.3.0/24
gateway: 192.168.3.1
volumes:
kneldevstack-supportstack-demo_homepage_data:
driver: local
kneldevstack-supportstack-demo_pihole_data:
driver: local
kneldevstack-supportstack-demo_dockhand_data:
driver: local
kneldevstack-supportstack-demo_influxdb_data:
driver: local
kneldevstack-supportstack-demo_grafana_data:
driver: local
kneldevstack-supportstack-demo_drawio_data:
driver: local
kneldevstack-supportstack-demo_kroki_data:
driver: local
kneldevstack-supportstack-demo_atomictracker_data:
driver: local
kneldevstack-supportstack-demo_archivebox_data:
driver: local
kneldevstack-supportstack-demo_tubearchivist_data:
driver: local
kneldevstack-supportstack-demo_ta_redis_data:
driver: local
kneldevstack-supportstack-demo_ta_es_data:
driver: local
kneldevstack-supportstack-demo_wakapi_data:
driver: local
kneldevstack-supportstack-demo_mailhog_data:
driver: local
kneldevstack-supportstack-demo_atuin_data:
driver: local
services:
# Docker Socket Proxy - Security Layer
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:latest
container_name: "kneldevstack-supportstack-demo-docker-socket-proxy"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1
- IMAGES=1
- NETWORKS=1
- VOLUMES=1
- EXEC=0
- PRIVILEGED=0
- SERVICES=0
- TASKS=0
- SECRETS=0
- CONFIGS=0
- PLUGINS=0
labels:
homepage.group: "Infrastructure"
homepage.name: "Docker Socket Proxy"
homepage.icon: "docker"
homepage.description: "Secure proxy for Docker socket access (internal only)"
# Homepage - Central Dashboard
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: "kneldevstack-supportstack-demo-homepage"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4000:3000"
volumes:
- kneldevstack-supportstack-demo_homepage_data:/app/config
- ./config/homepage:/app/config/default:ro
environment:
- PUID=1000
- PGID=1000
labels:
homepage.group: "Developer Tools"
homepage.name: "Homepage"
homepage.icon: "homepage"
homepage.href: "http://localhost:4000"
homepage.description: "Central dashboard for service discovery"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
# Pi-hole - DNS Management
pihole:
image: pihole/pihole:latest
container_name: "kneldevstack-supportstack-demo-pihole"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4006:80"
volumes:
- kneldevstack-supportstack-demo_pihole_data:/etc/pihole
environment:
- TZ=UTC
- WEBPASSWORD=demo_password
- WEBTHEME=default-darker
- PUID=1000
- PGID=1000
labels:
homepage.group: "Infrastructure"
homepage.name: "Pi-hole"
homepage.icon: "pihole"
homepage.href: "http://localhost:4006"
homepage.description: "DNS management with ad blocking"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost/admin"]
interval: 30s
timeout: 10s
retries: 3
# Dockhand - Docker Management
dockhand:
image: fnsys/dockhand:latest
container_name: "kneldevstack-supportstack-demo-dockhand"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4007:3000"
volumes:
- kneldevstack-supportstack-demo_dockhand_data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- PUID=1000
- PGID=1000
labels:
homepage.group: "Infrastructure"
homepage.name: "Dockhand"
homepage.icon: "dockhand"
homepage.href: "http://localhost:4007"
homepage.description: "Modern Docker management UI"
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
# InfluxDB - Time Series Database
influxdb:
image: influxdb:2.7-alpine
container_name: "kneldevstack-supportstack-demo-influxdb"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4008:8086"
volumes:
- kneldevstack-supportstack-demo_influxdb_data:/var/lib/influxdb2
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=admin
- DOCKER_INFLUXDB_INIT_PASSWORD=demo_password
- DOCKER_INFLUXDB_INIT_ORG=tsysdemo
- DOCKER_INFLUXDB_INIT_BUCKET=demo_metrics
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=demo_token_replace_in_production
- PUID=1000
- PGID=1000
labels:
homepage.group: "Monitoring"
homepage.name: "InfluxDB"
homepage.icon: "influxdb"
homepage.href: "http://localhost:4008"
homepage.description: "Time series database for metrics"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:8086/ping"]
interval: 30s
timeout: 10s
retries: 3
# Grafana - Visualization Platform
grafana:
image: grafana/grafana:latest
container_name: "kneldevstack-supportstack-demo-grafana"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4009:3000"
volumes:
- kneldevstack-supportstack-demo_grafana_data:/var/lib/grafana
- ./config/grafana:/etc/grafana/provisioning:ro
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=demo_password
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource
- GF_SERVER_HTTP_PORT=3000
- PUID=1000
- PGID=1000
labels:
homepage.group: "Monitoring"
homepage.name: "Grafana"
homepage.icon: "grafana"
homepage.href: "http://localhost:4009"
homepage.description: "Analytics and visualization platform"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
# Draw.io - Diagramming Server
drawio:
image: fjudith/draw.io:latest
container_name: "kneldevstack-supportstack-demo-drawio"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4010:8080"
volumes:
- kneldevstack-supportstack-demo_drawio_data:/root
environment:
- PUID=1000
- PGID=1000
labels:
homepage.group: "Documentation"
homepage.name: "Draw.io"
homepage.icon: "drawio"
homepage.href: "http://localhost:4010"
homepage.description: "Web-based diagramming application"
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:8080"]
interval: 30s
timeout: 10s
retries: 3
# Kroki - Diagrams as a Service
kroki:
image: yuzutech/kroki:latest
container_name: "kneldevstack-supportstack-demo-kroki"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4011:8000"
volumes:
- kneldevstack-supportstack-demo_kroki_data:/data
environment:
- KROKI_SAFE_MODE=secure
- PUID=1000
- PGID=1000
labels:
homepage.group: "Documentation"
homepage.name: "Kroki"
homepage.icon: "kroki"
homepage.href: "http://localhost:4011"
homepage.description: "Diagrams as a service"
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
# Atomic Tracker - Habit Tracking
atomictracker:
image: ghcr.io/majorpeter/atomic-tracker:v1.3.1
container_name: "kneldevstack-supportstack-demo-atomictracker"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4012:8080"
volumes:
- kneldevstack-supportstack-demo_atomictracker_data:/app/data
environment:
- NODE_ENV=production
- PUID=1000
- PGID=1000
labels:
homepage.group: "Developer Tools"
homepage.name: "Atomic Tracker"
homepage.icon: "atomic-tracker"
homepage.href: "http://localhost:4012"
homepage.description: "Habit tracking and personal dashboard"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:8080"]
interval: 30s
timeout: 10s
retries: 3
# ArchiveBox - Web Archiving
archivebox:
image: archivebox/archivebox:latest
container_name: "kneldevstack-supportstack-demo-archivebox"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4013:8000"
volumes:
- kneldevstack-supportstack-demo_archivebox_data:/data
environment:
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=demo_password
- ALLOWED_HOSTS=*
- CSRF_TRUSTED_ORIGINS=http://localhost:4013
- PUBLIC_INDEX=True
- PUBLIC_SNAPSHOTS=True
- PUBLIC_ADD_VIEW=False
- PUID=1000
- PGID=1000
labels:
homepage.group: "Developer Tools"
homepage.name: "ArchiveBox"
homepage.icon: "archivebox"
homepage.href: "http://localhost:4013"
homepage.description: "Web archiving solution"
healthcheck:
test: ["CMD", "curl", "-fsS",
"http://localhost:8000/health/"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
# Tube Archivist - Redis
ta-redis:
image: redis:7-alpine
container_name: "kneldevstack-supportstack-demo-ta-redis"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
volumes:
- kneldevstack-supportstack-demo_ta_redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Tube Archivist - Elasticsearch
ta-elasticsearch:
image: elasticsearch:8.12.0
container_name: "kneldevstack-supportstack-demo-ta-elasticsearch"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
volumes:
- kneldevstack-supportstack-demo_ta_es_data:/usr/share/elasticsearch/data
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- bootstrap.memory_lock=true
- path.repo=/usr/share/elasticsearch/data/snapshot
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
# Tube Archivist - YouTube Archiving
tubearchivist:
image: bbilly1/tubearchivist:latest
container_name: "kneldevstack-supportstack-demo-tubearchivist"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4014:8000"
volumes:
- kneldevstack-supportstack-demo_tubearchivist_data:/cache
environment:
- ES_URL=http://ta-elasticsearch:9200
- REDIS_CON=redis://ta-redis:6379
- ELASTIC_PASSWORD=demo_password
- HOST_UID=1000
- HOST_GID=1000
- TA_HOST=http://localhost:4014
- TA_USERNAME=admin
- TA_PASSWORD=demo_password
- TZ=UTC
depends_on:
ta-redis:
condition: service_healthy
ta-elasticsearch:
condition: service_healthy
labels:
homepage.group: "Developer Tools"
homepage.name: "Tube Archivist"
homepage.icon: "tube-archivist"
homepage.href: "http://localhost:4014"
homepage.description: "YouTube video archiving"
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:8000/api/health/"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
# Wakapi - Time Tracking
wakapi:
image: ghcr.io/muety/wakapi:latest
container_name: "kneldevstack-supportstack-demo-wakapi"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4015:3000"
volumes:
- kneldevstack-supportstack-demo_wakapi_data:/data
environment:
- WAKAPI_PASSWORD_SALT=demo_salt_replace_in_production
- PUID=1000
- PGID=1000
labels:
homepage.group: "Developer Tools"
homepage.name: "Wakapi"
homepage.icon: "wakapi"
homepage.href: "http://localhost:4015"
homepage.description: "Open-source WakaTime alternative"
healthcheck:
test: ["CMD", "/app/healthcheck"]
interval: 30s
timeout: 10s
retries: 3
# MailHog - Email Testing
mailhog:
image: mailhog/mailhog:latest
container_name: "kneldevstack-supportstack-demo-mailhog"
restart: unless-stopped
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4017:8025"
volumes:
- kneldevstack-supportstack-demo_mailhog_data:/maildir
environment:
- PUID=1000
- PGID=1000
labels:
homepage.group: "Developer Tools"
homepage.name: "MailHog"
homepage.icon: "mailhog"
homepage.href: "http://localhost:4017"
homepage.description: "Web and API based SMTP testing"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:8025"]
interval: 30s
timeout: 10s
retries: 3
# Atuin - Shell History Synchronization
atuin:
image: ghcr.io/atuinsh/atuin:v18.10.0
container_name: "kneldevstack-supportstack-demo-atuin"
restart: unless-stopped
command:
- server
- start
networks:
- kneldevstack-supportstack-demo-network
ports:
- "4018:8888"
volumes:
- kneldevstack-supportstack-demo_atuin_data:/config
environment:
- ATUIN_HOST=0.0.0.0
- ATUIN_PORT=8888
- ATUIN_OPEN_REGISTRATION=true
- ATUIN_DB_URI=sqlite:///config/atuin.db
- RUST_LOG=info,atuin_server=info
labels:
homepage.group: "Developer Tools"
homepage.name: "Atuin"
homepage.icon: "atuin"
homepage.href: "http://localhost:4018"
homepage.description: "Magical shell history synchronization"
healthcheck:
test: ["CMD", "bash", "-c", "echo > /dev/tcp/localhost/8888"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s

View File

@@ -66,11 +66,21 @@ services:
- SECRETS=${DOCKER_SOCKET_PROXY_SECRETS}
- CONFIGS=${DOCKER_SOCKET_PROXY_CONFIGS}
- PLUGINS=${DOCKER_SOCKET_PROXY_PLUGINS}
- POST=1
- DELETE=1
- ALLOW_START=1
- ALLOW_STOP=1
- ALLOW_RESTARTS=1
deploy:
resources:
limits:
memory: 128M
labels:
homepage.group: "Infrastructure"
homepage.name: "Docker Socket Proxy"
homepage.icon: "docker"
homepage.description: "Secure proxy for Docker socket access (internal only)"
homepage.description: >-
Secure proxy for Docker socket access (internal only)
# Homepage - Central Dashboard
homepage:
@@ -82,9 +92,9 @@ services:
ports:
- "${HOMEPAGE_PORT}:3000"
volumes:
- ${COMPOSE_PROJECT_NAME}_homepage_data:/app/config
- ./config/homepage:/app/config/default:ro
- ./config/homepage:/app/config
environment:
- HOMEPAGE_ALLOWED_HOSTS=${HOMEPAGE_ALLOWED_HOSTS}
- PUID=${DEMO_UID}
- PGID=${DEMO_GID}
labels:
@@ -93,6 +103,10 @@ services:
homepage.icon: "homepage"
homepage.href: "http://localhost:${HOMEPAGE_PORT}"
homepage.description: "Central dashboard for service discovery"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:3000"]
@@ -123,6 +137,10 @@ services:
homepage.icon: "pihole"
homepage.href: "http://localhost:${PIHOLE_PORT}"
homepage.description: "DNS management with ad blocking"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost/admin"]
@@ -141,16 +159,23 @@ services:
- "${DOCKHAND_PORT}:3000"
volumes:
- ${COMPOSE_PROJECT_NAME}_dockhand_data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOCKER_HOST=tcp://docker-socket-proxy:2375
- PUID=${DEMO_UID}
- PGID=${DEMO_GID}
depends_on:
docker-socket-proxy:
condition: service_started
labels:
homepage.group: "Infrastructure"
homepage.name: "Dockhand"
homepage.icon: "dockhand"
homepage.href: "http://localhost:${DOCKHAND_PORT}"
homepage.description: "Modern Docker management UI"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:3000"]
@@ -184,6 +209,10 @@ services:
homepage.icon: "influxdb"
homepage.href: "http://localhost:${INFLUXDB_PORT}"
homepage.description: "Time series database for metrics"
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:8086/ping"]
@@ -216,6 +245,10 @@ services:
homepage.icon: "grafana"
homepage.href: "http://localhost:${GRAFANA_PORT}"
homepage.description: "Analytics and visualization platform"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:3000/api/health"]
@@ -243,6 +276,10 @@ services:
homepage.icon: "drawio"
homepage.href: "http://localhost:${DRAWIO_PORT}"
homepage.description: "Web-based diagramming application"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:8080"]
@@ -271,6 +308,10 @@ services:
homepage.icon: "kroki"
homepage.href: "http://localhost:${KROKI_PORT}"
homepage.description: "Diagrams as a service"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:8000/health"]
@@ -299,6 +340,10 @@ services:
homepage.icon: "atomic-tracker"
homepage.href: "http://localhost:${ATOMIC_TRACKER_PORT}"
homepage.description: "Habit tracking and personal dashboard"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:8080"]
@@ -333,6 +378,10 @@ services:
homepage.icon: "archivebox"
homepage.href: "http://localhost:${ARCHIVEBOX_PORT}"
homepage.description: "Web archiving solution"
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD", "curl", "-fsS",
"http://localhost:8000/health/"]
@@ -350,6 +399,10 @@ services:
- ${COMPOSE_NETWORK_NAME}
volumes:
- ${COMPOSE_PROJECT_NAME}_ta_redis_data:/data
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: ${HEALTH_CHECK_INTERVAL}
@@ -376,8 +429,14 @@ services:
memlock:
soft: -1
hard: -1
deploy:
resources:
limits:
memory: 1024M
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
test:
["CMD-SHELL",
"curl -sf http://localhost:9200/_cluster/health || exit 1"]
interval: ${HEALTH_CHECK_INTERVAL}
timeout: ${HEALTH_CHECK_TIMEOUT}
retries: 10
@@ -415,6 +474,10 @@ services:
homepage.icon: "tube-archivist"
homepage.href: "http://localhost:${TUBE_ARCHIVIST_PORT}"
homepage.description: "YouTube video archiving"
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "--silent",
"http://localhost:8000/api/health/"]
@@ -444,6 +507,10 @@ services:
homepage.icon: "wakapi"
homepage.href: "http://localhost:${WAKAPI_PORT}"
homepage.description: "Open-source WakaTime alternative"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "/app/healthcheck"]
interval: ${HEALTH_CHECK_INTERVAL}
@@ -459,6 +526,7 @@ services:
- ${COMPOSE_NETWORK_NAME}
ports:
- "${MAILHOG_PORT}:8025"
- "${MAILHOG_SMTP_PORT}:1025"
volumes:
- ${COMPOSE_PROJECT_NAME}_mailhog_data:/maildir
environment:
@@ -470,6 +538,10 @@ services:
homepage.icon: "mailhog"
homepage.href: "http://localhost:${MAILHOG_PORT}"
homepage.description: "Web and API based SMTP testing"
deploy:
resources:
limits:
memory: 128M
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider",
"http://localhost:8025"]
@@ -503,6 +575,10 @@ services:
homepage.icon: "atuin"
homepage.href: "http://localhost:${ATUIN_PORT}"
homepage.description: "Magical shell history synchronization"
deploy:
resources:
limits:
memory: 256M
healthcheck:
test: ["CMD", "bash", "-c", "echo > /dev/tcp/localhost/8888"]
interval: ${HEALTH_CHECK_INTERVAL}

View File

@@ -1,5 +1,7 @@
# TSYS Developer Support Stack - Troubleshooting Guide
> **Note:** All commands in this guide assume your working directory is the `demo/` folder of the repository. Run `cd demo` first if needed.
## Common Issues and Solutions
### Services Not Starting

View File

@@ -3,6 +3,7 @@ set -euo pipefail
DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$DEMO_DIR/demo.env"
ENV_TEMPLATE="$DEMO_DIR/demo.env.template"
TEMPLATE_FILE="$DEMO_DIR/docker-compose.yml.template"
COMPOSE_FILE="$DEMO_DIR/docker-compose.yml"
@@ -17,17 +18,19 @@ log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
fix_env() {
log_info "Ensuring demo.env is complete..."
grep -q '^TA_USERNAME=' "$ENV_FILE" || echo "TA_USERNAME=demo" >> "$ENV_FILE"
grep -q '^TA_PASSWORD=' "$ENV_FILE" || echo "TA_PASSWORD=demo_password" >> "$ENV_FILE"
grep -q '^ELASTIC_PASSWORD=' "$ENV_FILE" || echo "ELASTIC_PASSWORD=demo_password" >> "$ENV_FILE"
grep -q '^ES_JAVA_OPTS=' "$ENV_FILE" || echo 'ES_JAVA_OPTS="-Xms512m -Xmx512m"' >> "$ENV_FILE"
grep -q '^ARCHIVEBOX_ADMIN_USER=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_USER=admin" >> "$ENV_FILE"
grep -q '^ARCHIVEBOX_ADMIN_PASSWORD=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_PASSWORD=demo_password" >> "$ENV_FILE"
sed -i 's/^ATUIN_HOST=.*/ATUIN_HOST=0.0.0.0/' "$ENV_FILE"
sed -i 's|^TA_HOST=.*|TA_HOST=http://localhost:4014|' "$ENV_FILE"
log_success "demo.env ready"
ensure_env() {
if [[ ! -f "$ENV_FILE" ]]; then
if [[ -f "$ENV_TEMPLATE" ]]; then
log_info "Creating demo.env from template..."
cp "$ENV_TEMPLATE" "$ENV_FILE"
else
log_error "No demo.env or demo.env.template found"
exit 1
fi
fi
# Ensure new variables exist in older env files
grep -q '^MAILHOG_SMTP_PORT=' "$ENV_FILE" || echo "MAILHOG_SMTP_PORT=4019" >> "$ENV_FILE"
grep -q '^HOMEPAGE_ALLOWED_HOSTS=' "$ENV_FILE" || echo "HOMEPAGE_ALLOWED_HOSTS=*" >> "$ENV_FILE"
}
detect_user() {
@@ -48,6 +51,10 @@ check_prerequisites() {
log_error "Docker is not running"
exit 1
fi
if ! command -v envsubst >/dev/null 2>&1; then
log_error "envsubst not found (install gettext package)"
exit 1
fi
local max_map_count
max_map_count=$(sysctl -n vm.max_map_count 2>/dev/null || echo "0")
if [[ "$max_map_count" -lt 262144 ]]; then
@@ -79,26 +86,25 @@ wait_healthy() {
log_info "Waiting for services to become healthy (max 5 min)..."
local elapsed=0 interval=15
while [[ $elapsed -lt 300 ]]; do
local all_ok=true
while IFS= read -r line; do
local name health
name=$(echo "$line" | awk '{print $1}')
health=$(echo "$line" | awk '{print $2}')
[[ "$name" == "NAMES" || -z "$name" ]] && continue
if [[ "$health" != "healthy" && -n "$health" ]]; then
all_ok=false
local unhealthy=0
while IFS= read -r name; do
local health
health=$(docker inspect --format='{{.State.Health.Status}}' "$name" 2>/dev/null || echo "unknown")
if [[ "$health" != "healthy" ]]; then
unhealthy=$((unhealthy + 1))
fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null | sed 's/(healthy)/healthy/g; s/(unhealthy)/unhealthy/g; s/(health: starting)/starting/g')
if $all_ok; then
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format '{{.Names}}' 2>/dev/null)
if [[ $unhealthy -eq 0 ]]; then
log_success "All services healthy"
return 0
fi
log_info " Still waiting... (${elapsed}s elapsed)"
log_info " $unhealthy services not yet healthy (${elapsed}s elapsed)"
sleep $interval
elapsed=$((elapsed + interval))
done
log_warn "Timeout - some services may not be fully healthy"
docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "table {{.Names}}\t{{.Status}}"
cd "$DEMO_DIR" && docker compose ps
}
display_summary() {
@@ -126,10 +132,11 @@ display_summary() {
echo " ArchiveBox http://localhost:${ARCHIVEBOX_PORT}"
echo " Tube Archivist http://localhost:${TUBE_ARCHIVIST_PORT}"
echo " Wakapi http://localhost:${WAKAPI_PORT}"
echo " MailHog http://localhost:${MAILHOG_PORT}"
echo " MailHog (Web) http://localhost:${MAILHOG_PORT}"
echo " MailHog (SMTP) localhost:${MAILHOG_SMTP_PORT}"
echo " Atuin http://localhost:${ATUIN_PORT}"
echo ""
echo " Credentials: ${DEMO_ADMIN_USER:-admin} / ${DEMO_ADMIN_PASSWORD:-demo_password}"
echo " Credentials: admin / demo_password"
echo " FOR DEMONSTRATION PURPOSES ONLY"
echo "========================================================"
}
@@ -137,15 +144,31 @@ display_summary() {
smoke_test() {
log_info "Running smoke tests..."
set -a; source "$ENV_FILE"; set +a
local ports=(4000 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4017 4018)
local ports=(
"${HOMEPAGE_PORT}:Homepage"
"${PIHOLE_PORT}:Pi-hole"
"${DOCKHAND_PORT}:Dockhand"
"${INFLUXDB_PORT}:InfluxDB"
"${GRAFANA_PORT}:Grafana"
"${DRAWIO_PORT}:Draw.io"
"${KROKI_PORT}:Kroki"
"${ATOMIC_TRACKER_PORT}:AtomicTracker"
"${ARCHIVEBOX_PORT}:ArchiveBox"
"${TUBE_ARCHIVIST_PORT}:TubeArchivist"
"${WAKAPI_PORT}:Wakapi"
"${MAILHOG_PORT}:MailHog"
"${ATUIN_PORT}:Atuin"
)
local pass=0 fail=0
for port in "${ports[@]}"; do
for pt in "${ports[@]}"; do
local port="${pt%:*}"
local svc="${pt#*:}"
if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
log_success "Port $port accessible"
((pass++))
log_success "$svc (:$port)"
((pass++)) || true
else
log_error "Port $port NOT accessible"
((fail++))
log_error "$svc (:$port) NOT accessible"
((fail++)) || true
fi
done
echo ""
@@ -179,9 +202,10 @@ show_usage() {
echo " help Show this help"
}
ensure_env
case "${1:-deploy}" in
deploy)
fix_env
detect_user
check_prerequisites
generate_compose
@@ -196,8 +220,8 @@ case "${1:-deploy}" in
restart)
stop_stack
sleep 5
fix_env
detect_user
check_prerequisites
generate_compose
deploy_stack
wait_healthy

View File

@@ -21,10 +21,10 @@ TESTS_FAILED=0
TESTS_TOTAL=0
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)); }
log_success() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)) || true; }
log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)); }
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; ((TESTS_TOTAL++)); }
log_error() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)) || true; }
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; ((TESTS_TOTAL++)) || true; }
test_file_ownership() {
log_test "File ownership (no root-owned files)"
@@ -83,7 +83,7 @@ test_service_health() {
log_success "$name running"
else
log_error "$name not running: $line"
((unhealthy++))
((unhealthy++)) || true
fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null)
if [[ $unhealthy -eq 0 ]]; then
@@ -120,7 +120,7 @@ test_port_accessibility() {
log_success "$svc (:$port)"
else
log_error "$svc (:$port) not accessible"
((failed++))
((failed++)) || true
fi
done
if [[ $failed -eq 0 ]]; then
@@ -168,14 +168,20 @@ test_security_compliance() {
log_error "Docker socket proxy not found"
fi
# Count direct socket mounts - proxy + dockhand are expected
# Count direct socket mounts - only proxy should have one
local socket_mounts
socket_mounts=$(grep -c "/var/run/docker.sock" "$COMPOSE_FILE" || echo "0")
local expected_mounts=2 # proxy (ro) + dockhand (rw for management)
if [[ "$socket_mounts" -le "$expected_mounts" ]]; then
log_success "Socket mounts within expected range ($socket_mounts)"
socket_mounts=$(grep -c '/var/run/docker.sock' "$COMPOSE_FILE" || echo "0")
if [[ "$socket_mounts" -le 1 ]]; then
log_success "Socket mount on proxy only ($socket_mounts)"
else
log_warning "Unexpected socket mounts: $socket_mounts (expected <= $expected_mounts)"
log_error "Unexpected socket mounts: $socket_mounts (expected 1, proxy only)"
fi
# Dockhand uses proxy, not direct socket
if grep -q 'DOCKER_HOST=tcp://docker-socket-proxy' "$COMPOSE_FILE"; then
log_success "Dockhand routes through socket proxy"
else
log_error "Dockhand not using socket proxy"
fi
}

View File

@@ -1,223 +0,0 @@
#!/bin/bash
set -euo pipefail
DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$DEMO_DIR/demo.env"
TEMPLATE_FILE="$DEMO_DIR/docker-compose.yml.template"
COMPOSE_FILE="$DEMO_DIR/docker-compose.yml"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
fix_env() {
log_info "Ensuring demo.env is complete..."
grep -q '^TA_USERNAME=' "$ENV_FILE" || echo "TA_USERNAME=demo" >> "$ENV_FILE"
grep -q '^TA_PASSWORD=' "$ENV_FILE" || echo "TA_PASSWORD=demo_password" >> "$ENV_FILE"
grep -q '^ELASTIC_PASSWORD=' "$ENV_FILE" || echo "ELASTIC_PASSWORD=demo_password" >> "$ENV_FILE"
grep -q '^ES_JAVA_OPTS=' "$ENV_FILE" || echo 'ES_JAVA_OPTS="-Xms512m -Xmx512m"' >> "$ENV_FILE"
grep -q '^ARCHIVEBOX_ADMIN_USER=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_USER=admin" >> "$ENV_FILE"
grep -q '^ARCHIVEBOX_ADMIN_PASSWORD=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_PASSWORD=demo_password" >> "$ENV_FILE"
sed -i 's/^ATUIN_HOST=.*/ATUIN_HOST=0.0.0.0/' "$ENV_FILE"
sed -i 's|^TA_HOST=.*|TA_HOST=http://localhost:4014|' "$ENV_FILE"
log_success "demo.env ready"
}
detect_user() {
log_info "Detecting user IDs..."
local uid gid docker_gid
uid=$(id -u)
gid=$(id -g)
docker_gid=$(getent group docker | cut -d: -f3)
sed -i "s/^DEMO_UID=.*/DEMO_UID=$uid/" "$ENV_FILE"
sed -i "s/^DEMO_GID=.*/DEMO_GID=$gid/" "$ENV_FILE"
sed -i "s/^DEMO_DOCKER_GID=.*/DEMO_DOCKER_GID=$docker_gid/" "$ENV_FILE"
log_success "UID=$uid GID=$gid DockerGID=$docker_gid"
}
check_prerequisites() {
log_info "Checking prerequisites..."
if ! docker info >/dev/null 2>&1; then
log_error "Docker is not running"
exit 1
fi
local max_map_count
max_map_count=$(sysctl -n vm.max_map_count 2>/dev/null || echo "0")
if [[ "$max_map_count" -lt 262144 ]]; then
log_warn "Setting vm.max_map_count=262144 for Elasticsearch..."
if sudo sysctl -w vm.max_map_count=262144 2>/dev/null; then
log_success "vm.max_map_count set"
else
log_warn "Could not set vm.max_map_count (TubeArchivist ES may fail)"
fi
fi
log_success "Prerequisites OK"
}
generate_compose() {
log_info "Generating docker-compose.yml from template..."
set -a; source "$ENV_FILE"; set +a
envsubst < "$TEMPLATE_FILE" > "$COMPOSE_FILE"
log_success "docker-compose.yml generated"
}
deploy_stack() {
log_info "Deploying TSYS Developer Support Stack..."
cd "$DEMO_DIR"
docker compose up -d 2>&1
log_success "Stack deployment initiated"
}
wait_healthy() {
log_info "Waiting for services to become healthy (max 5 min)..."
local elapsed=0 interval=15
while [[ $elapsed -lt 300 ]]; do
local all_ok=true
while IFS= read -r line; do
local name health
name=$(echo "$line" | awk '{print $1}')
health=$(echo "$line" | awk '{print $2}')
[[ "$name" == "NAMES" || -z "$name" ]] && continue
if [[ "$health" != "healthy" && -n "$health" ]]; then
all_ok=false
fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null | sed 's/(healthy)/healthy/g; s/(unhealthy)/unhealthy/g; s/(health: starting)/starting/g')
if $all_ok; then
log_success "All services healthy"
return 0
fi
log_info " Still waiting... (${elapsed}s elapsed)"
sleep $interval
elapsed=$((elapsed + interval))
done
log_warn "Timeout - some services may not be fully healthy"
docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "table {{.Names}}\t{{.Status}}"
}
display_summary() {
set -a; source "$ENV_FILE"; set +a
echo ""
echo "========================================================"
echo " TSYS Developer Support Stack - Deployment Summary"
echo "========================================================"
echo ""
echo " Infrastructure:"
echo " Homepage Dashboard http://localhost:${HOMEPAGE_PORT}"
echo " Pi-hole (DNS) http://localhost:${PIHOLE_PORT}"
echo " Dockhand (Docker) http://localhost:${DOCKHAND_PORT}"
echo ""
echo " Monitoring:"
echo " InfluxDB http://localhost:${INFLUXDB_PORT}"
echo " Grafana http://localhost:${GRAFANA_PORT}"
echo ""
echo " Documentation:"
echo " Draw.io http://localhost:${DRAWIO_PORT}"
echo " Kroki http://localhost:${KROKI_PORT}"
echo ""
echo " Developer Tools:"
echo " Atomic Tracker http://localhost:${ATOMIC_TRACKER_PORT}"
echo " ArchiveBox http://localhost:${ARCHIVEBOX_PORT}"
echo " Tube Archivist http://localhost:${TUBE_ARCHIVIST_PORT}"
echo " Wakapi http://localhost:${WAKAPI_PORT}"
echo " MailHog http://localhost:${MAILHOG_PORT}"
echo " Atuin http://localhost:${ATUIN_PORT}"
echo ""
echo " Credentials: ${DEMO_ADMIN_USER:-admin} / ${DEMO_ADMIN_PASSWORD:-demo_password}"
echo " FOR DEMONSTRATION PURPOSES ONLY"
echo "========================================================"
}
smoke_test() {
log_info "Running smoke tests..."
set -a; source "$ENV_FILE"; set +a
local ports=(4000 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4017 4018)
local pass=0 fail=0
for port in "${ports[@]}"; do
if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
log_success "Port $port accessible"
((pass++))
else
log_error "Port $port NOT accessible"
((fail++))
fi
done
echo ""
echo "SMOKE TEST: $pass passed, $fail failed"
}
stop_stack() {
log_info "Stopping stack..."
cd "$DEMO_DIR"
docker compose down 2>&1
log_success "Stack stopped"
}
show_status() {
cd "$DEMO_DIR"
docker compose ps
}
show_usage() {
echo "TSYS Developer Support Stack"
echo ""
echo "Usage: $0 {deploy|stop|restart|status|smoke|summary|help}"
echo ""
echo "Commands:"
echo " deploy Deploy the complete stack"
echo " stop Stop all services"
echo " restart Stop and redeploy"
echo " status Show service status"
echo " smoke Run port accessibility tests"
echo " summary Show service URLs"
echo " help Show this help"
}
case "${1:-deploy}" in
deploy)
fix_env
detect_user
check_prerequisites
generate_compose
deploy_stack
wait_healthy
display_summary
smoke_test
;;
stop)
stop_stack
;;
restart)
stop_stack
sleep 5
fix_env
detect_user
generate_compose
deploy_stack
wait_healthy
display_summary
;;
status)
show_status
;;
smoke)
smoke_test
;;
summary)
display_summary
;;
help|--help|-h)
show_usage
;;
*)
log_error "Unknown command: $1"
show_usage
exit 1
;;
esac

View File

@@ -95,7 +95,7 @@ validate_docker_images() {
validate_port_availability() {
log_validation "Validating port availability..."
set -a; source "$DEMO_DIR/demo.env" 2>/dev/null || true; set +a
set -a; source "$DEMO_DIR/demo.env" 2>/dev/null || source "$DEMO_DIR/demo.env.template" 2>/dev/null || true; set +a
local ports=(
"$HOMEPAGE_PORT"
"$PIHOLE_PORT"
@@ -124,8 +124,15 @@ validate_port_availability() {
validate_environment() {
log_validation "Validating environment variables..."
local env_source=""
if [[ -f "$DEMO_DIR/demo.env" ]]; then
set -a; source "$DEMO_DIR/demo.env"; set +a
env_source="$DEMO_DIR/demo.env"
elif [[ -f "$DEMO_DIR/demo.env.template" ]]; then
env_source="$DEMO_DIR/demo.env.template"
log_validation "Using demo.env.template (demo.env not found)"
fi
if [[ -n "$env_source" ]]; then
set -a; source "$env_source"; set +a
local required_vars=(
"COMPOSE_PROJECT_NAME"
"COMPOSE_NETWORK_NAME"
@@ -135,20 +142,20 @@ validate_environment() {
"DRAWIO_PORT" "KROKI_PORT"
"ATOMIC_TRACKER_PORT" "ARCHIVEBOX_PORT"
"TUBE_ARCHIVIST_PORT" "WAKAPI_PORT"
"MAILHOG_PORT" "ATUIN_PORT"
"MAILHOG_PORT" "MAILHOG_SMTP_PORT" "ATUIN_PORT"
"TA_USERNAME" "TA_PASSWORD" "ELASTIC_PASSWORD"
"GF_SECURITY_ADMIN_USER" "GF_SECURITY_ADMIN_PASSWORD"
"PIHOLE_WEBPASSWORD"
)
for var in "${required_vars[@]}"; do
if [[ -n "${!var:-}" ]]; then
log_pass "Environment variable set: $var=${!var}"
log_pass "Environment variable set: $var"
else
log_fail "Environment variable missing: $var"
fi
done
else
log_fail "demo.env file not found"
log_fail "No demo.env or demo.env.template found"
fi
}

View File

@@ -0,0 +1,8 @@
{
"name": "tsys-e2e-tests",
"version": "1.0.0",
"private": true,
"devDependencies": {
"@playwright/test": "1.52.0"
}
}

View File

@@ -0,0 +1,105 @@
import { test, expect } from '@playwright/test';
const services = [
{
name: 'Homepage',
url: 'http://localhost:4000',
contentCheck: 'tsys developer support stack',
titleCheck: 'TSYS Developer Support Stack',
},
{
name: 'Pi-hole',
url: 'http://localhost:4006/admin',
contentCheck: 'pihole',
},
{
name: 'Dockhand',
url: 'http://localhost:4007',
contentCheck: 'sveltekit',
},
{
name: 'InfluxDB',
url: 'http://localhost:4008',
contentCheck: 'influxdb',
},
{
name: 'Grafana',
url: 'http://localhost:4009',
contentCheck: 'grafana',
},
{
name: 'Draw.io',
url: 'http://localhost:4010',
contentCheck: 'diagram',
},
{
name: 'Kroki',
url: 'http://localhost:4011/health',
contentCheck: 'kroki',
},
{
name: 'Atomic Tracker',
url: 'http://localhost:4012',
contentCheck: 'journal',
},
{
name: 'ArchiveBox',
url: 'http://localhost:4013',
contentCheck: 'archive',
},
{
name: 'Tube Archivist',
url: 'http://localhost:4014',
contentCheck: 'tubearchivist',
},
{
name: 'Wakapi',
url: 'http://localhost:4015',
contentCheck: 'wakapi',
},
{
name: 'MailHog',
url: 'http://localhost:4017',
contentCheck: 'mailhog',
},
{
name: 'Atuin',
url: 'http://localhost:4018',
contentCheck: 'version',
},
];
for (const svc of services) {
test(`${svc.name} (${svc.url}) loads successfully`, async ({ page }) => {
const response = await page.goto(svc.url, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
expect(response).not.toBeNull();
expect(response!.status()).toBeLessThan(400);
const body = await page.textContent('body').catch(() => '');
const title = await page.title().catch(() => '');
const combined = (body + ' ' + title).toLowerCase();
expect(
combined,
`${svc.name} should not show an error page`
).not.toContain('host validation failed');
expect(
combined,
`${svc.name} should not show a server error`
).not.toContain('internal server error');
expect(
combined,
`${svc.name} should contain expected content`
).toContain(svc.contentCheck.toLowerCase());
if (svc.titleCheck) {
expect(
title.toLowerCase(),
`${svc.name} should have expected title`
).toContain(svc.titleCheck.toLowerCase());
}
});
}

View File

@@ -0,0 +1,21 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: '.',
testMatch: '*.spec.ts',
timeout: 60000,
retries: 1,
use: {
headless: true,
browserName: 'chromium',
launchOptions: {
args: ['--no-sandbox', '--disable-setuid-sandbox'],
},
},
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' },
},
],
});

View File

@@ -1,71 +1,117 @@
#!/bin/bash
# Integration test: Service-to-service communication
# Requires a running stack. Validates inter-service connectivity.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
ENV_FILE="$PROJECT_ROOT/demo.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE not found. Copy demo.env.template to demo.env and configure."
exit 1
fi
set -a; source "$ENV_FILE"; set +a
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PASS=0
FAIL=0
pass() { echo "PASS: $1"; ((PASS++)); }
fail() { echo "FAIL: $1"; ((FAIL++)); }
pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((PASS++)); }
fail() { echo -e "${RED}[FAIL]${NC} $1"; ((FAIL++)); }
check() { echo -e "${YELLOW}[CHECK]${NC} $1"; }
test_grafana_influxdb_integration() {
if docker exec "${COMPOSE_PROJECT_NAME}-grafana" wget -q --spider http://influxdb:8086/ping 2>/dev/null; then
pass "Grafana-InfluxDB integration"
else
fail "Grafana-InfluxDB integration"
require_stack_running() {
if ! docker ps --filter "name=${COMPOSE_PROJECT_NAME}" --format "{{.Names}}" | grep -q .; then
echo "ERROR: No running containers found for ${COMPOSE_PROJECT_NAME}"
echo "Run ./scripts/demo-stack.sh deploy first"
exit 1
fi
}
test_dockhand_docker_integration() {
if docker exec "${COMPOSE_PROJECT_NAME}-dockhand" sh -c 'command -v docker >/dev/null 2>&1 && docker version >/dev/null 2>&1' 2>/dev/null; then
pass "Dockhand-Docker integration"
test_grafana_influxdb_integration() {
check "Grafana can reach InfluxDB on internal network"
if docker exec "${COMPOSE_PROJECT_NAME}-grafana" wget -q --spider http://influxdb:8086/ping 2>/dev/null; then
pass "Grafana reaches InfluxDB via internal DNS"
else
pass "Dockhand-Docker integration (socket mount OK - no docker CLI in container)"
fail "Grafana cannot reach InfluxDB"
fi
}
test_dockhand_proxy_integration() {
check "Dockhand can reach Docker via socket proxy"
local dockhand_env
dockhand_env=$(docker exec "${COMPOSE_PROJECT_NAME}-dockhand" env 2>/dev/null || echo "")
if echo "$dockhand_env" | grep -q "DOCKER_HOST=tcp://docker-socket-proxy:2375"; then
pass "Dockhand configured with DOCKER_HOST pointing to socket proxy"
else
fail "Dockhand DOCKER_HOST not configured for socket proxy"
fi
}
test_homepage_discovery() {
local discovered
discovered=$(curl -sf "http://localhost:${HOMEPAGE_PORT}" 2>/dev/null | grep -ci "service\|href\|homepage" || echo "0")
if [[ "$discovered" -ge 1 ]]; then
pass "Homepage service discovery (found references)"
check "Homepage responds and contains service references"
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${HOMEPAGE_PORT}" 2>/dev/null || echo "000")
if [[ "$http_code" -ge 200 && "$http_code" -lt 400 ]]; then
pass "Homepage accessible (HTTP $http_code)"
else
fail "Homepage service discovery"
fail "Homepage not accessible (HTTP $http_code)"
fi
}
test_tubearchivist_redis() {
if docker exec "${COMPOSE_PROJECT_NAME}-tubearchivist" curl -sf http://ta-redis:6379 2>/dev/null || \
docker exec "${COMPOSE_PROJECT_NAME}-ta-redis" redis-cli ping 2>/dev/null | grep -q PONG; then
pass "TubeArchivist-Redis integration"
check "Tube Archivist can reach Redis"
if docker exec "${COMPOSE_PROJECT_NAME}-ta-redis" redis-cli ping 2>/dev/null | grep -q PONG; then
pass "Redis responds to PING"
else
fail "TubeArchivist-Redis integration"
fail "Redis not responding"
fi
}
test_tubearchivist_elasticsearch() {
if docker exec "${COMPOSE_PROJECT_NAME}-tubearchivist" curl -sf http://ta-elasticsearch:9200 2>/dev/null; then
pass "TubeArchivist-Elasticsearch integration"
check "Elasticsearch cluster is healthy"
local es_status
es_status=$(docker exec "${COMPOSE_PROJECT_NAME}-ta-elasticsearch" curl -sf http://localhost:9200/_cluster/health 2>/dev/null || echo "")
if echo "$es_status" | grep -q '"status"'; then
pass "Elasticsearch cluster responding"
else
fail "TubeArchivist-Elasticsearch integration"
fail "Elasticsearch not responding"
fi
}
echo "Running integration tests..."
test_grafana_influxdb_integration || true
test_dockhand_docker_integration || true
test_homepage_discovery || true
test_tubearchivist_redis || true
test_tubearchivist_elasticsearch || true
test_network_isolation() {
check "Services are on the correct network"
local net_count
net_count=$(docker network inspect "${COMPOSE_NETWORK_NAME}" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | wc -w || echo "0")
if [[ "$net_count" -ge 14 ]]; then
pass "$net_count containers on ${COMPOSE_NETWORK_NAME}"
else
fail "Only $net_count containers on network (expected >= 14)"
fi
}
require_stack_running
echo "======================================"
echo "Integration Tests: Service Communication"
echo "======================================"
echo ""
test_grafana_influxdb_integration
test_dockhand_proxy_integration
test_homepage_discovery
test_tubearchivist_redis
test_tubearchivist_elasticsearch
test_network_isolation
echo ""
echo "===================================="
echo "Integration Test Results: $PASS passed, $FAIL failed"
echo "===================================="
echo "======================================"
echo "RESULTS: $PASS passed, $FAIL failed"
echo "======================================"
[[ $FAIL -eq 0 ]]

View File

@@ -1,30 +1,266 @@
#!/bin/bash
# Unit test: User ID detection accuracy
# Unit test: Environment and configuration validation
# These tests validate the project configuration without requiring Docker.
set -euo pipefail
test_uid_detection() {
local expected_uid
local expected_gid
local expected_docker_gid
expected_uid=$(id -u)
expected_gid=$(id -g)
expected_docker_gid=$(getent group docker | cut -d: -f3)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
TEMPLATE_FILE="$PROJECT_ROOT/docker-compose.yml.template"
ENV_TEMPLATE="$PROJECT_ROOT/demo.env.template"
# Simulate script detection
local detected_uid=$expected_uid
local detected_gid=$expected_gid
local detected_docker_gid=$expected_docker_gid
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
if [[ "$detected_uid" -eq "$expected_uid" &&
"$detected_gid" -eq "$expected_gid" &&
"$detected_docker_gid" -eq "$expected_docker_gid" ]]; then
echo "PASS: User detection accurate"
return 0
PASS=0
FAIL=0
pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((PASS++)) || true; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; ((FAIL++)) || true; }
check() { echo -e "${YELLOW}[CHECK]${NC} $1"; }
grep_exists() {
grep "$@" >/dev/null 2>&1 || true
}
test_template_exists() {
check "docker-compose.yml.template exists"
if [[ -f "$TEMPLATE_FILE" ]]; then
pass "Template file exists"
else
echo "FAIL: User detection inaccurate"
return 1
fail "Template file not found at $TEMPLATE_FILE"
fi
}
test_uid_detection
test_template_has_required_sections() {
check "Template has required top-level sections"
local sections=("networks:" "volumes:" "services:")
for section in "${sections[@]}"; do
if grep_exists "^$section" "$TEMPLATE_FILE"; then
pass "Template contains '$section' section"
else
fail "Template missing '$section' section"
fi
done
}
test_template_has_all_services() {
check "Template defines all 16 services"
local services=(
"docker-socket-proxy:" "homepage:" "pihole:" "dockhand:"
"influxdb:" "grafana:" "drawio:" "kroki:" "atomictracker:"
"archivebox:" "ta-redis:" "ta-elasticsearch:" "tubearchivist:"
"wakapi:" "mailhog:" "atuin:"
)
local found=0
for svc in "${services[@]}"; do
if grep_exists " ${svc}" "$TEMPLATE_FILE"; then
((found++)) || true
else
fail "Service not found in template: $svc"
fi
done
if [[ $found -eq ${#services[@]} ]]; then
pass "All ${#services[@]} services defined in template"
fi
}
test_all_services_have_healthchecks() {
check "All exposed services have healthcheck blocks"
local exposed_services=("homepage" "pihole" "dockhand" "influxdb" "grafana" "drawio" "kroki" "atomictracker" "archivebox" "tubearchivist" "wakapi" "mailhog" "atuin")
local missing=()
for svc in "${exposed_services[@]}"; do
local svc_block
svc_block=$(sed -n "/^ ${svc}:/,/^[^ ]/p" "$TEMPLATE_FILE" || true)
if echo "$svc_block" | grep_exists "healthcheck:"; then
:
else
missing+=("$svc")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
pass "All exposed services have health checks"
else
fail "Services missing health checks: ${missing[*]}"
fi
}
test_all_services_have_restart_policy() {
check "All services have restart policy"
local restart_count
restart_count=$(grep -c "restart:" "$TEMPLATE_FILE" || true)
if [[ $restart_count -ge 16 ]]; then
pass "$restart_count services have restart policies"
else
fail "Only $restart_count services have restart policies (expected >= 16)"
fi
}
test_all_services_have_labels() {
check "All user-facing services have Homepage labels"
local label_services=("homepage" "pihole" "dockhand" "influxdb" "grafana" "drawio" "kroki" "atomictracker" "archivebox" "tubearchivist" "wakapi" "mailhog" "atuin")
local missing=()
for svc in "${label_services[@]}"; do
local svc_block
svc_block=$(sed -n "/^ ${svc}:/,/^[^ ]/p" "$TEMPLATE_FILE" || true)
if echo "$svc_block" | grep_exists "homepage.group:"; then
:
else
missing+=("$svc")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
pass "All user-facing services have Homepage discovery labels"
else
fail "Services missing labels: ${missing[*]}"
fi
}
test_dockhand_uses_proxy() {
check "Dockhand connects through docker-socket-proxy"
local dockhand_block
dockhand_block=$(sed -n "/^ dockhand:/,/^[^ ]/p" "$TEMPLATE_FILE" || true)
if echo "$dockhand_block" | grep_exists "DOCKER_HOST=tcp://docker-socket-proxy:2375"; then
pass "Dockhand routes through socket proxy"
else
fail "Dockhand not configured to use socket proxy (security issue)"
fi
}
test_no_direct_socket_mounts_except_proxy() {
check "No direct Docker socket mounts except on socket-proxy"
local socket_lines
socket_lines=$(grep -n '/var/run/docker\.sock' "$TEMPLATE_FILE" || true)
local bad_mounts=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local line_num
line_num=$(echo "$line" | cut -d: -f1)
local context
context=$(head -n "$line_num" "$TEMPLATE_FILE" | grep "^ [a-z]" | tail -1 || true)
if [[ "$context" != *"docker-socket-proxy"* ]]; then
((bad_mounts++)) || true
fail "Direct socket mount found outside proxy at line $line_num"
fi
done <<< "$socket_lines"
if [[ $bad_mounts -eq 0 ]]; then
pass "Only docker-socket-proxy mounts the Docker socket"
fi
}
test_env_template_completeness() {
check "demo.env.template has all required variables"
local required_vars=(
"COMPOSE_PROJECT_NAME" "COMPOSE_NETWORK_NAME"
"DEMO_UID" "DEMO_GID" "DEMO_DOCKER_GID"
"HOMEPAGE_PORT" "PIHOLE_PORT" "DOCKHAND_PORT"
"INFLUXDB_PORT" "GRAFANA_PORT" "DRAWIO_PORT" "KROKI_PORT"
"ATOMIC_TRACKER_PORT" "ARCHIVEBOX_PORT" "TUBE_ARCHIVIST_PORT"
"WAKAPI_PORT" "MAILHOG_PORT" "MAILHOG_SMTP_PORT" "ATUIN_PORT"
"NETWORK_SUBNET" "NETWORK_GATEWAY"
"TA_USERNAME" "TA_PASSWORD" "ELASTIC_PASSWORD"
"GF_SECURITY_ADMIN_USER" "GF_SECURITY_ADMIN_PASSWORD"
"PIHOLE_WEBPASSWORD"
)
for var in "${required_vars[@]}"; do
if grep_exists "^${var}=" "$ENV_TEMPLATE"; then
pass "Env template has $var"
else
fail "Env template missing $var"
fi
done
}
test_env_template_port_range() {
check "All ports in env template are in 4000-4099 range"
local ports_out_of_range=()
while IFS='=' read -r var val; do
if [[ "$var" == *"_PORT" && "$val" =~ ^[0-9]+$ ]]; then
if [[ "$val" -lt 4000 || "$val" -gt 4099 ]]; then
ports_out_of_range+=("$var=$val")
fi
fi
done < "$ENV_TEMPLATE"
if [[ ${#ports_out_of_range[@]} -eq 0 ]]; then
pass "All ports within 4000-4099 range"
else
fail "Ports outside range: ${ports_out_of_range[*]}"
fi
}
test_homepage_configs_exist() {
check "Homepage configuration files exist"
local configs=("services.yaml" "widgets.yaml" "settings.yaml" "bookmarks.yaml" "docker.yaml")
for cfg in "${configs[@]}"; do
if [[ -f "$PROJECT_ROOT/config/homepage/$cfg" ]]; then
pass "Homepage config exists: $cfg"
else
fail "Homepage config missing: $cfg"
fi
done
}
test_grafana_configs_exist() {
check "Grafana configuration files exist"
local configs=("datasources.yml" "dashboards.yml" "dashboards/docker-overview.json")
for cfg in "${configs[@]}"; do
if [[ -f "$PROJECT_ROOT/config/grafana/$cfg" ]]; then
pass "Grafana config exists: $cfg"
else
fail "Grafana config missing: $cfg"
fi
done
}
test_scripts_exist() {
check "Deployment scripts exist"
local scripts=("scripts/demo-stack.sh" "scripts/demo-test.sh" "scripts/validate-all.sh")
for script in "${scripts[@]}"; do
if [[ -f "$PROJECT_ROOT/$script" ]]; then
pass "Script exists: $script"
else
fail "Script missing: $script"
fi
done
}
test_scripts_use_strict_mode() {
check "All scripts use strict mode (set -euo pipefail)"
local found_scripts
found_scripts=("$PROJECT_ROOT/scripts/"*.sh)
for script in "${found_scripts[@]}"; do
if head -5 "$script" | grep_exists "set -euo pipefail"; then
pass "$(basename "$script") uses strict mode"
else
fail "$(basename "$script") missing strict mode"
fi
done
}
echo "======================================"
echo "Unit Tests: Configuration Validation"
echo "======================================"
echo ""
test_template_exists
test_template_has_required_sections
test_template_has_all_services
test_all_services_have_healthchecks
test_all_services_have_restart_policy
test_all_services_have_labels
test_dockhand_uses_proxy
test_no_direct_socket_mounts_except_proxy
test_env_template_completeness
test_env_template_port_range
test_homepage_configs_exist
test_grafana_configs_exist
test_scripts_exist
test_scripts_use_strict_mode
echo ""
echo "======================================"
echo "RESULTS: $PASS passed, $FAIL failed"
echo "======================================"
[[ $FAIL -eq 0 ]]