diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 87bd910..f13dc6a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,8 +1,8 @@ name: CI on: - push: - branches: [ feature/* ] + pull_request: + branches: [ main ] workflow_dispatch: jobs: @@ -22,8 +22,11 @@ jobs: - name: Install Dependencies run: go mod tidy - - name: Build Binary - run: go build -o monitor + - name: Build and Install Monitor + run: | + go build -o monitor + sudo mv monitor /usr/local/bin/ + chmod +x /usr/local/bin/monitor - name: Install Docker and Docker Compose run: | @@ -43,11 +46,23 @@ jobs: sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - - name: Start Docker Containers for Testing + - name: Set Up SSH Config for CI + run: | + mkdir -p ~/.ssh + echo "Host localhost" >> ~/.ssh/config + echo " HostName localhost" >> ~/.ssh/config + echo " User root" >> ~/.ssh/config + echo " IdentityFile /tmp/ci_ssh_key" >> ~/.ssh/config + chmod 600 ~/.ssh/config + ssh-keygen -t rsa -b 2048 -f /tmp/ci_ssh_key -q -N "" + cat /tmp/ci_ssh_key.pub >> ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys + ssh-keyscan localhost >> ~/.ssh/known_hosts + + - name: Start Local Docker Containers for Testing run: docker compose -f docker-compose.yml up -d - - name: Wait for Containers to Initialize + - name: Wait for Local Containers to Initialize run: sleep 10 # Ensure services are fully started - name: Run Installation Script @@ -62,6 +77,5 @@ jobs: echo "πŸ”„ Testing monitor --state" monitor state || { echo "❌ monitor state failed"; exit 1; } - - name: Stop and Clean Up Docker Containers run: docker compose down diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 4893219..95e3857 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,7 +1,7 @@ name: CI/CD on: - pull_request: + push: branches: [ main ] workflow_dispatch: @@ -22,8 +22,11 @@ jobs: - name: Install Dependencies run: go mod tidy - - name: Build Binary - run: go build -o monitor + - name: Build and Install Monitor + run: | + go build -o monitor + sudo mv monitor /usr/local/bin/ + chmod +x /usr/local/bin/monitor - name: Install Docker and Docker Compose run: | @@ -43,55 +46,68 @@ jobs: sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - - name: Start Docker Containers for Testing + - name: Set Up SSH Config for CI + run: | + mkdir -p ~/.ssh + echo "Host localhost" >> ~/.ssh/config + echo " HostName localhost" >> ~/.ssh/config + echo " User root" >> ~/.ssh/config + echo " IdentityFile /tmp/ci_ssh_key" >> ~/.ssh/config + chmod 600 ~/.ssh/config + ssh-keygen -t rsa -b 2048 -f /tmp/ci_ssh_key -q -N "" + cat /tmp/ci_ssh_key.pub >> ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys + ssh-keyscan localhost >> ~/.ssh/known_hosts + + - name: Start Local Docker Containers for Testing run: docker compose -f docker-compose.yml up -d - - name: Wait for Containers to Initialize + - name: Wait for Local Containers to Initialize run: sleep 10 # Ensure services are fully started - name: Run Installation Script run: | python3 install.py - - name: Stop and Clean Up Docker Containers - run: docker compose down - - name: Verify Monitor Commands run: | echo "πŸ”„ Testing monitor --service" - monitor --service || { echo "❌ monitor --service failed"; exit 1; } + monitor service || { echo "❌ monitor service failed"; exit 1; } echo "πŸ”„ Testing monitor --state" - monitor --state || { echo "❌ monitor --state failed"; exit 1; } + monitor state || { echo "❌ monitor state failed"; exit 1; } + + - name: Stop and Clean Up Docker Containers + run: docker compose down - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: monitor-binary - path: monitor + path: /usr/local/bin/monitor release: name: Create GitHub Release needs: build runs-on: ubuntu-latest + # Release tylko przy pushu do main + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} steps: - name: Checkout Repository uses: actions/checkout@v3 - name: Download Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: monitor-binary - name: Create GitHub Release uses: softprops/action-gh-release@v1 - if: github.ref == 'refs/heads/main' with: tag_name: v1.0.${{ github.run_number }} name: "Release v1.0.${{ github.run_number }}" body: "Automated release for GO Container Monitor" files: monitor env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.MONITOR_TOKEN_CICD }} diff --git a/README.md b/README.md index ec52ba9..7763faa 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,440 @@ -# Docker_Container_monitorπŸš€ +# 🐳 Docker Container Monitor -## CI/CD Status πŸš€ +[![CI/CD Status](https://github.com/Kobeep/Docker_Container_monitor/actions/workflows/CICD.yml/badge.svg)](https://github.com/Kobeep/Docker_Container_monitor/actions) +[![Go Version](https://img.shields.io/badge/Go-1.22+-00ADD8?style=flat&logo=go)](https://golang.org/) +[![License](https://img.shields.io/badge/License-GPL-blue.svg)](LICENSE) -![GitHub Workflow Status](https://github.com/Kobeep/Docker_Container_monitor/actions/workflows/go-container-monitor.yml/badge.svg) +> A powerful, production-ready CLI tool for monitoring Docker containers, tracking resource usage, and managing containerized applications across local and remote hosts. -## Overview +**Docker Container Monitor** is designed for DevOps engineers and developers who need comprehensive insights into their containerized infrastructure. With features like continuous monitoring, resource statistics, service health checks, and remote SSH support, it's an essential tool for container management. -`Docker_Container_monitor` is a lightweight CLI tool written in `Go` that helps monitor running Docker containers and their services. It provides real-time information about container states and checks if the services inside the containers are available. The project includes a `Python` installer script to automate the setup process. +--- -## Features +## ✨ Features -- βœ… **Automatic container detection** - No need to manually specify container names. -- βœ… **Service health check** - Verifies if the services inside containers are accessible. -- βœ… **Multiple output modes** - Choose between full status, container state, or service availability. -- βœ… **Simple CLI commands** - Use `monitor` to get an instant overview. -- βœ… **Systemd integration** - Runs as a background service to keep monitoring automatically. -- βœ… **Easy installation** - Fully automated setup with `install.py`. +| Feature | Description | +|---------|-------------| +| ⏰ **Watch Mode** | Continuous auto-refresh monitoring - no more manual re-runs! | +| πŸ“Š **Resource Statistics** | Real-time CPU, memory, and network usage with color-coded warnings | +| πŸ” **Container Filtering** | Focus on specific containers by name, status, or labels | +| πŸ₯ **Service Health Checks** | Automatic HTTP endpoint probing for service availability | +| πŸ“œ **Log Streaming** | Built-in container log viewer with follow mode | +| 🌐 **Remote Monitoring** | Monitor Docker hosts over SSH using config aliases | +| πŸ“‘ **Docker Events** | Subscribe to real-time Docker lifecycle events | +| πŸ”§ **Multiple Output Formats** | Human-readable or JSON for easy scripting | +| βš™οΈ **Systemd Integration** | Optional background service for continuous monitoring | +| 🎨 **Color-Coded Output** | Visual indicators for quick status recognition | -## Installation +--- -### Prerequisites +## πŸ“‹ Table of Contents + +- [Installation](#-installation) +- [Quick Start](#-quick-start) +- [Usage Examples](#-usage-examples) +- [Features in Detail](#-features-in-detail) +- [Configuration](#-configuration) +- [Systemd Service](#-systemd-service) +- [Uninstallation](#-uninstallation) +- [Development](#-development) +- [Contributing](#-contributing) +- [License](#-license) + +--- + +## πŸš€ Installation -- 🐳 Docker installed and running -- 🐍 Python3 installed -- 🦫 Go installed (if not, the installer will install it automatically) +### Prerequisites -### Steps to Install +- **Docker** (20.10+) - Required for container monitoring +- **Python 3.6+** - For the installation script +- **Go 1.22+** - Automatically installed if missing +- **SSH** (optional) - For remote monitoring -1. **Clone the repository**: +### Automated Installation -```sh -git clone https://github.com/yourusername/Docker_Container_monitor.git +```bash +# Clone the repository +git clone https://github.com/Kobeep/Docker_Container_monitor.git cd Docker_Container_monitor + +# Run the installer (automatically handles dependencies) +python3 install.py + +# Verify installation +monitor --version ``` -2. **Run the installation script:** +The installer will: +- βœ… Check and install Go if needed (Fedora/Ubuntu/Debian/RHEL/Arch supported) +- βœ… Verify Docker installation and version +- βœ… Build the monitor binary +- βœ… Install to `/usr/local/bin` +- βœ… Optionally set up systemd service -```sh -python3 install.py +**Installation Options:** + +```bash +python3 install.py --help # Show all options +python3 install.py --no-systemd # Skip systemd service setup +python3 install.py --uninstall # Remove installation ``` -3. **Verify installation:** +--- + +## 🎯 Quick Start + +Once installed, start monitoring immediately: + +```bash +# Basic container status +monitor + +# Watch mode with 3-second refresh +monitor watch --interval 3 + +# Show resource usage statistics +monitor stats -```sh -monitor --help +# Filter specific containers +monitor --filter "name=nginx" + +# Stream container logs +monitor logs --follow ``` -## Usage -### Display full container and service status: +--- + +## πŸ“– Usage Examples + +### Basic Monitoring + +**Full status overview:** -```sh +```bash monitor ``` -### Display only container states: +Shows containers, their states, exposed ports, and service health. -```sh +![Full Status](./readme/monitor.png) + +**Container states only:** + +```bash monitor state ``` -### Display only service availability: +![Container States](./readme/monitor-state.png) -```sh +**Service availability check:** + +```bash monitor service ``` -### Check systemd service status: +![Service Check](./readme/monitor-service.png) + +### Watch Mode (New!) + +Continuous monitoring with auto-refresh: + +```bash +# Refresh every 2 seconds (default) +monitor watch + +# Custom interval +monitor watch --interval 5 + +# Exit with Ctrl+C +``` + +### Resource Statistics (New!) -```sh +View CPU, memory, and network usage: + +```bash +monitor stats +``` + +**Output includes:** +- πŸ”΄ Red: High resource usage (>80%) +- 🟑 Yellow: Medium usage (50-80%) +- 🟒 Green: Normal usage (<50%) + +### Container Filtering (New!) + +Focus on specific containers: + +```bash +# Filter by name +monitor --filter "name=nginx" + +# Filter by status +monitor --filter "status=running" + +# Filter by label +monitor --filter "label=env=production" +``` + +### Log Streaming (New!) + +Built-in log viewer: + +```bash +# View last 100 lines +monitor logs + +# Follow mode (like docker logs -f) +monitor logs --follow + +# Limit lines +monitor logs --tail 50 +``` + +### Remote Monitoring + +Monitor Docker on remote hosts via SSH: + +```bash +# Using SSH config alias +monitor remote --host production-server + +# The tool uses your ~/.ssh/config for authentication +``` + +### Docker Events + +Real-time event monitoring: + +```bash +# Human-readable format +monitor events + +# JSON output for scripting +monitor events --json +``` + +--- + +## πŸ”§ Features in Detail + +### How It Works + +1. **Container Discovery**: Uses Docker API to list all running containers +2. **Port Detection**: Identifies exposed ports and their protocols +3. **Service Health**: Sends HTTP requests to verify service availability +4. **Resource Tracking**: Collects CPU, memory, and network metrics via Docker stats +5. **Event Subscription**: Listens to Docker daemon events for real-time updates + +### Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CLI Layer β”‚ (urfave/cli) +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Monitor Logic β”‚ (Go routines for concurrent checks) +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Docker Client β”‚ (Docker API v25.0+) +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ SSH Transport β”‚ (Optional remote monitoring) +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Output Modes + +| Mode | Command | Description | +|------|---------|-------------| +| Full | `monitor` | All information (default) | +| State | `monitor state` | Container states only | +| Service | `monitor service` | Service availability only | +| Watch | `monitor watch` | Continuous monitoring | +| Stats | `monitor stats` | Resource usage | +| Events | `monitor events` | Real-time Docker events | +| Logs | `monitor logs` | Container log streaming | + +--- + +## βš™οΈ Configuration + +### SSH Configuration + +For remote monitoring, set up `~/.ssh/config`: + +```ssh-config +Host production + HostName prod.example.com + User admin + IdentityFile ~/.ssh/prod_key + Port 22 +``` + +Then use: + +```bash +monitor remote --host production +``` + +### Environment Variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `DOCKER_HOST` | Docker daemon socket | `unix:///var/run/docker.sock` | +| `DOCKER_API_VERSION` | API version | Auto-detected | + +--- + +## πŸ”„ Systemd Service + +### Setup Service + +During installation, you can enable systemd integration: + +```bash +python3 install.py # Will prompt for systemd setup +``` + +Or manually: + +```bash +sudo systemctl enable monitor +sudo systemctl start monitor +``` + +### Check Service Status + +```bash systemctl status monitor + +# View logs +journalctl -u monitor -f ``` -## How It Works -πŸš€ **Retrieves a list of running Docker containers** using `docker ps` -πŸ”Œ **Gets exposed ports** for each container -🌐 **Attempts an HTTP request** to determine if the service inside the container is responsive -πŸ“Š **Displays results** based on the selected mode +### Service Configuration + +The service runs in background mode and logs to systemd journal. Edit the service file: + +```bash +sudo systemctl edit monitor +``` + +--- + +## πŸ—‘οΈ Uninstallation + +### Using Installer -## Uninstallation +```bash +python3 install.py --uninstall +``` + +### Manual Removal -To remove the tool: -```sh +```bash +# Stop and disable service sudo systemctl stop monitor sudo systemctl disable monitor + +# Remove files sudo rm /usr/local/bin/monitor sudo rm /etc/systemd/system/monitor.service +sudo systemctl daemon-reload + +# Remove repository rm -rf ~/Docker_Container_monitor ``` -## Contributing -πŸ’‘ Contributions are welcome! Feel free to submit a pull request or open an issue. +--- + +## πŸ› οΈ Development + +### Building from Source + +```bash +# Install dependencies +go mod download + +# Build binary +go build -o monitor monitor.go + +# Run tests +go test ./... + +# Install locally +sudo cp monitor /usr/local/bin/ +``` + +### Project Structure + +``` +Docker_Container_monitor/ +β”œβ”€β”€ monitor.go # Main application +β”œβ”€β”€ install.py # Installation script +β”œβ”€β”€ go.mod # Go dependencies +β”œβ”€β”€ go.sum # Dependency checksums +β”œβ”€β”€ README.md # This file +β”œβ”€β”€ LICENSE # GPL License +└── readme/ # Screenshots + β”œβ”€β”€ monitor.png + β”œβ”€β”€ monitor-state.png + └── monitor-service.png +``` + +### Dependencies + +- `github.com/docker/docker` - Docker Engine API +- `github.com/urfave/cli/v2` - CLI framework +- `github.com/fatih/color` - Colored output +- `golang.org/x/crypto/ssh` - SSH client +- `github.com/kevinburke/ssh_config` - SSH config parsing + +--- + +## 🀝 Contributing + +Contributions are welcome! Here's how you can help: + +1. **Fork the repository** +2. **Create a feature branch**: `git checkout -b feature/amazing-feature` +3. **Commit your changes**: `git commit -m 'Add amazing feature'` +4. **Push to the branch**: `git push origin feature/amazing-feature` +5. **Open a Pull Request** + +### Guidelines + +- Follow Go best practices and conventions +- Add tests for new features +- Update documentation as needed +- Keep commits atomic and well-described + +--- + +## πŸ“„ License + +This project is licensed under the **GNU General Public License v3.0** - see the [LICENSE](LICENSE) file for details. + +--- + +## οΏ½ Author + +**Jakub Pospieszny** + +- GitHub: [@Kobeep](https://github.com/Kobeep) +- Project: [Docker Container Monitor](https://github.com/Kobeep/Docker_Container_monitor) + +--- -## License +## 🌟 Acknowledgments -πŸ“œ This project is licensed under the `GNU GPL License`. +Built with: +- [Go](https://golang.org/) - Programming language +- [Docker Engine API](https://docs.docker.com/engine/api/) - Container management +- [urfave/cli](https://github.com/urfave/cli) - CLI framework -## Author +--- -πŸ‘¨β€πŸ’» **Author:** Jakub Pospieszny +
-## GitHub +**⭐ Star this repository if you find it helpful!** -πŸ“Œ **GitHub:** [Kobeep](https://github.com/Kobeep) +
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1427b47 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policy + +## Supported Versions + +We currently support security updates for the following versions of Docker Container Monitor: + +| Version | Supported | +| ------- | --------------------- | +| 1.x | :white_check_mark: | +| < 1.x | :x: | + +It is highly recommended to use the latest release to ensure that security updates are applied. + +## Reporting a Vulnerability + +If you discover a security vulnerability in Docker Container Monitor, please follow these steps: + +1. **Do not disclose the vulnerability publicly.** +2. **Report the vulnerability privately** by sending an email to: [kuba.pospieszny@gmail.com](mailto:kuba.pospieszny@gmail) (replace with your designated security contact). +3. **Provide as much detail as possible** about the vulnerability, including steps to reproduce and potential impact. +4. We aim to acknowledge your report within 5 business days and will keep you updated on the progress. +5. Once a fix is developed, we will release a security update and notify you accordingly. + +Thank you for helping to keep Docker Container Monitor secure! + diff --git a/docker-compose.yml b/docker-compose.yml index f027920..4839f24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: ports: - "8082:8000" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000"] + test: ["CMD", "curl", "-f", "http://localhost:8082"] interval: 10s retries: 3 web3: @@ -28,6 +28,6 @@ services: ports: - "8081:8000" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000"] + test: ["CMD", "curl", "-f", "http://localhost:8083"] interval: 10s retries: 3 diff --git a/go.mod b/go.mod index 414c59c..3b473b0 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,45 @@ -module monitor.go +module monitor go 1.22.10 -require github.com/urfave/cli/v2 v2.27.5 +require ( + github.com/docker/docker v25.0.6+incompatible + github.com/fatih/color v1.18.0 + github.com/kevinburke/ssh_config v1.2.0 + github.com/urfave/cli/v2 v2.27.5 + golang.org/x/crypto v0.32.0 +) require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/time v0.10.0 // indirect + gotest.tools/v3 v3.5.2 // indirect ) + +replace github.com/docker/distribution => github.com/docker/distribution v2.7.1+incompatible diff --git a/go.sum b/go.sum index c8b6c7e..033606e 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,146 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg= +github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/install.py b/install.py index 1569e8e..0cdbc84 100755 --- a/install.py +++ b/install.py @@ -1,82 +1,398 @@ +#!/usr/bin/env python3 +""" +Docker Container Monitor Installation Script +Installs the monitor tool and optionally sets up systemd service +""" import os import time import sys import subprocess +import threading +import shutil +import argparse +from pathlib import Path + +# Color codes for better output +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + +def print_color(text, color): + """Print colored text""" + print(f"{color}{text}{Colors.ENDC}") -# Loading animation -def loading_animation(text): - for _ in range(5): - sys.stdout.write(f"\r{text} [ {'-' * (_ % 4)} ]") +def spinner_animation(text, stop_event): + """Animated spinner for long-running operations""" + spinner_chars = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'] + idx = 0 + while not stop_event.is_set(): + sys.stdout.write(f"\r{Colors.CYAN}{text} {spinner_chars[idx % len(spinner_chars)]}{Colors.ENDC}") sys.stdout.flush() - time.sleep(0.5) - print("\rβœ… " + text + " complete!") - -# Check if Go is installed -loading_animation("Checking Go installation") -if subprocess.run(["which", "go"], capture_output=True).returncode != 0: - print("πŸ” Go is not installed, installing...") - os.system("sudo dnf install -y golang") - -# Ensure monitor.go exists -if not os.path.exists("monitor.go"): - print("❌ Error: monitor.go not found!") - sys.exit(1) - -# Copy monitor.go and go.mod to /tmp -loading_animation("Copying Go source code") -os.system("mkdir -p /tmp/monitor_build && cp monitor.go go.mod /tmp/monitor_build/") - -# Initialize Go modules in /tmp -loading_animation("Initializing Go modules") -os.system("cd /tmp/monitor_build && go mod tidy") - -# Install dependencies (Ensure CLI package is installed) -loading_animation("Installing Go dependencies") -os.system("cd /tmp/monitor_build && go get github.com/urfave/cli/v2") - -# Compile `monitor.go` -loading_animation("Compiling Go application") -compile_status = os.system("cd /tmp/monitor_build && go build -o monitor monitor.go") -if compile_status != 0: - print("❌ Error: Failed to compile monitor.go") - sys.exit(1) - -# Move binary to `/usr/local/bin` -loading_animation("Installing monitor command") -os.system("sudo mv /tmp/monitor_build/monitor /usr/local/bin/") -os.system("sudo chmod +x /usr/local/bin/monitor") - -# Verify installation -if not os.path.exists("/usr/local/bin/monitor"): - print("❌ Error: monitor binary not found in /usr/local/bin/") - sys.exit(1) - -# Create systemd service -loading_animation("Setting up systemd service") -service_config = """ -[Unit] -Description=Monitor Docker containers and services + idx += 1 + time.sleep(0.1) + sys.stdout.write("\r" + " " * (len(text) + 4) + "\r") + sys.stdout.flush() + +def run_with_spinner(text, command_func): + """Run a command with a spinner animation""" + stop_spinner = threading.Event() + spinner_thread = threading.Thread(target=spinner_animation, args=(text, stop_spinner)) + spinner_thread.start() + + try: + result = command_func() + stop_spinner.set() + spinner_thread.join() + print_color(f"βœ… {text}", Colors.GREEN) + return result + except Exception as e: + stop_spinner.set() + spinner_thread.join() + print_color(f"❌ {text} failed: {e}", Colors.RED) + raise + +def check_requirements(): + """Check if required tools are installed""" + print_color("\nπŸ” Checking requirements...", Colors.HEADER) + + # Check if running on Linux + if sys.platform != 'linux': + print_color(f"⚠️ Warning: This script is designed for Linux. Current OS: {sys.platform}", Colors.YELLOW) + + # Check Docker + if shutil.which('docker') is None: + print_color("❌ Docker is not installed. Please install Docker first.", Colors.RED) + sys.exit(1) + else: + print_color("βœ… Docker found", Colors.GREEN) + + # Check Python version + if sys.version_info < (3, 6): + print_color(f"❌ Python 3.6+ required. Current: {sys.version}", Colors.RED) + sys.exit(1) + + return True + +def check_go_installation(): + """Check if Go is installed, install if necessary""" + print_color("\nπŸ“¦ Checking Go installation...", Colors.HEADER) + + if shutil.which('go') is None: + print_color("⚠️ Go not found. Installing Go...", Colors.YELLOW) + + # Detect package manager + if shutil.which('dnf'): + cmd = "sudo dnf install -y golang" + elif shutil.which('apt'): + cmd = "sudo apt update && sudo apt install -y golang" + elif shutil.which('yum'): + cmd = "sudo yum install -y golang" + elif shutil.which('pacman'): + cmd = "sudo pacman -S --noconfirm go" + else: + print_color("❌ Unable to detect package manager. Please install Go manually.", Colors.RED) + sys.exit(1) + + if os.system(cmd) != 0: + print_color("❌ Failed to install Go", Colors.RED) + sys.exit(1) + + print_color("βœ… Go installed successfully", Colors.GREEN) + else: + go_version = subprocess.run(['go', 'version'], capture_output=True, text=True).stdout.strip() + print_color(f"βœ… Go already installed: {go_version}", Colors.GREEN) + + return True +def remove_existing_service(): + """ + Checks if monitor.service exists in systemd and removes it if present + """ + print_color("\nπŸ” Checking for existing installation...", Colors.HEADER) + + try: + result = subprocess.run( + ["systemctl", "list-unit-files", "monitor.service"], + capture_output=True, + text=True, + check=True + ) + + if "monitor.service" not in result.stdout: + print_color("βœ… No existing service found", Colors.GREEN) + return + + print_color("⚠️ Existing monitor.service found. Removing...", Colors.YELLOW) + + def remove_service(): + commands = [ + ("Stopping service", "sudo systemctl stop monitor"), + ("Disabling service", "sudo systemctl disable monitor"), + ("Removing binary", "sudo rm -f /usr/local/bin/monitor"), + ("Removing service file", "sudo rm -f /etc/systemd/system/monitor.service"), + ("Reloading systemd", "sudo systemctl daemon-reload") + ] + + for desc, cmd in commands: + try: + subprocess.run(cmd, shell=True, check=False, capture_output=True) + print_color(f" βœ“ {desc}", Colors.GREEN) + except Exception as e: + print_color(f" ⚠️ {desc} (non-fatal): {e}", Colors.YELLOW) + + remove_service() + print_color("βœ… Previous installation removed", Colors.GREEN) + + except subprocess.CalledProcessError as e: + print_color(f"⚠️ Error checking service (continuing anyway): {e}", Colors.YELLOW) + except Exception as e: + print_color(f"⚠️ Unexpected error (continuing anyway): {e}", Colors.YELLOW) + +def build_monitor(): + """Build the monitor binary""" + print_color("\nπŸ”¨ Building monitor...", Colors.HEADER) + + # Check if source files exist + if not os.path.exists("monitor.go"): + print_color("❌ Error: monitor.go not found in current directory!", Colors.RED) + sys.exit(1) + + if not os.path.exists("go.mod"): + print_color("❌ Error: go.mod not found in current directory!", Colors.RED) + sys.exit(1) + + # Create temporary build directory + build_dir = "/tmp/monitor_build" + os.makedirs(build_dir, exist_ok=True) + + def build(): + # Copy source files + print_color(" πŸ“„ Copying source files...", Colors.CYAN) + shutil.copy2("monitor.go", f"{build_dir}/monitor.go") + shutil.copy2("go.mod", f"{build_dir}/go.mod") + if os.path.exists("go.sum"): + shutil.copy2("go.sum", f"{build_dir}/go.sum") + + # Download dependencies + print_color(" πŸ“¦ Downloading dependencies...", Colors.CYAN) + result = subprocess.run( + "go mod download", + shell=True, + cwd=build_dir, + capture_output=True, + text=True + ) + if result.returncode != 0: + raise Exception(f"Failed to download dependencies: {result.stderr}") + + # Build + print_color(" πŸ”§ Compiling...", Colors.CYAN) + result = subprocess.run( + "go build -o monitor monitor.go", + shell=True, + cwd=build_dir, + capture_output=True, + text=True + ) + if result.returncode != 0: + raise Exception(f"Compilation failed: {result.stderr}") + + return f"{build_dir}/monitor" + + try: + binary_path = build() + print_color("βœ… Build successful", Colors.GREEN) + return binary_path + except Exception as e: + print_color(f"❌ Build failed: {e}", Colors.RED) + sys.exit(1) + +def install_binary(binary_path): + """Install the binary to /usr/local/bin""" + print_color("\nπŸ“₯ Installing binary...", Colors.HEADER) + + install_path = "/usr/local/bin/monitor" + + try: + # Copy binary + result = subprocess.run( + f"sudo cp {binary_path} {install_path}", + shell=True, + capture_output=True, + text=True + ) + if result.returncode != 0: + raise Exception(f"Failed to copy binary: {result.stderr}") + + # Make executable + result = subprocess.run( + f"sudo chmod +x {install_path}", + shell=True, + capture_output=True, + text=True + ) + if result.returncode != 0: + raise Exception(f"Failed to set permissions: {result.stderr}") + + # Verify installation + if not os.path.exists(install_path): + raise Exception(f"Binary not found at {install_path}") + + print_color(f"βœ… Binary installed to {install_path}", Colors.GREEN) + return True + + except Exception as e: + print_color(f"❌ Installation failed: {e}", Colors.RED) + sys.exit(1) + +def setup_systemd_service(): + """Set up systemd service (optional)""" + print_color("\nβš™οΈ Setting up systemd service...", Colors.HEADER) + + service_config = """[Unit] +Description=Docker Container Monitor +Documentation=https://github.com/Kobeep/Docker_Container_monitor After=network.target docker.service +Wants=docker.service [Service] -ExecStart=/usr/local/bin/monitor -Restart=always +Type=simple +ExecStart=/usr/local/bin/monitor watch --interval 10 +Restart=on-failure +RestartSec=10 User=root +StandardOutput=journal +StandardError=journal [Install] WantedBy=multi-user.target """ -with open("/tmp/monitor.service", "w") as f: - f.write(service_config) + try: + # Write service file + service_path = "/tmp/monitor.service" + with open(service_path, "w") as f: + f.write(service_config) + + # Install service + subprocess.run("sudo cp /tmp/monitor.service /etc/systemd/system/monitor.service", + shell=True, check=True, capture_output=True) + subprocess.run("sudo systemctl daemon-reload", + shell=True, check=True, capture_output=True) + subprocess.run("sudo systemctl enable monitor", + shell=True, check=True, capture_output=True) + subprocess.run("sudo systemctl start monitor", + shell=True, check=True, capture_output=True) + + print_color("βœ… Systemd service configured and started", Colors.GREEN) + print_color(" ℹ️ Service runs: monitor watch --interval 10", Colors.CYAN) + print_color(" ℹ️ Check status: sudo systemctl status monitor", Colors.CYAN) + + except Exception as e: + print_color(f"❌ Systemd setup failed: {e}", Colors.RED) + print_color(" ℹ️ You can still use the command-line tool", Colors.YELLOW) + +def print_usage(): + """Print usage instructions""" + print_color("\n" + "="*70, Colors.BLUE) + print_color("πŸŽ‰ Installation Complete!", Colors.HEADER + Colors.BOLD) + print_color("="*70, Colors.BLUE) + + print_color("\nπŸ“– Usage Examples:", Colors.HEADER) + print_color(" monitor # Full status", Colors.CYAN) + print_color(" monitor state # Container states only", Colors.CYAN) + print_color(" monitor service # Service health checks", Colors.CYAN) + print_color(" monitor stats # Resource usage (NEW!)", Colors.CYAN) + print_color(" monitor watch --interval 5 # Auto-refresh every 5s (NEW!)", Colors.CYAN) + print_color(" monitor logs # Stream container logs (NEW!)", Colors.CYAN) + print_color(" monitor --filter 'name=nginx' # Filter containers (NEW!)", Colors.CYAN) + print_color(" monitor remote --host # Monitor remote host", Colors.CYAN) + print_color(" monitor events # Watch Docker events", Colors.CYAN) + print_color(" monitor --help # Show all options", Colors.CYAN) + + print_color("\nπŸ”§ System Commands:", Colors.HEADER) + print_color(" sudo systemctl status monitor # Check service status", Colors.CYAN) + print_color(" sudo systemctl stop monitor # Stop service", Colors.CYAN) + print_color(" sudo systemctl start monitor # Start service", Colors.CYAN) + print_color(" sudo systemctl restart monitor # Restart service", Colors.CYAN) + + print_color("\nπŸ“š Documentation:", Colors.HEADER) + print_color(" GitHub: https://github.com/Kobeep/Docker_Container_monitor", Colors.CYAN) + print_color("="*70 + "\n", Colors.BLUE) + +def main(): + """Main installation flow""" + parser = argparse.ArgumentParser( + description='Install Docker Container Monitor', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python3 install.py # Full installation with systemd + python3 install.py --no-systemd # Install binary only + python3 install.py --uninstall # Uninstall everything + ''' + ) + parser.add_argument('--no-systemd', action='store_true', + help='Skip systemd service installation') + parser.add_argument('--uninstall', action='store_true', + help='Uninstall monitor and service') + + args = parser.parse_args() + + print_color("\n" + "="*70, Colors.BLUE) + print_color("🐳 Docker Container Monitor - Installation Script", Colors.HEADER + Colors.BOLD) + print_color("="*70 + "\n", Colors.BLUE) + + if args.uninstall: + print_color("πŸ—‘οΈ Uninstalling...", Colors.YELLOW) + remove_existing_service() + print_color("\nβœ… Uninstallation complete!", Colors.GREEN) + return + + try: + # Check requirements + check_requirements() + + # Check/install Go + check_go_installation() + + # Remove existing installation + remove_existing_service() + + # Build binary + binary_path = build_monitor() + + # Install binary + install_binary(binary_path) + + # Setup systemd (optional) + if not args.no_systemd: + response = input(f"\n{Colors.YELLOW}Do you want to set up systemd service? (Y/n): {Colors.ENDC}").strip().lower() + if response in ['', 'y', 'yes']: + setup_systemd_service() + else: + print_color("⏭️ Skipping systemd service setup", Colors.YELLOW) + else: + print_color("⏭️ Systemd service setup skipped (--no-systemd flag)", Colors.YELLOW) + + # Print usage instructions + print_usage() + + # Cleanup + print_color("🧹 Cleaning up temporary files...", Colors.CYAN) + shutil.rmtree("/tmp/monitor_build", ignore_errors=True) -os.system("sudo mv /tmp/monitor.service /etc/systemd/system/monitor.service") -os.system("sudo systemctl daemon-reload") -os.system("sudo systemctl enable monitor") -os.system("sudo systemctl start monitor") + except KeyboardInterrupt: + print_color("\n\n⚠️ Installation cancelled by user", Colors.YELLOW) + sys.exit(1) + except Exception as e: + print_color(f"\n\n❌ Installation failed: {e}", Colors.RED) + sys.exit(1) -# Final message -print("\nπŸŽ‰ Installation complete! Use:") -print(" βœ… `monitor` β†’ Full container and service status") -print(" βœ… `monitor state` β†’ Displays only container names and states") -print(" βœ… `monitor service` β†’ Displays only service availability") +if __name__ == "__main__": + main() diff --git a/monitor b/monitor new file mode 100755 index 0000000..a00c519 Binary files /dev/null and b/monitor differ diff --git a/monitor.go b/monitor.go index a6badb9..c5826b8 100644 --- a/monitor.go +++ b/monitor.go @@ -1,179 +1,780 @@ package main import ( + "bytes" + "context" + "encoding/json" "fmt" - "log" "net/http" "os" "os/exec" + "os/signal" + "runtime" "strings" + "sync" + "syscall" "time" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/fatih/color" + "github.com/kevinburke/ssh_config" "github.com/urfave/cli/v2" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" ) -// Retrieves a list of running Docker containers -func getRunningContainers() []string { - out, err := exec.Command("docker", "ps", "--format", "{{.Names}}").Output() +type ServiceCheckResult struct { + Container string `json:"container"` + Port string `json:"port"` + Status string `json:"status"` +} + +// ContainerStat represents resource usage statistics for a container. +type ContainerStat struct { + Name string `json:"name"` + CPUPercent float64 `json:"cpu_percent"` + MemoryUsage uint64 `json:"memory_usage"` + MemoryLimit uint64 `json:"memory_limit"` + MemoryPercent float64 `json:"memory_percent"` + NetworkRx uint64 `json:"network_rx"` + NetworkTx uint64 `json:"network_tx"` +} + +func main() { + app := &cli.App{ + Name: "monitor", + Usage: "Monitor Docker containers, services and events (local and remote)", + Version: "1.1.0", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "json", + Usage: "Output in JSON format", + }, + &cli.StringFlag{ + Name: "filter", + Aliases: []string{"f"}, + Usage: "Filter containers (e.g., 'name=nginx' or 'status=running')", + }, + }, + Commands: []*cli.Command{ + { + Name: "state", + Usage: "Show container names and states", + Action: stateOnly, + }, + { + Name: "service", + Usage: "Show service statuses", + Action: serviceOnly, + }, + { + Name: "remote", + Usage: "Monitor remote Docker via SSH", + Description: `Connect to a remote Docker host via SSH. +Options: +- Use SSH config: monitor remote --host +- Use manual: monitor remote @ -i `, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "host", + Usage: "SSH config alias", + }, + &cli.StringFlag{ + Name: "i", + Usage: "Path to SSH private key", + }, + }, + Action: remoteStatus, + }, + { + Name: "events", + Usage: "Monitor Docker events in real time", + Action: dockerEvents, + }, + { + Name: "watch", + Usage: "Continuously monitor containers with auto-refresh", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "interval", + Aliases: []string{"n"}, + Value: 5, + Usage: "Refresh interval in seconds", + }, + &cli.BoolFlag{ + Name: "no-clear", + Usage: "Don't clear screen between updates", + }, + }, + Action: watchMode, + }, + { + Name: "stats", + Usage: "Display container resource usage statistics", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "no-stream", + Usage: "Disable streaming stats and only pull the first result", + }, + }, + Action: showStats, + }, + { + Name: "logs", + Usage: "Stream logs from a container", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "tail", + Aliases: []string{"n"}, + Value: 100, + Usage: "Number of lines to show from the end of the logs", + }, + &cli.BoolFlag{ + Name: "follow", + Aliases: []string{"f"}, + Usage: "Follow log output", + }, + }, + Action: containerLogs, + }, + }, + Action: fullStatus, + } + + if err := app.Run(os.Args); err != nil { + color.Red("❌ Error: %v", err) + os.Exit(1) + } +} + +// fullStatus displays full local Docker container and service status. +func fullStatus(c *cli.Context) error { + useJSON := c.Bool("json") + filter := c.String("filter") + if !useJSON { + color.Cyan("Checking local Docker containers and services...") + } + return executeLocalDockerStatus(c.Context, []string{}, useJSON, filter) +} + +// stateOnly displays only container states. +func stateOnly(c *cli.Context) error { + useJSON := c.Bool("json") + filter := c.String("filter") + if !useJSON { + color.Cyan("Checking local container states...") + } + return executeLocalDockerStatus(c.Context, []string{"--format", "πŸ“‚ {{.Names}}: πŸ”Ή {{.Status}}"}, useJSON, filter) +} + +// serviceOnly checks local service availability. +func serviceOnly(c *cli.Context) error { + useJSON := c.Bool("json") + if !useJSON { + color.Cyan("Checking local service availability...") + } + return executeLocalServiceCheck(c.Context, useJSON) +} + +// remoteStatus connects to a remote Docker host via SSH. +func remoteStatus(c *cli.Context) error { + useJSON := c.Bool("json") + host := c.String("host") + args := c.Args() + + if host != "" { + clientConfig, remoteAddress, err := getSSHConfig(host) + if err != nil { + return fmt.Errorf("SSH config error for '%s': %v", host, err) + } + if !useJSON { + color.Cyan("Connecting to %s (%s)...", host, remoteAddress) + } + return executeRemoteDockerStatus(c.Context, clientConfig, remoteAddress, useJSON) + } else if args.Len() > 0 { + userHost := args.Get(0) + keyPath := c.String("i") + if keyPath == "" { + return fmt.Errorf("Missing SSH key (-i )") + } + clientConfig, remoteAddress, err := getManualSSHConfig(userHost, keyPath) + if err != nil { + return fmt.Errorf("SSH config error for '%s': %v", userHost, err) + } + if !useJSON { + color.Cyan("Connecting to %s with provided SSH key...", remoteAddress) + } + return executeRemoteDockerStatus(c.Context, clientConfig, remoteAddress, useJSON) + } + return fmt.Errorf("Missing args. Use '--host ' or '@ -i '") +} + +// dockerEvents subscribes to Docker events in real time. +func dockerEvents(c *cli.Context) error { + useJSON := c.Bool("json") + if !useJSON { + color.Cyan("Subscribing to Docker events... (press Ctrl+C to exit)") + } + + cliDocker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - log.Println("❌ Error fetching running containers:", err) - return nil + return fmt.Errorf("Docker client error: %v", err) + } + defer cliDocker.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + options := types.EventsOptions{} + msgChan, errChan := cliDocker.Events(ctx, options) + + for { + select { + case event := <-msgChan: + if useJSON { + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("JSON marshal error: %v", err) + } + fmt.Println(string(data)) + } else { + fmt.Printf("Type: %s | Action: %s | Actor: %v | Time: %s\n", + event.Type, + event.Action, + event.Actor.Attributes, + time.Unix(event.Time, 0).Format(time.RFC3339)) + } + case err := <-errChan: + return fmt.Errorf("Event error: %v", err) + } } - containers := strings.Split(strings.TrimSpace(string(out)), "\n") - if len(containers) == 1 && containers[0] == "" { +} + +// executeLocalDockerStatus runs "docker ps" locally. +func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool, filter string) error { + baseArgs := []string{"ps"} + + // Add filter if provided + if filter != "" { + baseArgs = append(baseArgs, "--filter", filter) + } + + if useJSON { + baseArgs = append(baseArgs, "--format", "{{json .}}") + cmdArgs := append(baseArgs, args...) + cmd := exec.CommandContext(ctx, "docker", cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("docker ps failed: %v\n%s", err, string(output)) + } + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + jsonArray := "[" + strings.Join(lines, ",") + "]" + fmt.Println(jsonArray) return nil } - return containers + + baseArgs = append(baseArgs, "--format", "πŸ“¦ {{.Names}} | πŸ”Ή {{.Status}} | πŸ” {{.Ports}}") + cmdArgs := append(baseArgs, args...) + cmd := exec.CommandContext(ctx, "docker", cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("docker ps failed: %v\n%s", err, string(output)) + } + trimmed := strings.TrimSpace(string(output)) + if trimmed == "" { + color.Yellow("No running containers found!") + } else { + color.Green("Local Containers:") + fmt.Println(trimmed) + } + return nil } -// Retrieves the exposed ports for a given container -func getContainerPorts(container string) (string, string) { - out, err := exec.Command("docker", "inspect", "-f", "{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} {{end}}", container).Output() +// executeLocalServiceCheck checks services using HTTP. +func executeLocalServiceCheck(ctx context.Context, useJSON bool) error { + if !useJSON { + color.Cyan("Checking services on ports...") + } + cmd := exec.CommandContext(ctx, "docker", "ps", "--format", "{{.Names}}: {{.Ports}}") + output, err := cmd.CombinedOutput() if err != nil { - log.Println("❌ Error fetching ports for container:", container, err) - return "N/A", "N/A" + return fmt.Errorf("Failed to get containers: %v\n%s", err, string(output)) + } + lines := strings.Split(string(output), "\n") + if len(lines) == 0 || (len(lines) == 1 && strings.TrimSpace(lines[0]) == "") { + if !useJSON { + color.Yellow("No running containers found!") + } else { + fmt.Println("[]") + } + return nil } - ports := strings.Fields(strings.TrimSpace(string(out))) // Splitting safely - if len(ports) > 0 { - // Extract host and container ports - portMappingOut, _ := exec.Command("docker", "port", container).Output() - portMapping := strings.Split(strings.TrimSpace(string(portMappingOut)), "\n") + var wg sync.WaitGroup + var mu sync.Mutex + var results []ServiceCheckResult - if len(portMapping) > 0 { - portParts := strings.Fields(portMapping[0]) // Example: "80/tcp -> 0.0.0.0:8081" - if len(portParts) > 2 { - hostPort := strings.Split(portParts[2], ":")[1] // Extract 8081 - return ports[0], hostPort + for _, line := range lines { + if line == "" { + continue + } + parts := strings.Split(line, ": ") + if len(parts) != 2 { + continue + } + container := parts[0] + ports := strings.Split(parts[1], ", ") + for _, portInfo := range ports { + portParts := strings.Split(portInfo, "->") + if len(portParts) != 2 { + continue + } + hostPortParts := strings.Split(portParts[0], ":") + if len(hostPortParts) < 2 { + continue + } + port := hostPortParts[1] + url := fmt.Sprintf("http://localhost:%s", port) + wg.Add(1) + go func(container, port, url string) { + defer wg.Done() + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + mu.Lock() + results = append(results, ServiceCheckResult{Container: container, Port: port, Status: "request error"}) + mu.Unlock() + return + } + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + mu.Lock() + defer mu.Unlock() + if err != nil { + results = append(results, ServiceCheckResult{Container: container, Port: port, Status: "unreachable"}) + return + } + defer resp.Body.Close() + if resp.StatusCode == 200 { + results = append(results, ServiceCheckResult{Container: container, Port: port, Status: "available"}) + } else { + results = append(results, ServiceCheckResult{Container: container, Port: port, Status: fmt.Sprintf("HTTP %d", resp.StatusCode)}) + } + }(container, port, url) + } + } + wg.Wait() + if useJSON { + jsonData, err := json.Marshal(results) + if err != nil { + return fmt.Errorf("JSON marshal error: %v", err) + } + fmt.Println(string(jsonData)) + } else { + for _, r := range results { + switch r.Status { + case "available": + color.Green("%s service is available on port %s.", r.Container, r.Port) + case "unreachable": + color.Red("%s on port %s is unreachable.", r.Container, r.Port) + default: + color.Yellow("%s service returned %s on port %s.", r.Container, r.Status, r.Port) } } - return ports[0], "Unknown" + color.Green("Service check completed.") + } + return nil +} + +// executeRemoteDockerStatus runs "docker ps" on a remote host via SSH. +func executeRemoteDockerStatus(ctx context.Context, config *ssh.ClientConfig, addr string, useJSON bool) error { + client, err := ssh.Dial("tcp", addr, config) + if err != nil { + return fmt.Errorf("Failed to connect to %s: %v", addr, err) + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("Session error on %s: %v", addr, err) + } + defer session.Close() + + var b bytes.Buffer + session.Stdout = &b + + if useJSON { + err = session.Run("docker ps --format '{{json .}}'") + } else { + err = session.Run("docker ps --format 'πŸ“¦ {{.Names}} | πŸ”Ή {{.Status}} | πŸ”Ž {{.Ports}}'") + } + if err != nil { + return fmt.Errorf("docker ps failed on %s: %v", addr, err) } - return "N/A", "N/A" + output := strings.TrimSpace(b.String()) + if output == "" { + if useJSON { + fmt.Println("[]") + } else { + color.Yellow("No running containers on remote host!") + } + } else { + if useJSON { + lines := strings.Split(output, "\n") + jsonArray := "[" + strings.Join(lines, ",") + "]" + fmt.Println(jsonArray) + } else { + color.Green("Remote Containers:") + fmt.Println(output) + } + } + return nil } -// Checks if the service inside the container is available -func checkService(port string) bool { - url := fmt.Sprintf("http://localhost:%s", port) - client := http.Client{Timeout: 2 * time.Second} - resp, err := client.Get(url) +// getSSHConfig retrieves SSH configuration from ~/.ssh/config using an alias. +func getSSHConfig(alias string) (*ssh.ClientConfig, string, error) { + sshConfigPath := os.ExpandEnv("$HOME/.ssh/config") + f, err := os.Open(sshConfigPath) if err != nil { - return false + return nil, "", fmt.Errorf("Cannot open SSH config: %v", err) + } + defer f.Close() + + cfg, err := ssh_config.Decode(f) + if err != nil { + return nil, "", fmt.Errorf("Decode error: %v", err) + } + + hostname, err := cfg.Get(alias, "HostName") + if err != nil || hostname == "" { + return nil, "", fmt.Errorf("HostName not found for %s", alias) + } + + user, err := cfg.Get(alias, "User") + if err != nil || user == "" { + user = os.Getenv("USER") } - defer resp.Body.Close() - return resp.StatusCode == 200 + + keyPath, err := cfg.Get(alias, "IdentityFile") + if err != nil || keyPath == "" { + keyPath = os.ExpandEnv("$HOME/.ssh/id_rsa") + } else { + keyPath, err = expandPath(keyPath) + if err != nil { + return nil, "", fmt.Errorf("Key path error: %v", err) + } + } + + key, err := os.ReadFile(keyPath) + if err != nil { + return nil, "", fmt.Errorf("Cannot read key at %s: %v", keyPath, err) + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, "", fmt.Errorf("Cannot parse key at %s: %v", keyPath, err) + } + + knownHostsFile := os.ExpandEnv("$HOME/.ssh/known_hosts") + hostKeyCallback, err := knownhosts.New(knownHostsFile) + if err != nil { + return nil, "", fmt.Errorf("Host key callback error: %v", err) + } + + clientConfig := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: hostKeyCallback, + Timeout: 10 * time.Second, + } + + return clientConfig, fmt.Sprintf("%s:22", hostname), nil } -// Displays full container and service status -func fullStatus(c *cli.Context) error { - // Separator line before printing new results - fmt.Println("\n--------------------------------------") - fmt.Println("πŸ”„ Checking container and service status...") - fmt.Println("--------------------------------------\n") +// getManualSSHConfig retrieves SSH configuration from a user@host string and key path. +func getManualSSHConfig(userHost, keyPath string) (*ssh.ClientConfig, string, error) { + keyPath, err := expandPath(keyPath) + if err != nil { + return nil, "", fmt.Errorf("Key path error: %v", err) + } - containers := getRunningContainers() - if len(containers) == 0 { - fmt.Println("❌ No running containers found!") - return nil + key, err := os.ReadFile(keyPath) + if err != nil { + return nil, "", fmt.Errorf("Cannot read key at %s: %v", keyPath, err) } - for _, container := range containers { - containerPort, hostPort := getContainerPorts(container) - serviceRunning := containerPort != "N/A" && checkService(hostPort) + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, "", fmt.Errorf("Cannot parse key at %s: %v", keyPath, err) + } - fmt.Printf("πŸ“Œ %s: Container - 🟒 Running, Service Port - %s, Host Port - %s, Service - %v\n", - container, - containerPort, - hostPort, - map[bool]string{true: "🟒 Available", false: "πŸ”΄ Unavailable"}[serviceRunning], - ) + parts := strings.Split(userHost, "@") + if len(parts) != 2 { + return nil, "", fmt.Errorf("Invalid user@host: %s", userHost) } + user := parts[0] + host := parts[1] - return nil + knownHostsFile := os.ExpandEnv("$HOME/.ssh/known_hosts") + hostKeyCallback, err := knownhosts.New(knownHostsFile) + if err != nil { + return nil, "", fmt.Errorf("Host key callback error: %v", err) + } + + clientConfig := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: hostKeyCallback, + Timeout: 10 * time.Second, + } + + return clientConfig, fmt.Sprintf("%s:22", host), nil } -// Displays only container state -func stateOnly(c *cli.Context) error { - fmt.Println("\n--------------------------------------") - fmt.Println("πŸ”„ Checking container states...") - fmt.Println("--------------------------------------\n") +// expandPath expands the "~" to the home directory. +func expandPath(path string) (string, error) { + if strings.HasPrefix(path, "~") { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return strings.Replace(path, "~", home, 1), nil + } + return path, nil +} - containers := getRunningContainers() - if len(containers) == 0 { - fmt.Println("❌ No running containers found!") - return nil +// watchMode continuously monitors containers with auto-refresh. +func watchMode(c *cli.Context) error { + useJSON := c.Bool("json") + interval := time.Duration(c.Int("interval")) * time.Second + noClear := c.Bool("no-clear") + filter := c.String("filter") + + if useJSON { + return fmt.Errorf("watch mode not supported with JSON output") } - for _, container := range containers { - fmt.Printf("πŸ“Œ %s: 🟒 Running\n", container) + ticker := time.NewTicker(interval) + defer ticker.Stop() + + // Setup signal handling for graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // Initial display + if !noClear { + clearScreen() + } + displayStatus(c.Context, filter) + + for { + select { + case <-ticker.C: + if !noClear { + clearScreen() + } + displayStatus(c.Context, filter) + fmt.Printf("\nπŸ• Last update: %s | Refresh: %ds | Press Ctrl+C to exit\n", + time.Now().Format("15:04:05"), c.Int("interval")) + case <-sigChan: + color.Yellow("\nπŸ‘‹ Exiting watch mode...") + return nil + } } - return nil } -// Displays only service availability -func serviceOnly(c *cli.Context) error { - fmt.Println("\n--------------------------------------") - fmt.Println("πŸ”„ Checking service availability...") - fmt.Println("--------------------------------------\n") +func clearScreen() { + fmt.Print("\033[H\033[2J") +} + +func displayStatus(ctx context.Context, filter string) { + color.Cyan("🐳 Docker Container Monitor - Live View") + fmt.Println(strings.Repeat("=", 70)) + executeLocalDockerStatus(ctx, []string{}, false, filter) +} + +// showStats displays container resource usage statistics. +func showStats(c *cli.Context) error { + useJSON := c.Bool("json") + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Docker client error: %v", err) + } + defer cli.Close() + + ctx := context.Background() + + // List all running containers + containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) + if err != nil { + return fmt.Errorf("Failed to list containers: %v", err) + } - containers := getRunningContainers() if len(containers) == 0 { - fmt.Println("❌ No running containers found!") + color.Yellow("No running containers found!") return nil } + var stats []ContainerStat + for _, container := range containers { - containerPort, hostPort := getContainerPorts(container) - serviceRunning := containerPort != "N/A" && checkService(hostPort) + statsResponse, err := cli.ContainerStats(ctx, container.ID, false) + if err != nil { + continue + } + + var v types.StatsJSON + if err := json.NewDecoder(statsResponse.Body).Decode(&v); err != nil { + statsResponse.Body.Close() + continue + } + statsResponse.Body.Close() + + // Calculate CPU percentage + cpuPercent := calculateCPUPercent(&v) + memPercent := 0.0 + if v.MemoryStats.Limit > 0 { + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + } - fmt.Printf("πŸ“Œ %s: Service Port - %s, Host Port - %s, Service - %v\n", - container, - containerPort, - hostPort, - map[bool]string{true: "🟒 Available", false: "πŸ”΄ Unavailable"}[serviceRunning], - ) + // Calculate network I/O + var networkRx, networkTx uint64 + for _, network := range v.Networks { + networkRx += network.RxBytes + networkTx += network.TxBytes + } + + name := container.Names[0] + if strings.HasPrefix(name, "/") { + name = name[1:] + } + + stats = append(stats, ContainerStat{ + Name: name, + CPUPercent: cpuPercent, + MemoryUsage: v.MemoryStats.Usage, + MemoryLimit: v.MemoryStats.Limit, + MemoryPercent: memPercent, + NetworkRx: networkRx, + NetworkTx: networkTx, + }) + } + + if useJSON { + jsonData, _ := json.MarshalIndent(stats, "", " ") + fmt.Println(string(jsonData)) + } else { + printStatsTable(stats) } return nil } -// Runs the monitor tool as a continuous systemd service -func runAsService() { - fmt.Println("πŸš€ Starting Monitor Service...") +func calculateCPUPercent(stats *types.StatsJSON) float64 { + cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage) + cpuCount := float64(stats.CPUStats.OnlineCPUs) - for { - fullStatus(nil) - time.Sleep(10 * time.Second) + if cpuCount == 0 { + cpuCount = float64(runtime.NumCPU()) } -} -func main() { - app := &cli.App{ - Name: "monitor", - Usage: "Monitor running Docker containers and their services", - Commands: []*cli.Command{ - { - Name: "state", - Usage: "Displays only container names and their states", - Action: stateOnly, - }, - { - Name: "service", - Usage: "Displays only the status of services", - Action: serviceOnly, - }, - }, - Action: fullStatus, + if systemDelta > 0.0 && cpuDelta > 0.0 { + return (cpuDelta / systemDelta) * cpuCount * 100.0 } + return 0.0 +} - // If no arguments are given, assume it's running as a systemd service - if len(os.Args) > 1 { - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) +func printStatsTable(stats []ContainerStat) { + color.Cyan("πŸ“Š Container Resource Statistics") + fmt.Println(strings.Repeat("=", 100)) + fmt.Printf("%-25s %-10s %-25s %-10s %-20s\n", + "CONTAINER", "CPU %", "MEMORY USAGE", "MEM %", "NET I/O (RX/TX)") + fmt.Println(strings.Repeat("-", 100)) + + for _, stat := range stats { + memUsage := formatBytes(stat.MemoryUsage) + memLimit := formatBytes(stat.MemoryLimit) + netIO := fmt.Sprintf("%s / %s", formatBytes(stat.NetworkRx), formatBytes(stat.NetworkTx)) + + // Color code based on usage + cpuColor := color.GreenString + if stat.CPUPercent > 80 { + cpuColor = color.RedString + } else if stat.CPUPercent > 50 { + cpuColor = color.YellowString } - } else { - runAsService() + + memColor := color.GreenString + if stat.MemoryPercent > 90 { + memColor = color.RedString + } else if stat.MemoryPercent > 70 { + memColor = color.YellowString + } + + fmt.Printf("%-25s %s %-25s %s %-20s\n", + truncate(stat.Name, 25), + cpuColor("%-10.2f", stat.CPUPercent), + fmt.Sprintf("%s / %s", memUsage, memLimit), + memColor("%-10.2f", stat.MemoryPercent), + netIO) + } + fmt.Println(strings.Repeat("=", 100)) +} + +func formatBytes(bytes uint64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := uint64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s } + return s[:maxLen-3] + "..." +} + +// containerLogs streams logs from a container. +func containerLogs(c *cli.Context) error { + if c.NArg() == 0 { + return fmt.Errorf("Please specify a container name or ID") + } + + containerName := c.Args().Get(0) + tail := c.Int("tail") + follow := c.Bool("follow") + + args := []string{"logs"} + if tail > 0 { + args = append(args, "--tail", fmt.Sprintf("%d", tail)) + } + if follow { + args = append(args, "-f") + } + args = append(args, containerName) + + cmd := exec.CommandContext(c.Context, "docker", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if !follow { + color.Cyan("πŸ“œ Logs from container: %s", containerName) + fmt.Println(strings.Repeat("=", 70)) + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to get logs for %s: %v\nπŸ’‘ Try: docker ps to see available containers", containerName, err) + } + + return nil } diff --git a/readme/monitor-service.png b/readme/monitor-service.png new file mode 100644 index 0000000..2a8d9a2 Binary files /dev/null and b/readme/monitor-service.png differ diff --git a/readme/monitor-state.png b/readme/monitor-state.png new file mode 100644 index 0000000..f0fc04b Binary files /dev/null and b/readme/monitor-state.png differ diff --git a/readme/monitor.png b/readme/monitor.png new file mode 100644 index 0000000..b07addc Binary files /dev/null and b/readme/monitor.png differ