# gitops-status-server API Reference This document provides a reference implementation example for the gitops-status-server API endpoint that receives status updates from the rsyslog repository. ## API Endpoint Specification ### POST /api/status Receives GitOps status updates from repositories. **Request:** ``` POST /api/status HTTP/1.1 Host: gitops-status-server.observability-stack.svc.cluster.local:80 Content-Type: application/json { "repo": "rsyslog", "server": "rsyslog-lab", "sync_status": "OUT_OF_SYNC", "drift_count": 2, "files": [ { "name": "rsyslog.conf" }, { "name": "rsyslog.d/30-lab.conf" } ], "last_check": "2026-04-21T10:32:15Z" } ``` **Response (Success):** ``` HTTP/1.1 200 OK Content-Type: application/json { "status": "ok", "message": "Status updated successfully" } ``` **Response (Error):** ``` HTTP/1.1 400 Bad Request Content-Type: application/json { "status": "error", "message": "Invalid JSON payload" } ``` ## Example Implementation (Python/Flask) ```python from flask import Flask, request, jsonify from datetime import datetime import json import os app = Flask(__name__) # In-memory storage (replace with database in production) status_data = {} @app.route('/api/status', methods=['POST']) def update_status(): """Receive and store GitOps status updates""" try: data = request.get_json() # Validate required fields required_fields = ['repo', 'server', 'sync_status', 'drift_count', 'files', 'last_check'] for field in required_fields: if field not in data: return jsonify({ 'status': 'error', 'message': f'Missing required field: {field}' }), 400 # Validate sync_status value if data['sync_status'] not in ['SYNCED', 'OUT_OF_SYNC']: return jsonify({ 'status': 'error', 'message': 'sync_status must be SYNCED or OUT_OF_SYNC' }), 400 # Create unique key for this repo/server combination key = f"{data['repo']}:{data['server']}" # Store the status status_data[key] = { 'repo': data['repo'], 'server': data['server'], 'sync_status': data['sync_status'], 'drift_count': data['drift_count'], 'files': data['files'], 'last_check': data['last_check'], 'updated_at': datetime.utcnow().isoformat() + 'Z' } # Log the update print(f"Status update: {key} -> {data['sync_status']} (drift_count: {data['drift_count']})") return jsonify({ 'status': 'ok', 'message': 'Status updated successfully' }), 200 except Exception as e: print(f"Error processing status update: {e}") return jsonify({ 'status': 'error', 'message': str(e) }), 500 @app.route('/status.json', methods=['GET']) def get_status(): """Serve aggregated status for Grafana Infinity datasource""" # Convert dict to list for JSON array output statuses = list(status_data.values()) return jsonify(statuses), 200 @app.route('/health', methods=['GET']) def health(): """Health check endpoint""" return jsonify({ 'status': 'healthy', 'timestamp': datetime.utcnow().isoformat() + 'Z', 'tracked_repos': len(status_data) }), 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=8080) ``` ## Example Implementation (Go) ```go package main import ( "encoding/json" "fmt" "log" "net/http" "sync" "time" ) type StatusUpdate struct { Repo string `json:"repo"` Server string `json:"server"` SyncStatus string `json:"sync_status"` DriftCount int `json:"drift_count"` Files []File `json:"files"` LastCheck string `json:"last_check"` } type File struct { Name string `json:"name"` } type StoredStatus struct { StatusUpdate UpdatedAt string `json:"updated_at"` } var ( statusStore = make(map[string]StoredStatus) storeMutex sync.RWMutex ) func updateStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var status StatusUpdate if err := json.NewDecoder(r.Body).Decode(&status); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } // Validate sync_status if status.SyncStatus != "SYNCED" && status.SyncStatus != "OUT_OF_SYNC" { http.Error(w, "sync_status must be SYNCED or OUT_OF_SYNC", http.StatusBadRequest) return } // Store the status key := fmt.Sprintf("%s:%s", status.Repo, status.Server) stored := StoredStatus{ StatusUpdate: status, UpdatedAt: time.Now().UTC().Format(time.RFC3339), } storeMutex.Lock() statusStore[key] = stored storeMutex.Unlock() log.Printf("Status update: %s -> %s (drift_count: %d)", key, status.SyncStatus, status.DriftCount) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "ok", "message": "Status updated successfully", }) } func getStatusHandler(w http.ResponseWriter, r *http.Request) { storeMutex.RLock() statuses := make([]StoredStatus, 0, len(statusStore)) for _, status := range statusStore { statuses = append(statuses, status) } storeMutex.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(statuses) } func healthHandler(w http.ResponseWriter, r *http.Request) { storeMutex.RLock() count := len(statusStore) storeMutex.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "status": "healthy", "timestamp": time.Now().UTC().Format(time.RFC3339), "tracked_repos": count, }) } func main() { http.HandleFunc("/api/status", updateStatusHandler) http.HandleFunc("/status.json", getStatusHandler) http.HandleFunc("/health", healthHandler) log.Println("Starting gitops-status-server on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } ``` ## Testing the API ### Using curl **Send a status update:** ```bash curl -X POST http://localhost:8080/api/status \ -H "Content-Type: application/json" \ -d '{ "repo": "rsyslog", "server": "rsyslog-lab", "sync_status": "OUT_OF_SYNC", "drift_count": 2, "files": [ {"name": "rsyslog.conf"}, {"name": "rsyslog.d/30-lab.conf"} ], "last_check": "2026-04-21T10:32:15Z" }' ``` **Get all statuses:** ```bash curl http://localhost:8080/status.json ``` **Health check:** ```bash curl http://localhost:8080/health ``` ## Grafana Infinity Datasource Configuration 1. Install Grafana Infinity datasource plugin 2. Add new datasource: - Type: Infinity - URL: `http://gitops-status-server.observability-stack.svc.cluster.local:80` 3. Create a panel with query: - URL: `/status.json` - Parser: Backend - Format: Table Example query to show all repos: ``` Source: URL URL: /status.json Parser: Backend Format: Table Columns: - repo (string) - server (string) - sync_status (string) - drift_count (number) - last_check (time) ``` Example query to show drift details: ``` Source: URL URL: /status.json Parser: Backend Format: Table Root/Rows: $[?(@.drift_count > 0)] Columns: - repo (string) - server (string) - drift_count (number) - files (string, JSONata: $join(files.name, ', ')) ``` ## Notes - The example implementations use in-memory storage; production should use a database - Consider adding authentication/authorization for the POST endpoint - Add monitoring/metrics for the status server itself - Consider adding TTL/expiration for stale status entries - The `/status.json` endpoint should support filtering (e.g., by repo or server)