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

8.0 KiB

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)

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)

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:

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:

curl http://localhost:8080/status.json

Health check:

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)