diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f13dc6a..d07a9c0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,81 +1,66 @@ name: CI on: + push: + branches: + - main + - develop + - 'feature/**' pull_request: - branches: [ main ] - workflow_dispatch: + branches: + - main + - develop jobs: - build: - name: Build/Test + build-and-test: + name: Build & Test runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.22' - - name: Install Dependencies - run: go mod tidy + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- - - name: Build and Install Monitor + - name: Download dependencies run: | - go build -o monitor - sudo mv monitor /usr/local/bin/ - chmod +x /usr/local/bin/monitor + go mod download + go mod verify - - name: Install Docker and Docker Compose + - name: Build monitor binary run: | - sudo apt-get update - for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done - sudo apt-get update - sudo apt-get install ca-certificates curl - sudo install -m 0755 -d /etc/apt/keyrings - sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources: - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + go build -v -o monitor monitor.go + chmod +x monitor - - name: Set Up SSH Config for CI + - name: Verify binary 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 + ./monitor --version + ./monitor --help - - name: Start Local Docker Containers for Testing - run: docker compose -f docker-compose.yml up -d + - name: Run Go vet + run: go vet ./... - - name: Wait for Local Containers to Initialize - run: sleep 10 # Ensure services are fully started - - - name: Run Installation Script + - name: Test Python installer run: | - python3 install.py + python3 -m py_compile install.py + python3 install.py --help - - name: Verify Monitor Commands - run: | - echo "🔄 Testing monitor --service" - monitor service || { echo "❌ monitor service failed"; exit 1; } - - echo "🔄 Testing monitor --state" - 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@v4 + with: + name: monitor-binary + path: monitor + retention-days: 30 diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml deleted file mode 100644 index 95e3857..0000000 --- a/.github/workflows/CICD.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: CI/CD - -on: - push: - branches: [ main ] - workflow_dispatch: - -jobs: - build: - name: Build/Test - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23' - - - name: Install Dependencies - run: go mod tidy - - - 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: | - sudo apt-get update - for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done - sudo apt-get update - sudo apt-get install ca-certificates curl - sudo install -m 0755 -d /etc/apt/keyrings - sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources: - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - - 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 Local Containers to Initialize - run: sleep 10 # Ensure services are fully started - - - name: Run Installation Script - run: | - python3 install.py - - - name: Verify Monitor Commands - run: | - echo "🔄 Testing monitor --service" - monitor service || { echo "❌ monitor service failed"; exit 1; } - - echo "🔄 Testing monitor --state" - 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@v4 - with: - name: monitor-binary - 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@v4 - with: - name: monitor-binary - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - 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.MONITOR_TOKEN_CICD }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..434c4d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,263 @@ +name: CI Pipeline + +on: + push: + branches: + - main + - develop + - 'feature/**' + pull_request: + branches: + - main + - develop + release: + types: [ created ] + +env: + GO_VERSION: '1.22' + PYTHON_VERSION: '3.9' + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install golangci-lint + run: | + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 + + - name: Run golangci-lint + run: golangci-lint run --timeout=5m || true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Python linters + run: | + pip install flake8 black pylint + + - name: Lint Python code + run: | + flake8 install.py --max-line-length=120 --ignore=E501,W503 || true + black --check install.py || true + + build: + name: Build and Test + runs-on: ubuntu-latest + needs: lint + strategy: + matrix: + os: [ubuntu-latest, ubuntu-20.04] + go: ['1.22', '1.21'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go }}- + + - name: Download dependencies + run: | + go mod download + go mod verify + + - name: Build binary + run: | + go build -v -o monitor monitor.go + chmod +x monitor + + - name: Check binary + run: | + ./monitor --version + ./monitor --help + + - name: Run Go vet + run: go vet ./... + + - name: Run Go fmt check + run: | + if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then + echo "Please run 'go fmt' on your code" + gofmt -s -l . + exit 1 + fi + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: monitor-${{ matrix.os }}-go${{ matrix.go }} + path: monitor + retention-days: 7 + + test-install-script: + name: Test Installation Script + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Docker + run: | + sudo apt-get update + sudo apt-get install -y docker.io + sudo systemctl start docker + sudo systemctl enable docker + + - name: Test install.py syntax + run: python3 -m py_compile install.py + + - name: Test install.py --help + run: python3 install.py --help + + - name: Test build process (no systemd) + run: | + python3 install.py --no-systemd || true + + security-scan: + name: Security Scanning + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + docker-build: + name: Build Docker Image (Optional) + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + run: | + cat > Dockerfile << 'EOF' + FROM golang:1.22-alpine AS builder + WORKDIR /app + COPY go.mod go.sum ./ + RUN go mod download + COPY monitor.go . + RUN go build -o monitor monitor.go + + FROM alpine:latest + RUN apk --no-cache add ca-certificates docker-cli + WORKDIR /root/ + COPY --from=builder /app/monitor . + ENTRYPOINT ["./monitor"] + EOF + + docker build -t docker-container-monitor:latest . + + release: + name: Create Release + runs-on: ubuntu-latest + needs: [build, test-install-script, security-scan] + if: github.event_name == 'release' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Build release binaries + run: | + # Linux AMD64 + GOOS=linux GOARCH=amd64 go build -o monitor-linux-amd64 monitor.go + + # Linux ARM64 + GOOS=linux GOARCH=arm64 go build -o monitor-linux-arm64 monitor.go + + # macOS AMD64 + GOOS=darwin GOARCH=amd64 go build -o monitor-darwin-amd64 monitor.go + + # macOS ARM64 (Apple Silicon) + GOOS=darwin GOARCH=arm64 go build -o monitor-darwin-arm64 monitor.go + + # Create checksums + sha256sum monitor-* > checksums.txt + + - name: Upload release assets + uses: softprops/action-gh-release@v1 + with: + files: | + monitor-linux-amd64 + monitor-linux-arm64 + monitor-darwin-amd64 + monitor-darwin-arm64 + checksums.txt + install.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + notify: + name: Notify Status + runs-on: ubuntu-latest + needs: [lint, build, test-install-script, security-scan] + if: always() + + steps: + - name: Check build status + run: | + if [ "${{ needs.build.result }}" != "success" ]; then + echo "Build failed!" + exit 1 + fi + echo "All checks passed! ✅" diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3240016 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,56 @@ +# golangci-lint configuration +# https://golangci-lint.run/usage/configuration/ + +run: + timeout: 5m + tests: false + skip-dirs: + - .github + - readme + +linters: + enable: + - errcheck # Check for unchecked errors + - gosimple # Simplify code + - govet # Reports suspicious constructs + - ineffassign # Detect ineffectual assignments + - staticcheck # Static analysis checks + - unused # Check for unused code + - gofmt # Check formatting + - goimports # Check imports + - misspell # Check spelling + - revive # Fast, configurable linter + - gosec # Security checker + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + + govet: + check-shadowing: true + + gofmt: + simplify: true + + revive: + rules: + - name: exported + severity: warning + disabled: false + + gosec: + severity: medium + confidence: medium + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 + + exclude-rules: + # Exclude some linters from running on tests files + - path: _test\.go + linters: + - errcheck + - gosec diff --git a/README.md b/README.md index 9185f7e..115c60f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Docker Container Monitor 🚀 -[![CI/CD Status](https://github.com/Kobeep/Docker_Container_monitor/actions/workflows/CICD.yml/badge.svg)](https://github.com/Kobeep/Docker_Container_monitor/actions) +[![CI Status](https://github.com/Kobeep/Docker_Container_monitor/actions/workflows/CI.yml/badge.svg)](https://github.com/Kobeep/Docker_Container_monitor/actions) Docker Container Monitor is a lightweight CLI tool written in Go that monitors running Docker containers and their services. It provides real-time information about container states, checks service availability, and even supports remote monitoring via SSH. The project also includes a Python installer script for an automated setup. diff --git a/__pycache__/install.cpython-312.pyc b/__pycache__/install.cpython-312.pyc new file mode 100644 index 0000000..bbeab4e Binary files /dev/null and b/__pycache__/install.cpython-312.pyc differ diff --git a/install.py b/install.py index 5969bfc..a5cd0b5 100755 --- a/install.py +++ b/install.py @@ -1,124 +1,411 @@ +#!/usr/bin/env python3 +""" +Docker Container Monitor - Installation Script +Automates installation with dependency checking, Go setup, and systemd integration +""" + import os -import time import sys import subprocess -import threading - -def spinner_animation(text, stop_event): - spinner_chars = ['|', '/', '-', '\\'] - idx = 0 - while not stop_event.is_set(): - sys.stdout.write(f"\r{text} {spinner_chars[idx % len(spinner_chars)]}") - sys.stdout.flush() - idx += 1 - time.sleep(0.1) - sys.stdout.write("\r" + " " * (len(text) + 4) + "\r") - sys.stdout.flush() +import shutil +import argparse +from pathlib import Path -def remove_existing_service(): + +class Colors: + """ANSI color codes for terminal output""" + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + +def print_header(text): + """Print a formatted header""" + print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*70}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{text.center(70)}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{'='*70}{Colors.ENDC}\n") + + +def print_success(text): + """Print success message""" + print(f"{Colors.OKGREEN}✅ {text}{Colors.ENDC}") + + +def print_error(text): + """Print error message""" + print(f"{Colors.FAIL}❌ {text}{Colors.ENDC}") + + +def print_warning(text): + """Print warning message""" + print(f"{Colors.WARNING}⚠️ {text}{Colors.ENDC}") + + +def print_info(text): + """Print info message""" + print(f"{Colors.OKCYAN}ℹ️ {text}{Colors.ENDC}") + + +def run_command(command, check=True, capture_output=False, shell=False): """ - Checks if monitor.service exists in systemd and, if it does, - stops, disables, and removes the service along with its binary, - displaying an animated spinner during the process. + Execute a shell command with error handling """ try: - result = subprocess.run( - ["systemctl", "list-unit-files", "monitor.service"], - capture_output=True, - text=True, - check=True - ) - if "monitor.service" in result.stdout: - print("monitor.service found. Removing service and related files...") - - stop_spinner = threading.Event() - spinner_thread = threading.Thread(target=spinner_animation, args=("Removing monitor service...", stop_spinner)) - spinner_thread.start() - - commands = [ - "sudo systemctl stop monitor", - "sudo systemctl disable monitor", - "sudo rm /usr/local/bin/monitor", - "sudo rm /etc/systemd/system/monitor.service" - ] - for cmd in commands: - subprocess.run(cmd, shell=True, check=True) - time.sleep(0.5) - - stop_spinner.set() - spinner_thread.join() - print("Monitor service removal complete!") + if capture_output: + result = subprocess.run( + command if shell else command.split(), + capture_output=True, + text=True, + check=check, + shell=shell + ) + return result.returncode == 0, result.stdout, result.stderr else: - print("monitor.service not found. Nothing to do.") + result = subprocess.run( + command if shell else command.split(), + check=check, + shell=shell + ) + return result.returncode == 0, "", "" except subprocess.CalledProcessError as e: - print(f"Error checking for monitor.service: {e}") + return False, "", str(e) + except Exception as e: + return False, "", str(e) + + +def check_requirements(): + """Check if Docker and Python are installed""" + print_info("Checking system requirements...") + + # Check Docker + success, stdout, _ = run_command("docker --version", capture_output=True) + if not success: + print_error("Docker is not installed or not running!") + print_info("Please install Docker: https://docs.docker.com/get-docker/") + sys.exit(1) + print_success(f"Docker found: {stdout.strip()}") + + # Check Python version + if sys.version_info < (3, 6): + print_error("Python 3.6 or higher is required!") + sys.exit(1) + print_success(f"Python {sys.version_info.major}.{sys.version_info.minor} found") + + +def detect_distro(): + """Detect Linux distribution""" + try: + with open('/etc/os-release', 'r') as f: + content = f.read().lower() + if 'fedora' in content or 'rhel' in content or 'centos' in content: + return 'fedora' + elif 'ubuntu' in content or 'debian' in content: + return 'debian' + elif 'arch' in content: + return 'arch' + except FileNotFoundError: + pass + return 'unknown' + + +def check_go_installation(): + """Check if Go is installed, install if missing""" + print_info("Checking Go installation...") + + success, stdout, _ = run_command("go version", capture_output=True) + if success: + print_success(f"Go found: {stdout.strip()}") + return True + + print_warning("Go is not installed. Attempting to install...") + distro = detect_distro() + + install_commands = { + 'fedora': 'sudo dnf install -y golang', + 'debian': 'sudo apt update && sudo apt install -y golang-go', + 'arch': 'sudo pacman -S --noconfirm go' + } + + if distro in install_commands: + print_info(f"Detected {distro.capitalize()}-based system") + success, _, stderr = run_command(install_commands[distro], shell=True) + if success: + print_success("Go installed successfully!") + return True + else: + print_error(f"Failed to install Go: {stderr}") + print_info("Please install Go manually: https://golang.org/doc/install") + sys.exit(1) + else: + print_error("Could not detect distribution") + print_info("Please install Go manually: https://golang.org/doc/install") + sys.exit(1) + + +def remove_existing_service(): + """Remove existing monitor service if present""" + print_info("Checking for existing installation...") + + success, stdout, _ = run_command( + "systemctl list-unit-files monitor.service", + capture_output=True + ) + + if success and "monitor.service" in stdout: + print_warning("Existing installation found. Removing...") + + commands = [ + "sudo systemctl stop monitor", + "sudo systemctl disable monitor", + "sudo rm -f /usr/local/bin/monitor", + "sudo rm -f /etc/systemd/system/monitor.service" + ] + + for cmd in commands: + run_command(cmd, check=False, shell=True) -remove_existing_service() + run_command("sudo systemctl daemon-reload", shell=True) + print_success("Previous installation removed") + else: + print_success("No existing installation found") -def loading_animation(text): - for _ in range(5): - sys.stdout.write(f"\r{text} [ {'-' * (_ % 4)} ]") - sys.stdout.flush() - time.sleep(0.5) - print("\r✅ " + text + " complete!") -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") +def build_monitor(): + """Build the monitor binary""" + print_header("Building Monitor") -if not os.path.exists("monitor.go"): - print("❌ Error: monitor.go not found!") - sys.exit(1) + if not os.path.exists("monitor.go"): + print_error("monitor.go not found in current directory!") + sys.exit(1) -loading_animation("Copying Go source code") -os.system("mkdir -p /tmp/monitor_build && cp monitor.go go.mod /tmp/monitor_build/") + if not os.path.exists("go.mod"): + print_error("go.mod not found in current directory!") + sys.exit(1) -loading_animation("Initializing Go modules") -os.system("cd /tmp/monitor_build && go mod tidy") + # Create build directory + build_dir = "/tmp/monitor_build" + if os.path.exists(build_dir): + shutil.rmtree(build_dir) + os.makedirs(build_dir) -loading_animation("Installing Go dependencies") -os.system("cd /tmp/monitor_build && go get github.com/urfave/cli/v2") + print_info("Copying source files to build directory...") + shutil.copy("monitor.go", build_dir) + shutil.copy("go.mod", build_dir) + if os.path.exists("go.sum"): + shutil.copy("go.sum", build_dir) -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) + # Change to build directory + original_dir = os.getcwd() + os.chdir(build_dir) -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") + try: + print_info("Downloading Go dependencies...") + success, _, stderr = run_command("go mod download", capture_output=True) + if not success: + print_error(f"Failed to download dependencies: {stderr}") + sys.exit(1) + + print_info("Running go mod tidy...") + run_command("go mod tidy") + + print_info("Building binary...") + success, _, stderr = run_command( + "go build -o monitor monitor.go", + capture_output=True + ) + if not success: + print_error(f"Build failed: {stderr}") + sys.exit(1) + + if not os.path.exists("monitor"): + print_error("Binary was not created!") + sys.exit(1) + + print_success("Monitor binary built successfully!") + + finally: + os.chdir(original_dir) + + return build_dir + + +def install_binary(build_dir): + """Install the monitor binary to /usr/local/bin""" + print_info("Installing monitor binary...") -if not os.path.exists("/usr/local/bin/monitor"): - print("❌ Error: monitor binary not found in /usr/local/bin/") - sys.exit(1) + binary_path = os.path.join(build_dir, "monitor") + success, _, stderr = run_command( + f"sudo cp {binary_path} /usr/local/bin/monitor", + shell=True + ) + if not success: + print_error(f"Failed to copy binary: {stderr}") + sys.exit(1) -loading_animation("Setting up systemd service") -service_config = """ -[Unit] -Description=Monitor Docker containers and services + run_command("sudo chmod +x /usr/local/bin/monitor", shell=True) + + if not os.path.exists("/usr/local/bin/monitor"): + print_error("Binary installation verification failed!") + sys.exit(1) + + print_success("Binary installed to /usr/local/bin/monitor") + + +def setup_systemd_service(): + """Create and enable systemd service""" + print_info("Setting up systemd service...") + + service_content = """[Unit] +Description=Docker Container Monitor +Documentation=https://github.com/Kobeep/Docker_Container_monitor After=network.target docker.service +Wants=docker.service [Service] +Type=simple ExecStart=/usr/local/bin/monitor -Restart=always +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) + service_path = "/tmp/monitor.service" + with open(service_path, "w") as f: + f.write(service_content) + + run_command(f"sudo mv {service_path} /etc/systemd/system/monitor.service", shell=True) + run_command("sudo systemctl daemon-reload", shell=True) + run_command("sudo systemctl enable monitor", shell=True) + run_command("sudo systemctl start monitor", shell=True) + + # Verify service status + success, stdout, _ = run_command( + "systemctl is-active monitor", + capture_output=True, + check=False + ) + + if "active" in stdout: + print_success("Systemd service configured and started!") + else: + print_warning("Service installed but may not be running. Check: systemctl status monitor") + + +def uninstall(): + """Uninstall monitor completely""" + print_header("Uninstalling Monitor") -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") + print_info("Stopping and removing service...") + commands = [ + "sudo systemctl stop monitor", + "sudo systemctl disable monitor", + "sudo rm -f /usr/local/bin/monitor", + "sudo rm -f /etc/systemd/system/monitor.service" + ] -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") -print(" ✅ `monitor remote` → Displays container and service status on remote hosts") + for cmd in commands: + run_command(cmd, check=False, shell=True) + + run_command("sudo systemctl daemon-reload", shell=True) + + print_success("Monitor uninstalled successfully!") + print_info("To remove the source code: rm -rf ~/Docker_Container_monitor") + + +def main(): + """Main installation flow""" + parser = argparse.ArgumentParser( + description="Docker Container Monitor Installer", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python3 install.py # Full installation with systemd + python3 install.py --no-systemd # Install without systemd service + python3 install.py --uninstall # Remove installation +""" + ) + parser.add_argument( + '--no-systemd', + action='store_true', + help='Skip systemd service setup' + ) + parser.add_argument( + '--uninstall', + action='store_true', + help='Uninstall monitor' + ) + + args = parser.parse_args() + + if args.uninstall: + uninstall() + return + + print_header("Docker Container Monitor - Installer v1.1.0") + + # Pre-installation checks + check_requirements() + check_go_installation() + remove_existing_service() + + # Build and install + build_dir = build_monitor() + install_binary(build_dir) + + # Optional systemd setup + if not args.no_systemd: + setup_systemd_service() + else: + print_warning("Skipping systemd service setup (--no-systemd flag)") + + # Clean up + if os.path.exists(build_dir): + shutil.rmtree(build_dir) + + # Success message + print_header("Installation Complete!") + print_success("Monitor installed successfully!\n") + + print(f"{Colors.BOLD}Available Commands:{Colors.ENDC}") + print(f" {Colors.OKCYAN}monitor{Colors.ENDC} - Full container status") + print(f" {Colors.OKCYAN}monitor state{Colors.ENDC} - Container states only") + print(f" {Colors.OKCYAN}monitor service{Colors.ENDC} - Service availability") + print(f" {Colors.OKCYAN}monitor watch{Colors.ENDC} - Continuous monitoring") + print(f" {Colors.OKCYAN}monitor watch --interval 5{Colors.ENDC} - Custom refresh interval") + print(f" {Colors.OKCYAN}monitor stats{Colors.ENDC} - Resource statistics") + print(f" {Colors.OKCYAN}monitor logs {Colors.ENDC} - Stream container logs") + print(f" {Colors.OKCYAN}monitor --filter 'name=nginx'{Colors.ENDC} - Filter containers") + print(f" {Colors.OKCYAN}monitor remote --host {Colors.ENDC} - Remote monitoring") + print(f" {Colors.OKCYAN}monitor events{Colors.ENDC} - Docker events") + print(f" {Colors.OKCYAN}monitor --version{Colors.ENDC} - Show version") + + if not args.no_systemd: + print(f"\n{Colors.BOLD}Systemd Service:{Colors.ENDC}") + print(f" {Colors.OKCYAN}systemctl status monitor{Colors.ENDC} - Check service status") + print(f" {Colors.OKCYAN}journalctl -u monitor -f{Colors.ENDC} - View service logs") + + print(f"\n{Colors.BOLD}Documentation:{Colors.ENDC}") + print(f" {Colors.OKCYAN}https://github.com/Kobeep/Docker_Container_monitor{Colors.ENDC}") + print() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print(f"\n{Colors.WARNING}Installation cancelled by user{Colors.ENDC}") + sys.exit(1) + except Exception as e: + print_error(f"Unexpected error: {e}") + sys.exit(1) diff --git a/monitor.go b/monitor.go index 3ae5d93..e7ecbcd 100644 --- a/monitor.go +++ b/monitor.go @@ -5,14 +5,20 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "os" "os/exec" + "os/signal" + "runtime" + "strconv" "strings" "sync" + "syscall" "time" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/fatih/color" "github.com/kevinburke/ssh_config" @@ -27,15 +33,30 @@ type ServiceCheckResult struct { Status string `json:"status"` } +type ContainerStat struct { + Name string `json:"name"` + CPUPercent float64 `json:"cpu_percent"` + MemUsage uint64 `json:"mem_usage"` + MemLimit uint64 `json:"mem_limit"` + MemPercent float64 `json:"mem_percent"` + NetInput uint64 `json:"net_input"` + NetOutput uint64 `json:"net_output"` +} + func main() { app := &cli.App{ - Name: "monitor", - Usage: "Monitor Docker containers, services and events (local and remote)", + 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", + Usage: "Filter containers (e.g., 'name=nginx', 'status=running', 'label=env=prod')", + }, }, Commands: []*cli.Command{ { @@ -48,6 +69,41 @@ func main() { Usage: "Show service statuses", Action: serviceOnly, }, + { + Name: "watch", + Usage: "Continuously monitor containers with auto-refresh", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "interval", + Usage: "Refresh interval in seconds", + Value: 2, + }, + }, + Action: watchMode, + }, + { + Name: "stats", + Usage: "Show container resource statistics (CPU, memory, network)", + Action: showStats, + }, + { + Name: "logs", + Usage: "Stream container logs", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "follow", + Usage: "Follow log output", + Aliases: []string{"f"}, + }, + &cli.IntFlag{ + Name: "tail", + Usage: "Number of lines to show from the end", + Value: 100, + }, + }, + Action: containerLogs, + }, { Name: "remote", Usage: "Monitor remote Docker via SSH", @@ -85,19 +141,21 @@ Options: // 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) + 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) + return executeLocalDockerStatus(c.Context, []string{"--format", "📂 {{.Names}}: 🔹 {{.Status}}"}, useJSON, filter) } // serviceOnly checks local service availability. @@ -184,9 +242,16 @@ func dockerEvents(c *cli.Context) error { } // executeLocalDockerStatus runs "docker ps" locally. -func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool) error { +func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool, filter string) error { + baseArgs := []string{"ps"} + + // Apply filter if provided + if filter != "" { + baseArgs = append(baseArgs, "--filter", filter) + } + if useJSON { - baseArgs := []string{"ps", "--format", "{{json .}}"} + baseArgs = append(baseArgs, "--format", "{{json .}}") cmdArgs := append(baseArgs, args...) cmd := exec.CommandContext(ctx, "docker", cmdArgs...) output, err := cmd.CombinedOutput() @@ -199,7 +264,7 @@ func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool) return nil } - baseArgs := []string{"ps", "--format", "📦 {{.Names}} | 🔹 {{.Status}} | 🔍 {{.Ports}}"} + baseArgs = append(baseArgs, "--format", "📦 {{.Names}} | 🔹 {{.Status}} | 🔍 {{.Ports}}") cmdArgs := append(baseArgs, args...) cmd := exec.CommandContext(ctx, "docker", cmdArgs...) output, err := cmd.CombinedOutput() @@ -468,3 +533,238 @@ func expandPath(path string) (string, error) { } return path, nil } + +// watchMode continuously monitors containers with auto-refresh +func watchMode(c *cli.Context) error { + interval := c.Int("interval") + if interval < 1 { + interval = 2 + } + + color.Cyan("🔄 Watch Mode - Refreshing every %d seconds (Press Ctrl+C to exit)", interval) + + // Setup signal handling for graceful exit + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + ticker := time.NewTicker(time.Duration(interval) * time.Second) + defer ticker.Stop() + + // Display immediately + clearScreen() + executeLocalDockerStatus(c.Context, []string{}, false, c.String("filter")) + + for { + select { + case <-ticker.C: + clearScreen() + executeLocalDockerStatus(c.Context, []string{}, false, c.String("filter")) + case <-sigChan: + color.Yellow("\n👋 Exiting watch mode...") + return nil + } + } +} + +// clearScreen clears the terminal screen +func clearScreen() { + if runtime.GOOS == "windows" { + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + cmd.Run() + } else { + fmt.Print("\033[H\033[2J") + } +} + +// showStats displays container resource statistics +func showStats(c *cli.Context) error { + color.Cyan("📊 Fetching container statistics...") + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Docker client error: %v", err) + } + defer cli.Close() + + containers, err := cli.ContainerList(c.Context, container.ListOptions{}) + if err != nil { + return fmt.Errorf("Failed to list containers: %v", err) + } + + if len(containers) == 0 { + color.Yellow("No running containers found!") + return nil + } + + var stats []ContainerStat + for _, cont := range containers { + resp, err := cli.ContainerStats(c.Context, cont.ID, false) + if err != nil { + color.Yellow("Warning: Could not get stats for %s", cont.Names[0]) + continue + } + + var v types.StatsJSON + if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { + resp.Body.Close() + continue + } + resp.Body.Close() + + // Calculate CPU percentage + cpuPercent := calculateCPUPercent(&v) + + // Memory stats + memUsage := v.MemoryStats.Usage + memLimit := v.MemoryStats.Limit + memPercent := float64(memUsage) / float64(memLimit) * 100.0 + + // Network stats + var netInput, netOutput uint64 + for _, netStats := range v.Networks { + netInput += netStats.RxBytes + netOutput += netStats.TxBytes + } + + containerName := strings.TrimPrefix(cont.Names[0], "/") + stats = append(stats, ContainerStat{ + Name: containerName, + CPUPercent: cpuPercent, + MemUsage: memUsage, + MemLimit: memLimit, + MemPercent: memPercent, + NetInput: netInput, + NetOutput: netOutput, + }) + } + + if c.Bool("json") { + jsonData, err := json.Marshal(stats) + if err != nil { + return fmt.Errorf("JSON marshal error: %v", err) + } + fmt.Println(string(jsonData)) + } else { + printStatsTable(stats) + } + + return nil +} + +// calculateCPUPercent calculates the CPU usage percentage +func calculateCPUPercent(v *types.StatsJSON) float64 { + cpuDelta := float64(v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(v.CPUStats.SystemUsage - v.PreCPUStats.SystemUsage) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + return (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + } + return 0.0 +} + +// printStatsTable displays statistics in a formatted table +func printStatsTable(stats []ContainerStat) { + fmt.Println() + color.Cyan("┌─────────────────────────────────────────────────────────────────────────────┐") + color.Cyan("│ CONTAINER STATISTICS │") + color.Cyan("├─────────────────────────────────────────────────────────────────────────────┤") + + // Header + fmt.Printf("│ %-20s │ %8s │ %15s │ %20s │\n", "CONTAINER", "CPU %", "MEMORY", "NETWORK I/O") + color.Cyan("├─────────────────────────────────────────────────────────────────────────────┤") + + for _, s := range stats { + name := truncate(s.Name, 20) + cpuStr := fmt.Sprintf("%.2f%%", s.CPUPercent) + memStr := fmt.Sprintf("%s / %s", formatBytes(s.MemUsage), formatBytes(s.MemLimit)) + netStr := fmt.Sprintf("%s / %s", formatBytes(s.NetInput), formatBytes(s.NetOutput)) + + // Color coding based on resource usage + cpuColor := color.New(color.FgGreen) + if s.CPUPercent > 80 { + cpuColor = color.New(color.FgRed, color.Bold) + } else if s.CPUPercent > 50 { + cpuColor = color.New(color.FgYellow) + } + + memColor := color.New(color.FgGreen) + if s.MemPercent > 80 { + memColor = color.New(color.FgRed, color.Bold) + } else if s.MemPercent > 50 { + memColor = color.New(color.FgYellow) + } + + fmt.Printf("│ %-20s │ ", name) + cpuColor.Printf("%8s", cpuStr) + fmt.Printf(" │ ") + memColor.Printf("%15s", memStr) + fmt.Printf(" │ %20s │\n", netStr) + } + + color.Cyan("└─────────────────────────────────────────────────────────────────────────────┘") + fmt.Println() +} + +// formatBytes converts bytes to human-readable format +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 %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// truncate truncates a string to the specified length +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() < 1 { + return fmt.Errorf("Container name required. Usage: monitor logs ") + } + + containerName := c.Args().Get(0) + follow := c.Bool("follow") + tailLines := strconv.Itoa(c.Int("tail")) + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Docker client error: %v", err) + } + defer cli.Close() + + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: follow, + Tail: tailLines, + Timestamps: true, + } + + out, err := cli.ContainerLogs(c.Context, containerName, options) + if err != nil { + return fmt.Errorf("Failed to get logs for %s: %v", containerName, err) + } + defer out.Close() + + if follow { + color.Cyan("📜 Following logs for %s (Press Ctrl+C to exit)...", containerName) + } else { + color.Cyan("📜 Logs for %s (last %s lines):", containerName, tailLines) + } + fmt.Println() + + _, err = io.Copy(os.Stdout, out) + return err +}