rsyslog/GITOPS_STATUS_API_REFERENCE.md
dvirlabs 082ed0a0a4
Some checks failed
ci/woodpecker/cron/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
Migrate to infinity datasource
2026-04-21 04:54:47 +03:00

327 lines
8.0 KiB
Markdown

# 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)