327 lines
8.0 KiB
Markdown
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)
|