commit ec636a02b937bf6dff5aa01eb666b228bb08d920 Author: dvirlabs <114520947+dvirlabs@users.noreply.github.com> Date: Tue Apr 21 13:10:31 2026 +0300 Initial commit: GitOps Status API diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..34636a8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +__pycache__ +*.pyc +*.pyo +.git +.gitignore +README.md +.pytest_cache +venv +env diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7d9777 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Local +.env +.env.local +*.local + +# Docker +Dockerfile.local +docker-compose.local.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..224bac4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.11-alpine + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application +COPY app.py . + +# Create data directory for status.json +RUN mkdir -p /data && chmod 777 /data + +# Non-root user +RUN adduser -D -u 1000 apiuser && \ + chown -R apiuser:apiuser /app /data + +USER apiuser + +# Health checks +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1 + +EXPOSE 5000 + +ENV FLASK_ENV=production \ + API_HOST=0.0.0.0 \ + API_PORT=5000 \ + STATUS_FILE=/data/status.json + +CMD ["python", "app.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..21211f6 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# GitOps Status API + +Simple Flask API for serving and updating GitOps status information. + +## Features + +- **GET /status.json** - Retrieve current status in JSON format +- **GET /api/status** - API endpoint to retrieve status +- **POST /api/status** - Update status with new data +- **GET /health** - Kubernetes liveness probe +- **GET /ready** - Kubernetes readiness probe + +## Local Development + +```bash +# Install dependencies +pip install -r requirements.txt + +# Run the app +python app.py + +# Test +curl http://localhost:5000/status.json +curl -X POST http://localhost:5000/api/status -H "Content-Type: application/json" -d '{"sync_status":"SYNCED"}' +``` + +## Docker Build + +```bash +# Build the image +docker build -t gitops-status-api:1.0.0 . + +# Run locally +docker run -it -p 5000:5000 -v /tmp/data:/data gitops-status-api:1.0.0 +``` + +## Push to Harbor + +```bash +# Login to Harbor +docker login harbor.your-domain.com + +# Tag for Harbor +docker tag gitops-status-api:1.0.0 harbor.your-domain.com/gitops/status-api:1.0.0 +docker tag gitops-status-api:1.0.0 harbor.your-domain.com/gitops/status-api:latest + +# Push to Harbor +docker push harbor.your-domain.com/gitops/status-api:1.0.0 +docker push harbor.your-domain.com/gitops/status-api:latest +``` + +## Environment Variables + +- `API_HOST` - Listen address (default: 0.0.0.0) +- `API_PORT` - Listen port (default: 5000) +- `STATUS_FILE` - Path to status.json file (default: /data/status.json) +- `FLASK_ENV` - Flask environment (default: production) + +## API Examples + +### Get status +```bash +curl http://localhost:5000/status.json +curl http://localhost:5000/api/status +``` + +### Update status +```bash +curl -X POST http://localhost:5000/api/status \ + -H "Content-Type: application/json" \ + -d '{ + "sync_status": "SYNCED", + "drift_count": 0, + "files": ["app1", "app2"] + }' +``` + +### Check health +```bash +curl http://localhost:5000/health +curl http://localhost:5000/ready +``` + +## Version + +1.0.0 diff --git a/app.py b/app.py new file mode 100644 index 0000000..d9f58f5 --- /dev/null +++ b/app.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +GitOps Status Server API +Simple Flask API for serving and updating status.json +Listens on port 5000 and handles GET/POST requests +""" +import os +import json +import logging +from flask import Flask, request, jsonify +from datetime import datetime + +app = Flask(__name__) + +# Configuration from environment +STATUS_FILE = os.environ.get('STATUS_FILE', '/data/status.json') +API_HOST = os.environ.get('API_HOST', '0.0.0.0') +API_PORT = int(os.environ.get('API_PORT', 5000)) + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def load_status(): + """Load the current status from file""" + try: + if os.path.exists(STATUS_FILE): + with open(STATUS_FILE, 'r') as f: + return json.load(f) + else: + logger.warning(f"Status file not found: {STATUS_FILE}") + return { + "repo": "unknown", + "server": "unknown", + "sync_status": "UNKNOWN", + "drift_count": 0, + "files": [], + "last_check": "" + } + except Exception as e: + logger.error(f"Error loading status: {e}") + return {} + + +def save_status(status): + """Save the status to file""" + try: + # Ensure directory exists + os.makedirs(os.path.dirname(STATUS_FILE), exist_ok=True) + + # Write with proper formatting + with open(STATUS_FILE, 'w') as f: + json.dump(status, f, indent=2) + + logger.info(f"Status updated: {status.get('repo', 'unknown')}/{status.get('server', 'unknown')} -> {status.get('sync_status', 'UNKNOWN')}") + return True + except Exception as e: + logger.error(f"Error saving status: {e}") + return False + + +@app.route('/status.json', methods=['GET']) +def get_status(): + """GET /status.json - Retrieve current status""" + try: + status = load_status() + if status: + return jsonify(status), 200 + else: + return jsonify({"error": "Failed to load status"}), 500 + except Exception as e: + logger.error(f"Error in GET /status.json: {e}") + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/status', methods=['GET', 'POST', 'OPTIONS']) +def api_status(): + """ + GET /api/status - Retrieve current status + POST /api/status - Update status with new data + """ + if request.method == 'OPTIONS': + return '', 204 + + if request.method == 'GET': + status = load_status() + return jsonify(status), 200 + + if request.method == 'POST': + try: + incoming_data = request.get_json() + if not incoming_data: + return jsonify({"error": "No JSON data provided"}), 400 + + # Load current status + status = load_status() + + # Update with incoming data (merge) + status.update(incoming_data) + + # Add/update timestamp if not present + if 'last_check' not in status or not status['last_check']: + status['last_check'] = datetime.utcnow().isoformat() + 'Z' + + # Save updated status + if save_status(status): + return jsonify({ + "success": True, + "message": "Status updated successfully", + "status": status + }), 200 + else: + return jsonify({ + "error": "Failed to save status" + }), 500 + + except json.JSONDecodeError: + return jsonify({"error": "Invalid JSON"}), 400 + except Exception as e: + logger.error(f"Error in POST /api/status: {e}") + return jsonify({"error": str(e)}), 500 + + +@app.route('/health', methods=['GET']) +def health(): + """GET /health - Kubernetes liveness probe""" + return jsonify({"status": "healthy"}), 200 + + +@app.route('/ready', methods=['GET']) +def ready(): + """GET /ready - Kubernetes readiness probe""" + try: + # Try to access the status file + if os.path.exists(STATUS_FILE): + status = load_status() + if isinstance(status, dict): + return jsonify({"status": "ready"}), 200 + return jsonify({"status": "not_ready", "reason": "status file not accessible"}), 503 + except Exception as e: + logger.error(f"Readiness check failed: {e}") + return jsonify({"status": "not_ready", "error": str(e)}), 503 + + +@app.route('/', methods=['GET']) +def root(): + """GET / - Simple info endpoint""" + return jsonify({ + "name": "GitOps Status API", + "version": "1.0.0", + "endpoints": { + "GET /status.json": "Retrieve current status", + "GET /api/status": "Retrieve current status (JSON API)", + "POST /api/status": "Update status with new data", + "GET /health": "Liveness probe", + "GET /ready": "Readiness probe" + } + }), 200 + + +if __name__ == '__main__': + logger.info(f"Starting GitOps Status API on {API_HOST}:{API_PORT}") + logger.info(f"Status file location: {STATUS_FILE}") + + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(STATUS_FILE) or '.', exist_ok=True) + + app.run(host=API_HOST, port=API_PORT, debug=False, threaded=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c209774 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.3.2 +Werkzeug==2.3.6