refactor the project to make it generic
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
58e31bed84
commit
f8fa847c11
656
INTEGRATION_EXAMPLES.md
Normal file
656
INTEGRATION_EXAMPLES.md
Normal file
@ -0,0 +1,656 @@
|
||||
# Integration Examples
|
||||
|
||||
This document provides integration examples for common CI/CD tools and automation frameworks.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Ansible Integration](#ansible-integration)
|
||||
- [Woodpecker CI Integration](#woodpecker-ci-integration)
|
||||
- [GitLab CI Integration](#gitlab-ci-integration)
|
||||
- [Cron Job for Drift Detection](#cron-job-for-drift-detection)
|
||||
- [Grafana Dashboard JSON](#grafana-dashboard-json)
|
||||
|
||||
---
|
||||
|
||||
## Ansible Integration
|
||||
|
||||
### Example: rsyslog Deployment Playbook
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Deploy rsyslog configuration
|
||||
hosts: rsyslog_servers
|
||||
vars:
|
||||
api_url: "http://gitops-status-api:5000"
|
||||
repo_name: "rsyslog"
|
||||
server_group: "rsyslog-{{ env }}"
|
||||
commit_sha: "{{ lookup('env', 'CI_COMMIT_SHA') | default('manual', true) }}"
|
||||
branch: "{{ lookup('env', 'CI_COMMIT_BRANCH') | default('unknown', true) }}"
|
||||
updated_by: "{{ lookup('env', 'CI_PIPELINE_SOURCE') | default('ansible', true) }}"
|
||||
|
||||
tasks:
|
||||
- name: Copy rsyslog configuration files
|
||||
copy:
|
||||
src: "files/{{ item }}"
|
||||
dest: "/etc/rsyslog.d/{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
loop:
|
||||
- 50-default.conf
|
||||
- 60-custom.conf
|
||||
register: copy_result
|
||||
|
||||
- name: Validate rsyslog configuration
|
||||
command: rsyslogd -N1
|
||||
register: validation
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Restart rsyslog if configuration changed
|
||||
service:
|
||||
name: rsyslog
|
||||
state: restarted
|
||||
when: copy_result.changed and validation.rc == 0
|
||||
|
||||
- name: Report status to API (success)
|
||||
uri:
|
||||
url: "{{ api_url }}/status/pipeline"
|
||||
method: POST
|
||||
body_format: json
|
||||
status_code: 200
|
||||
body:
|
||||
project_name: "gitops-for-servers"
|
||||
repo_name: "{{ repo_name }}"
|
||||
server_group: "{{ server_group }}"
|
||||
server_type: "rsyslog"
|
||||
environment: "{{ env }}"
|
||||
host: "{{ inventory_hostname }}"
|
||||
status: "synced"
|
||||
changed_files: "{{ copy_result.results | selectattr('changed') | map(attribute='dest') | list }}"
|
||||
validation_status: "{{ 'passed' if validation.rc == 0 else 'failed' }}"
|
||||
validation_message: "{{ validation.stderr if validation.rc != 0 else 'rsyslog syntax validation passed' }}"
|
||||
commit_sha: "{{ commit_sha }}"
|
||||
branch: "{{ branch }}"
|
||||
updated_by: "{{ updated_by }}"
|
||||
last_pipeline_run: "{{ ansible_date_time.iso8601 }}"
|
||||
when: validation.rc == 0
|
||||
|
||||
- name: Report status to API (failure)
|
||||
uri:
|
||||
url: "{{ api_url }}/status/pipeline"
|
||||
method: POST
|
||||
body_format: json
|
||||
status_code: 200
|
||||
body:
|
||||
project_name: "gitops-for-servers"
|
||||
repo_name: "{{ repo_name }}"
|
||||
server_group: "{{ server_group }}"
|
||||
server_type: "rsyslog"
|
||||
environment: "{{ env }}"
|
||||
host: "{{ inventory_hostname }}"
|
||||
status: "failed"
|
||||
changed_files: []
|
||||
validation_status: "failed"
|
||||
validation_message: "{{ validation.stderr }}"
|
||||
commit_sha: "{{ commit_sha }}"
|
||||
branch: "{{ branch }}"
|
||||
updated_by: "{{ updated_by }}"
|
||||
last_pipeline_run: "{{ ansible_date_time.iso8601 }}"
|
||||
when: validation.rc != 0
|
||||
|
||||
- name: Fail the playbook if validation failed
|
||||
fail:
|
||||
msg: "rsyslog configuration validation failed"
|
||||
when: validation.rc != 0
|
||||
```
|
||||
|
||||
### Example: nginx Deployment Playbook
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Deploy nginx configuration
|
||||
hosts: nginx_servers
|
||||
vars:
|
||||
api_url: "http://gitops-status-api:5000"
|
||||
|
||||
tasks:
|
||||
- name: Copy nginx configuration
|
||||
template:
|
||||
src: templates/nginx.conf.j2
|
||||
dest: /etc/nginx/nginx.conf
|
||||
register: nginx_config
|
||||
|
||||
- name: Validate nginx configuration
|
||||
command: nginx -t
|
||||
register: validation
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Reload nginx if valid
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
when: nginx_config.changed and validation.rc == 0
|
||||
|
||||
- name: Report to status API
|
||||
uri:
|
||||
url: "{{ api_url }}/status/pipeline"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
project_name: "gitops-for-servers"
|
||||
repo_name: "nginx"
|
||||
server_group: "nginx-{{ env }}"
|
||||
server_type: "nginx"
|
||||
environment: "{{ env }}"
|
||||
host: "{{ inventory_hostname }}"
|
||||
status: "{{ 'synced' if validation.rc == 0 else 'failed' }}"
|
||||
changed_files: "{{ ['/etc/nginx/nginx.conf'] if nginx_config.changed else [] }}"
|
||||
validation_status: "{{ 'passed' if validation.rc == 0 else 'failed' }}"
|
||||
validation_message: "{{ validation.stderr if validation.rc != 0 else 'nginx -t passed' }}"
|
||||
commit_sha: "{{ lookup('env', 'CI_COMMIT_SHA') | default('manual') }}"
|
||||
branch: "{{ lookup('env', 'CI_COMMIT_BRANCH') | default('unknown') }}"
|
||||
updated_by: "ansible"
|
||||
last_pipeline_run: "{{ ansible_date_time.iso8601 }}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Woodpecker CI Integration
|
||||
|
||||
### .woodpecker.yml Example
|
||||
|
||||
```yaml
|
||||
pipeline:
|
||||
validate:
|
||||
image: rsyslog/rsyslog:latest
|
||||
commands:
|
||||
- rsyslogd -N1 -f config/rsyslog.conf
|
||||
when:
|
||||
branch: master
|
||||
|
||||
deploy:
|
||||
image: ansible/ansible:latest
|
||||
secrets: [ansible_vault_password]
|
||||
commands:
|
||||
- ansible-playbook -i inventory/prod deploy.yml
|
||||
when:
|
||||
branch: master
|
||||
event: push
|
||||
|
||||
report-status:
|
||||
image: curlimages/curl:latest
|
||||
environment:
|
||||
- API_URL=http://gitops-status-api:5000
|
||||
commands:
|
||||
- |
|
||||
curl -X POST $API_URL/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"project_name\": \"gitops-for-servers\",
|
||||
\"repo_name\": \"rsyslog\",
|
||||
\"server_group\": \"rsyslog-prod\",
|
||||
\"server_type\": \"rsyslog\",
|
||||
\"environment\": \"prod\",
|
||||
\"host\": \"rsyslog-group\",
|
||||
\"status\": \"synced\",
|
||||
\"validation_status\": \"passed\",
|
||||
\"commit_sha\": \"$CI_COMMIT_SHA\",
|
||||
\"branch\": \"$CI_COMMIT_BRANCH\",
|
||||
\"updated_by\": \"woodpecker\",
|
||||
\"last_pipeline_run\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
|
||||
}"
|
||||
when:
|
||||
status: success
|
||||
|
||||
report-failure:
|
||||
image: curlimages/curl:latest
|
||||
environment:
|
||||
- API_URL=http://gitops-status-api:5000
|
||||
commands:
|
||||
- |
|
||||
curl -X POST $API_URL/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"project_name\": \"gitops-for-servers\",
|
||||
\"repo_name\": \"rsyslog\",
|
||||
\"server_group\": \"rsyslog-prod\",
|
||||
\"server_type\": \"rsyslog\",
|
||||
\"environment\": \"prod\",
|
||||
\"host\": \"rsyslog-group\",
|
||||
\"status\": \"failed\",
|
||||
\"validation_status\": \"failed\",
|
||||
\"validation_message\": \"Pipeline failed\",
|
||||
\"commit_sha\": \"$CI_COMMIT_SHA\",
|
||||
\"branch\": \"$CI_COMMIT_BRANCH\",
|
||||
\"updated_by\": \"woodpecker\",
|
||||
\"last_pipeline_run\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
|
||||
}"
|
||||
when:
|
||||
status: failure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GitLab CI Integration
|
||||
|
||||
### .gitlab-ci.yml Example
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- validate
|
||||
- deploy
|
||||
- report
|
||||
|
||||
variables:
|
||||
API_URL: "http://gitops-status-api:5000"
|
||||
SERVER_GROUP: "rsyslog-prod"
|
||||
SERVER_TYPE: "rsyslog"
|
||||
|
||||
validate:
|
||||
stage: validate
|
||||
image: rsyslog/rsyslog:latest
|
||||
script:
|
||||
- rsyslogd -N1 -f config/rsyslog.conf
|
||||
only:
|
||||
- master
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: ansible/ansible:latest
|
||||
script:
|
||||
- ansible-playbook -i inventory/prod deploy.yml
|
||||
only:
|
||||
- master
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: deploy.env
|
||||
|
||||
report_success:
|
||||
stage: report
|
||||
image: curlimages/curl:latest
|
||||
script:
|
||||
- |
|
||||
curl -X POST $API_URL/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"project_name\": \"gitops-for-servers\",
|
||||
\"repo_name\": \"$CI_PROJECT_NAME\",
|
||||
\"server_group\": \"$SERVER_GROUP\",
|
||||
\"server_type\": \"$SERVER_TYPE\",
|
||||
\"environment\": \"prod\",
|
||||
\"host\": \"${SERVER_GROUP}-group\",
|
||||
\"status\": \"synced\",
|
||||
\"validation_status\": \"passed\",
|
||||
\"commit_sha\": \"$CI_COMMIT_SHA\",
|
||||
\"branch\": \"$CI_COMMIT_BRANCH\",
|
||||
\"updated_by\": \"gitlab-ci\",
|
||||
\"last_pipeline_run\": \"$CI_PIPELINE_CREATED_AT\"
|
||||
}"
|
||||
when: on_success
|
||||
only:
|
||||
- master
|
||||
|
||||
report_failure:
|
||||
stage: report
|
||||
image: curlimages/curl:latest
|
||||
script:
|
||||
- |
|
||||
curl -X POST $API_URL/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"project_name\": \"gitops-for-servers\",
|
||||
\"repo_name\": \"$CI_PROJECT_NAME\",
|
||||
\"server_group\": \"$SERVER_GROUP\",
|
||||
\"server_type\": \"$SERVER_TYPE\",
|
||||
\"environment\": \"prod\",
|
||||
\"host\": \"${SERVER_GROUP}-group\",
|
||||
\"status\": \"failed\",
|
||||
\"validation_status\": \"failed\",
|
||||
\"validation_message\": \"Pipeline failed at $CI_JOB_NAME\",
|
||||
\"commit_sha\": \"$CI_COMMIT_SHA\",
|
||||
\"branch\": \"$CI_COMMIT_BRANCH\",
|
||||
\"updated_by\": \"gitlab-ci\",
|
||||
\"last_pipeline_run\": \"$CI_PIPELINE_CREATED_AT\"
|
||||
}"
|
||||
when: on_failure
|
||||
only:
|
||||
- master
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cron Job for Drift Detection
|
||||
|
||||
### Bash Script: check_rsyslog_drift.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
#
|
||||
# Drift detection script for rsyslog
|
||||
# Run this via cron every 15 minutes
|
||||
#
|
||||
# Crontab entry:
|
||||
# */15 * * * * /usr/local/bin/check_rsyslog_drift.sh
|
||||
|
||||
set -e
|
||||
|
||||
API_URL="${API_URL:-http://gitops-status-api:5000}"
|
||||
SERVER_GROUP="rsyslog-prod"
|
||||
SERVER_TYPE="rsyslog"
|
||||
HOST=$(hostname)
|
||||
|
||||
DEPLOYED_CONFIG_DIR="/opt/gitops/deployed/rsyslog"
|
||||
ACTIVE_CONFIG_DIR="/etc/rsyslog.d"
|
||||
|
||||
# Compare deployed vs active configuration
|
||||
CHANGED_FILES=()
|
||||
|
||||
for file in "$DEPLOYED_CONFIG_DIR"/*.conf; do
|
||||
filename=$(basename "$file")
|
||||
active_file="$ACTIVE_CONFIG_DIR/$filename"
|
||||
|
||||
if [ ! -f "$active_file" ]; then
|
||||
CHANGED_FILES+=("$active_file")
|
||||
elif ! diff -q "$file" "$active_file" > /dev/null 2>&1; then
|
||||
CHANGED_FILES+=("$active_file")
|
||||
fi
|
||||
done
|
||||
|
||||
# Determine status
|
||||
if [ ${#CHANGED_FILES[@]} -eq 0 ]; then
|
||||
STATUS="synced"
|
||||
else
|
||||
STATUS="out_of_sync"
|
||||
fi
|
||||
|
||||
# Build JSON array of changed files
|
||||
CHANGED_JSON="[]"
|
||||
if [ ${#CHANGED_FILES[@]} -gt 0 ]; then
|
||||
CHANGED_JSON=$(printf '%s\n' "${CHANGED_FILES[@]}" | jq -R . | jq -s .)
|
||||
fi
|
||||
|
||||
# Report to API
|
||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
curl -X POST "$API_URL/status/cron" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"server_group\": \"$SERVER_GROUP\",
|
||||
\"host\": \"$HOST\",
|
||||
\"status\": \"$STATUS\",
|
||||
\"changed_files\": $CHANGED_JSON,
|
||||
\"last_cron_check\": \"$TIMESTAMP\"
|
||||
}"
|
||||
|
||||
# Log the result
|
||||
logger -t gitops-drift "Drift check: $STATUS (${#CHANGED_FILES[@]} files changed)"
|
||||
|
||||
# Exit with error if drift detected (for alerting)
|
||||
if [ "$STATUS" == "out_of_sync" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
```
|
||||
|
||||
### Python Script: check_drift.py
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generic drift detection script
|
||||
Works with any server type
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
API_URL = os.environ.get('API_URL', 'http://gitops-status-api:5000')
|
||||
SERVER_GROUP = os.environ.get('SERVER_GROUP', 'unknown')
|
||||
HOST = os.environ.get('HOSTNAME', os.uname().nodename)
|
||||
DEPLOYED_DIR = os.environ.get('DEPLOYED_DIR', '/opt/gitops/deployed')
|
||||
ACTIVE_DIR = os.environ.get('ACTIVE_DIR', '/etc')
|
||||
|
||||
def file_hash(filepath):
|
||||
"""Calculate SHA256 hash of file"""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
sha256.update(chunk)
|
||||
return sha256.hexdigest()
|
||||
|
||||
def check_drift():
|
||||
"""Compare deployed vs active configuration"""
|
||||
changed_files = []
|
||||
|
||||
deployed_path = Path(DEPLOYED_DIR)
|
||||
active_path = Path(ACTIVE_DIR)
|
||||
|
||||
if not deployed_path.exists():
|
||||
print(f"Deployed directory not found: {DEPLOYED_DIR}")
|
||||
return changed_files
|
||||
|
||||
for deployed_file in deployed_path.rglob('*'):
|
||||
if deployed_file.is_file():
|
||||
# Calculate relative path
|
||||
rel_path = deployed_file.relative_to(deployed_path)
|
||||
active_file = active_path / rel_path
|
||||
|
||||
# Check if active file exists
|
||||
if not active_file.exists():
|
||||
changed_files.append(str(active_file))
|
||||
continue
|
||||
|
||||
# Compare file hashes
|
||||
if file_hash(deployed_file) != file_hash(active_file):
|
||||
changed_files.append(str(active_file))
|
||||
|
||||
return changed_files
|
||||
|
||||
def report_status(status, changed_files):
|
||||
"""Report status to API"""
|
||||
timestamp = datetime.utcnow().isoformat() + 'Z'
|
||||
|
||||
payload = {
|
||||
'server_group': SERVER_GROUP,
|
||||
'host': HOST,
|
||||
'status': status,
|
||||
'changed_files': changed_files,
|
||||
'last_cron_check': timestamp
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{API_URL}/status/cron',
|
||||
json=payload,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"Status reported: {status}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to report status: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
changed_files = check_drift()
|
||||
|
||||
if len(changed_files) == 0:
|
||||
status = 'synced'
|
||||
print("No drift detected")
|
||||
else:
|
||||
status = 'out_of_sync'
|
||||
print(f"Drift detected: {len(changed_files)} files changed")
|
||||
for f in changed_files:
|
||||
print(f" - {f}")
|
||||
|
||||
if report_status(status, changed_files):
|
||||
sys.exit(0 if status == 'synced' else 1)
|
||||
else:
|
||||
sys.exit(2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
### Systemd Timer for Drift Detection
|
||||
|
||||
**File: /etc/systemd/system/gitops-drift-check.service**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GitOps Configuration Drift Check
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/check_drift.py
|
||||
Environment="API_URL=http://gitops-status-api:5000"
|
||||
Environment="SERVER_GROUP=rsyslog-prod"
|
||||
Environment="DEPLOYED_DIR=/opt/gitops/deployed/rsyslog"
|
||||
Environment="ACTIVE_DIR=/etc/rsyslog.d"
|
||||
User=root
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
```
|
||||
|
||||
**File: /etc/systemd/system/gitops-drift-check.timer**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Run GitOps drift check every 15 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=15min
|
||||
AccuracySec=1min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
**Enable the timer:**
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable gitops-drift-check.timer
|
||||
systemctl start gitops-drift-check.timer
|
||||
systemctl status gitops-drift-check.timer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grafana Dashboard JSON
|
||||
|
||||
### Simple Status Table Dashboard
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "GitOps Server Status",
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Server Status Overview",
|
||||
"type": "table",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"type": "json",
|
||||
"source": "url",
|
||||
"url": "/status",
|
||||
"method": "GET",
|
||||
"parser": "backend"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "status" },
|
||||
"properties": [
|
||||
{
|
||||
"id": "mappings",
|
||||
"value": [
|
||||
{
|
||||
"type": "value",
|
||||
"value": "synced",
|
||||
"text": "✓ Synced",
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"type": "value",
|
||||
"value": "out_of_sync",
|
||||
"text": "⚠ Out of Sync",
|
||||
"color": "red"
|
||||
},
|
||||
{
|
||||
"type": "value",
|
||||
"value": "failed",
|
||||
"text": "✗ Failed",
|
||||
"color": "dark-red"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Servers Out of Sync",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"type": "json",
|
||||
"source": "url",
|
||||
"url": "/status",
|
||||
"method": "GET",
|
||||
"parser": "backend",
|
||||
"filterExpression": "status == 'out_of_sync'"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"graphMode": "none",
|
||||
"colorMode": "value",
|
||||
"reduceOptions": {
|
||||
"values": false,
|
||||
"calcs": ["count"]
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "value": 0, "color": "green" },
|
||||
{ "value": 1, "color": "red" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
These integration examples show how to:
|
||||
|
||||
1. **Ansible**: Report deployment status from playbooks
|
||||
2. **CI/CD**: Integrate with Woodpecker, GitLab, or any CI system
|
||||
3. **Cron**: Detect configuration drift periodically
|
||||
4. **Grafana**: Visualize status across all servers
|
||||
|
||||
Adapt these examples to your specific environment and server types.
|
||||
514
README.md
514
README.md
@ -1,14 +1,374 @@
|
||||
# GitOps Status API
|
||||
# GitOps Status API v2.0
|
||||
|
||||
Simple Flask API for serving and updating GitOps status information.
|
||||
**Generic multi-server status API for GitOps deployments**
|
||||
|
||||
## Features
|
||||
A centralized status API that collects and exposes sync/drift status from multiple server types and groups. Designed to work with GitOps workflows and provide Grafana-friendly monitoring.
|
||||
|
||||
- **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
|
||||
## Purpose
|
||||
|
||||
This API serves as a central status hub for GitOps-managed servers across your infrastructure:
|
||||
|
||||
- **Multi-server type support**: rsyslog, Splunk, IBM ITNM, nginx, or any future server type
|
||||
- **Separation of concerns**: API only stores and exposes data - validation logic lives in each server's Ansible/config repo
|
||||
- **Pipeline + Cron updates**: Separate endpoints for deployment status vs drift detection
|
||||
- **Grafana integration**: Optimized output format for Grafana Infinity datasource
|
||||
- **Backward compatible**: Existing rsyslog workflows continue to work
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Server Repo 1 │
|
||||
│ (rsyslog) │──┐
|
||||
└─────────────────────┘ │
|
||||
│
|
||||
┌─────────────────────┐ │ ┌──────────────────┐ ┌─────────────┐
|
||||
│ Server Repo 2 │ ├───►│ Status API │◄──────│ Grafana │
|
||||
│ (Splunk) │──┤ │ (This Project) │ │ Dashboard │
|
||||
└─────────────────────┘ │ └──────────────────┘ └─────────────┘
|
||||
│
|
||||
┌─────────────────────┐ │
|
||||
│ Server Repo 3 │ │
|
||||
│ (nginx) │──┘
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
Each server repository:
|
||||
1. Runs validation in its own Ansible playbook/CI pipeline
|
||||
2. Sends status updates to this API via POST /status/pipeline
|
||||
3. Optionally runs cron drift checks that POST to /status/cron
|
||||
|
||||
Grafana queries this API via GET /status to visualize sync status across all servers.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Pipeline Updates (Full Status)
|
||||
|
||||
**POST /status/pipeline**
|
||||
|
||||
Updates complete deployment status from CI/CD pipeline. Use this when deploying configuration changes.
|
||||
|
||||
**Required fields:**
|
||||
- `server_group` - logical grouping (e.g., "rsyslog-prod")
|
||||
- `host` - hostname (e.g., "rsyslog-01")
|
||||
- `status` - one of: synced, out_of_sync, failed, unknown
|
||||
|
||||
**Optional fields:**
|
||||
- `project_name` - project identifier
|
||||
- `repo_name` - repository name
|
||||
- `server_type` - type of server (rsyslog, splunk, nginx, etc.)
|
||||
- `environment` - environment (prod, staging, dev, etc.)
|
||||
- `changed_files` - array of changed file paths
|
||||
- `validation_status` - validation result (passed, failed, skipped)
|
||||
- `validation_message` - human-readable validation output
|
||||
- `commit_sha` - git commit hash
|
||||
- `branch` - git branch name
|
||||
- `updated_by` - who/what triggered the update (e.g., "woodpecker", "jenkins")
|
||||
- `last_pipeline_run` - ISO 8601 timestamp of pipeline execution
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "rsyslog syntax validation passed",
|
||||
"commit_sha": "abc1234567890",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker",
|
||||
"last_pipeline_run": "2026-04-26T10:30:00Z"
|
||||
}'
|
||||
```
|
||||
|
||||
### Cron Updates (Drift Detection)
|
||||
|
||||
**POST /status/cron**
|
||||
|
||||
Updates only drift-related fields. Does **NOT** overwrite commit_sha, validation results, or other pipeline metadata.
|
||||
|
||||
**Required fields:**
|
||||
- `server_group`
|
||||
- `host`
|
||||
|
||||
**Optional fields:**
|
||||
- `status` - current drift status
|
||||
- `changed_files` - files that drifted
|
||||
- `last_cron_check` - ISO 8601 timestamp
|
||||
|
||||
**Important:** The server must exist (created via /status/pipeline first).
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/status/cron \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "rsyslog-prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"last_cron_check": "2026-04-26T11:00:00Z"
|
||||
}'
|
||||
```
|
||||
|
||||
If drift detected:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/status/cron \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "rsyslog-prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "out_of_sync",
|
||||
"changed_files": ["/etc/rsyslog.conf", "/etc/rsyslog.d/50-default.conf"],
|
||||
"last_cron_check": "2026-04-26T11:00:00Z"
|
||||
}'
|
||||
```
|
||||
|
||||
### Query Endpoints
|
||||
|
||||
**GET /status**
|
||||
|
||||
Returns all servers in Grafana-friendly flat array format.
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files_count": 0,
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "rsyslog syntax validation passed",
|
||||
"last_pipeline_run": "2026-04-26T10:30:00Z",
|
||||
"last_cron_check": "2026-04-26T11:00:00Z",
|
||||
"commit_sha": "abc1234567890",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker",
|
||||
"timestamp": "2026-04-26T11:00:00Z"
|
||||
},
|
||||
{
|
||||
"server_group": "splunk-prod",
|
||||
"server_type": "splunk",
|
||||
"host": "splunk-indexer-01",
|
||||
"status": "synced",
|
||||
...
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**GET /status/{server_group}**
|
||||
|
||||
Returns all servers in a specific server group.
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/status/rsyslog-prod
|
||||
```
|
||||
|
||||
**GET /status/{server_group}/{host}**
|
||||
|
||||
Returns status for a specific server.
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/status/rsyslog-prod/rsyslog-01
|
||||
```
|
||||
|
||||
### Legacy Endpoints (Backward Compatibility)
|
||||
|
||||
**GET /api/status** and **POST /api/status**
|
||||
|
||||
Supports the old v1.x format for existing rsyslog workflows.
|
||||
|
||||
If you have existing pipelines using the old format, they will continue to work. The API automatically migrates old data to the new structure.
|
||||
|
||||
### Health Checks
|
||||
|
||||
**GET /health** - Liveness probe (always returns 200)
|
||||
|
||||
**GET /ready** - Readiness probe (checks file system)
|
||||
|
||||
## Grafana Infinity Configuration
|
||||
|
||||
### Setup
|
||||
|
||||
1. Install [Infinity datasource](https://grafana.com/grafana/plugins/yesoreyeram-infinity-datasource/) in Grafana
|
||||
2. Add a new Infinity datasource pointing to your API:
|
||||
- URL: `http://gitops-status-api:5000`
|
||||
- Type: JSON
|
||||
|
||||
### Example Query
|
||||
|
||||
Create a new panel with these settings:
|
||||
|
||||
- **Type:** JSON
|
||||
- **Source:** URL
|
||||
- **URL:** `/status`
|
||||
- **Method:** GET
|
||||
- **Parser:** Backend
|
||||
- **Root/Rows selector:** Leave empty (already flat array)
|
||||
|
||||
### Visualization Examples
|
||||
|
||||
**Table Panel:**
|
||||
- Show all servers with status, last check times, validation results
|
||||
|
||||
**Stat Panel:**
|
||||
- Filter by `status == "out_of_sync"` to show drift count
|
||||
- Use threshold colors (green for 0, red for > 0)
|
||||
|
||||
**Bar Gauge:**
|
||||
- Group by `server_group` and count `status` values
|
||||
|
||||
**Example Alert:**
|
||||
```
|
||||
Alert when changed_files_count > 0
|
||||
```
|
||||
|
||||
## Adding a New Server Type
|
||||
|
||||
This API is generic - no code changes needed to support new server types.
|
||||
|
||||
### Steps:
|
||||
|
||||
1. **Create your server config repository** (e.g., `nginx-config`)
|
||||
|
||||
2. **Add validation in your Ansible playbook:**
|
||||
|
||||
```yaml
|
||||
- name: Validate nginx config
|
||||
command: nginx -t
|
||||
register: validation_result
|
||||
|
||||
- name: Send status to API
|
||||
uri:
|
||||
url: "http://gitops-status-api:5000/status/pipeline"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
project_name: "gitops-for-servers"
|
||||
repo_name: "nginx"
|
||||
server_group: "nginx-prod"
|
||||
server_type: "nginx"
|
||||
environment: "prod"
|
||||
host: "{{ inventory_hostname }}"
|
||||
status: "{{ 'synced' if validation_result.rc == 0 else 'failed' }}"
|
||||
validation_status: "{{ 'passed' if validation_result.rc == 0 else 'failed' }}"
|
||||
validation_message: "{{ validation_result.stderr }}"
|
||||
commit_sha: "{{ lookup('env', 'CI_COMMIT_SHA') }}"
|
||||
branch: "{{ lookup('env', 'CI_COMMIT_BRANCH') }}"
|
||||
updated_by: "woodpecker"
|
||||
```
|
||||
|
||||
3. **Optional: Add drift detection cron job:**
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Check if local config matches deployed config
|
||||
if diff /etc/nginx/nginx.conf /opt/deployed/nginx.conf; then
|
||||
STATUS="synced"
|
||||
CHANGED=[]
|
||||
else
|
||||
STATUS="out_of_sync"
|
||||
CHANGED='["/etc/nginx/nginx.conf"]'
|
||||
fi
|
||||
|
||||
curl -X POST http://gitops-status-api:5000/status/cron \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"server_group\": \"nginx-prod\",
|
||||
\"host\": \"$(hostname)\",
|
||||
\"status\": \"$STATUS\",
|
||||
\"changed_files\": $CHANGED,
|
||||
\"last_cron_check\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
|
||||
}"
|
||||
```
|
||||
|
||||
That's it! No changes to the API needed.
|
||||
|
||||
## Working with Multiple Repositories
|
||||
|
||||
Each repository can manage different server types:
|
||||
|
||||
```
|
||||
gitops-for-servers/
|
||||
├── rsyslog/ # Repository for rsyslog servers
|
||||
│ ├── playbook.yml # Includes validation + API update
|
||||
│ └── ...
|
||||
├── splunk/ # Repository for Splunk servers
|
||||
│ ├── playbook.yml
|
||||
│ └── ...
|
||||
├── nginx/ # Repository for nginx servers
|
||||
│ ├── playbook.yml
|
||||
│ └── ...
|
||||
└── ibm-itnm/ # Repository for IBM ITNM
|
||||
├── playbook.yml
|
||||
└── ...
|
||||
```
|
||||
|
||||
Each repository independently:
|
||||
1. Validates its own configuration (syntax, linting, etc.)
|
||||
2. Posts status to the shared API
|
||||
3. Uses consistent field names but server-specific values
|
||||
|
||||
The API automatically handles all server types without configuration.
|
||||
|
||||
## Storage Format
|
||||
|
||||
Data is stored in a JSON file (`/data/status.json` by default):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"servers": {
|
||||
"rsyslog-prod/rsyslog-01": {
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "rsyslog syntax validation passed",
|
||||
"commit_sha": "abc1234",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker",
|
||||
"last_pipeline_run": "2026-04-26T10:30:00Z",
|
||||
"last_cron_check": "2026-04-26T11:00:00Z",
|
||||
"timestamp": "2026-04-26T11:00:00Z"
|
||||
},
|
||||
"rsyslog-prod/rsyslog-02": { ... },
|
||||
"splunk-prod/splunk-indexer-01": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Updates are safe:
|
||||
- New servers are added without affecting existing ones
|
||||
- Pipeline updates overwrite deployment fields only
|
||||
- Cron updates modify drift fields only
|
||||
- Concurrent updates are handled by Flask's threading
|
||||
|
||||
## Local Development
|
||||
|
||||
@ -19,68 +379,128 @@ 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"}'
|
||||
# Test pipeline update
|
||||
curl -X POST http://localhost:5000/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"server_group":"test","host":"test-01","status":"synced"}'
|
||||
|
||||
# Test cron update
|
||||
curl -X POST http://localhost:5000/status/cron \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"server_group":"test","host":"test-01","status":"synced"}'
|
||||
|
||||
# Get all status
|
||||
curl http://localhost:5000/status
|
||||
|
||||
# Get specific server
|
||||
curl http://localhost:5000/status/test/test-01
|
||||
```
|
||||
|
||||
## Docker Build
|
||||
## Docker Build & Deploy
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t gitops-status-api:1.0.0 .
|
||||
docker build -t gitops-status-api:2.0.0 .
|
||||
|
||||
# Run locally
|
||||
docker run -it -p 5000:5000 -v /tmp/data:/data gitops-status-api:1.0.0
|
||||
# Run locally with persistent storage
|
||||
docker run -d \
|
||||
-p 5000:5000 \
|
||||
-v /var/lib/gitops-status:/data \
|
||||
--name gitops-status-api \
|
||||
gitops-status-api:2.0.0
|
||||
|
||||
# Test
|
||||
curl http://localhost:5000/
|
||||
```
|
||||
|
||||
## Push to Harbor
|
||||
## Push to Container Registry
|
||||
|
||||
```bash
|
||||
# Login to Harbor
|
||||
# Login to your registry (Harbor, Docker Hub, etc.)
|
||||
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
|
||||
# Tag for registry
|
||||
docker tag gitops-status-api:2.0.0 harbor.your-domain.com/gitops/status-api:2.0.0
|
||||
docker tag gitops-status-api:2.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
|
||||
# Push
|
||||
docker push harbor.your-domain.com/gitops/status-api:2.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)
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `API_HOST` | `0.0.0.0` | Listen address |
|
||||
| `API_PORT` | `5000` | Listen port |
|
||||
| `STATUS_FILE` | `/data/status.json` | Path to status data file |
|
||||
| `FLASK_ENV` | `production` | Flask environment |
|
||||
|
||||
## API Examples
|
||||
## Kubernetes Deployment Example
|
||||
|
||||
### Get status
|
||||
```bash
|
||||
curl http://localhost:5000/status.json
|
||||
curl http://localhost:5000/api/status
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gitops-status-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitops-status-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitops-status-api
|
||||
spec:
|
||||
containers:
|
||||
- name: api
|
||||
image: harbor.your-domain.com/gitops/status-api:2.0.0
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 5000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 5000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: gitops-status-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitops-status-api
|
||||
spec:
|
||||
selector:
|
||||
app: gitops-status-api
|
||||
ports:
|
||||
- port: 5000
|
||||
targetPort: 5000
|
||||
```
|
||||
|
||||
### 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"]
|
||||
}'
|
||||
```
|
||||
## Migration from v1.x
|
||||
|
||||
### Check health
|
||||
```bash
|
||||
curl http://localhost:5000/health
|
||||
curl http://localhost:5000/ready
|
||||
```
|
||||
If you have an existing v1.x deployment:
|
||||
|
||||
1. **Automatic migration:** The API automatically detects and migrates old format on first load
|
||||
2. **Legacy endpoints work:** Existing `/api/status` calls continue to function
|
||||
3. **Update pipelines gradually:** Migrate to new `/status/pipeline` endpoint when ready
|
||||
|
||||
No data loss - old status.json is converted to new format automatically.
|
||||
|
||||
## Version
|
||||
|
||||
1.0.0
|
||||
2.0.0
|
||||
|
||||
275
REFACTORING_SUMMARY.md
Normal file
275
REFACTORING_SUMMARY.md
Normal file
@ -0,0 +1,275 @@
|
||||
# Refactoring Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Successfully refactored the GitOps Status API from a single-server, rsyslog-specific implementation to a generic, multi-server status API supporting any server type.
|
||||
|
||||
## Version Change
|
||||
|
||||
- **From:** v1.0.0 (single server, rsyslog-specific)
|
||||
- **To:** v2.0.0 (multi-server, generic)
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 1. Data Model
|
||||
|
||||
**Old (v1.x):**
|
||||
```json
|
||||
{
|
||||
"repo": "rsyslog",
|
||||
"server": "rsyslog-lab",
|
||||
"sync_status": "SYNCED",
|
||||
"drift_count": 0,
|
||||
"deployed_files": [],
|
||||
"drifted_files": [],
|
||||
"last_check": "2026-04-26T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**New (v2.0):**
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"servers": {
|
||||
"rsyslog-prod/rsyslog-01": {
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "",
|
||||
"commit_sha": "abc1234",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker",
|
||||
"last_pipeline_run": "2026-04-26T10:30:00Z",
|
||||
"last_cron_check": "2026-04-26T11:00:00Z",
|
||||
"timestamp": "2026-04-26T11:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. New Endpoints
|
||||
|
||||
**Pipeline Updates (Full Status):**
|
||||
```
|
||||
POST /status/pipeline
|
||||
```
|
||||
Updates complete deployment status including validation, commit info, and changed files.
|
||||
|
||||
**Cron Updates (Drift Detection):**
|
||||
```
|
||||
POST /status/cron
|
||||
```
|
||||
Updates only drift-related fields without overwriting pipeline metadata.
|
||||
|
||||
**Query Endpoints:**
|
||||
```
|
||||
GET /status # All servers (Grafana-friendly)
|
||||
GET /status/{server_group} # Servers in a group
|
||||
GET /status/{server_group}/{host} # Specific server
|
||||
```
|
||||
|
||||
**Legacy Endpoints (Backward Compatible):**
|
||||
```
|
||||
GET /api/status
|
||||
POST /api/status
|
||||
```
|
||||
Automatically converts between old and new formats.
|
||||
|
||||
### 3. Separation of Concerns
|
||||
|
||||
**Pipeline updates** handle:
|
||||
- Deployment status
|
||||
- Validation results
|
||||
- Commit SHA and branch
|
||||
- Changed files from deployment
|
||||
- Last pipeline run timestamp
|
||||
|
||||
**Cron updates** handle:
|
||||
- Drift detection status
|
||||
- Changed files from drift
|
||||
- Last cron check timestamp
|
||||
- Does NOT overwrite: commit_sha, validation_status, branch, etc.
|
||||
|
||||
### 4. Multi-Server Support
|
||||
|
||||
The API now supports unlimited servers of any type:
|
||||
- rsyslog
|
||||
- Splunk
|
||||
- IBM ITNM
|
||||
- nginx
|
||||
- Any future server type
|
||||
|
||||
No code changes needed to add new server types - just send the appropriate payload.
|
||||
|
||||
### 5. Grafana Integration
|
||||
|
||||
The `GET /status` endpoint returns a flat array optimized for Grafana Infinity datasource:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files_count": 0,
|
||||
"validation_status": "passed",
|
||||
...
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 6. Backward Compatibility
|
||||
|
||||
- Old `/api/status` endpoints still work
|
||||
- Automatic migration from v1 to v2 format on first load
|
||||
- Legacy payloads are converted to new structure automatically
|
||||
- Existing rsyslog workflows continue without changes
|
||||
|
||||
## Testing Results
|
||||
|
||||
All tests passed successfully:
|
||||
|
||||
✅ **Health checks**: `/health` and `/ready` working
|
||||
✅ **Pipeline updates**: Multiple server types (rsyslog, nginx) added successfully
|
||||
✅ **Cron updates**: Drift detection without overwriting pipeline data
|
||||
✅ **Query endpoints**: GET /status returns Grafana-friendly array
|
||||
✅ **Group queries**: GET /status/{server_group} filters correctly
|
||||
✅ **Host queries**: GET /status/{server_group}/{host} returns specific server
|
||||
✅ **Legacy API**: Old format accepted and converted to new format
|
||||
✅ **Data persistence**: Status stored in `/data/status.json` safely
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Pipeline Update (rsyslog)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/status/pipeline \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"validation_status": "passed",
|
||||
"validation_message": "rsyslog syntax validation passed",
|
||||
"commit_sha": "abc1234",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker"
|
||||
}'
|
||||
```
|
||||
|
||||
### Cron Update (drift detected)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/status/cron \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "rsyslog-prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "out_of_sync",
|
||||
"changed_files": ["/etc/rsyslog.conf", "/etc/rsyslog.d/50-default.conf"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Get All Status
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/status
|
||||
```
|
||||
|
||||
### Get Server Group
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/status/rsyslog-prod
|
||||
```
|
||||
|
||||
### Get Specific Server
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/status/rsyslog-prod/rsyslog-01
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **app.py** - Complete refactor with new data model and endpoints
|
||||
2. **README.md** - Comprehensive documentation update
|
||||
3. **requirements.txt** - Updated Flask to v3.x for Python 3.14 compatibility
|
||||
4. **test_api.sh** - New comprehensive test script (created)
|
||||
5. **INTEGRATION_EXAMPLES.md** - Integration examples for Ansible, CI/CD, Cron (created)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Existing rsyslog Deployments
|
||||
|
||||
**Option 1: Continue using legacy endpoint**
|
||||
No changes needed - keep using `/api/status`
|
||||
|
||||
**Option 2: Migrate to new endpoint**
|
||||
Update your pipeline to use `/status/pipeline` with new field names:
|
||||
- `sync_status` → `status` (lowercase: synced/out_of_sync/failed/unknown)
|
||||
- `last_check` → `last_pipeline_run`
|
||||
- Add new fields: `server_type`, `environment`, `validation_status`, etc.
|
||||
|
||||
### For New Server Types
|
||||
|
||||
1. Create server config repository
|
||||
2. Add validation in Ansible playbook or CI pipeline
|
||||
3. POST to `/status/pipeline` with appropriate fields
|
||||
4. Optionally add cron drift detection using `/status/cron`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Test with current rsyslog flow**
|
||||
- Run existing rsyslog pipeline
|
||||
- Verify backward compatibility
|
||||
- Gradually migrate to new endpoint
|
||||
|
||||
2. **Add other server types**
|
||||
- Splunk
|
||||
- IBM ITNM
|
||||
- nginx
|
||||
- Others as needed
|
||||
|
||||
3. **Set up Grafana dashboard**
|
||||
- Install Infinity datasource
|
||||
- Point to `GET /status`
|
||||
- Create visualizations for sync status
|
||||
|
||||
4. **Deploy to production**
|
||||
- Build Docker image
|
||||
- Push to container registry
|
||||
- Deploy to Kubernetes
|
||||
- Configure persistent volume for `/data`
|
||||
|
||||
## Notes
|
||||
|
||||
- All datetime objects now use timezone-aware UTC format
|
||||
- Fixed deprecation warnings for Python 3.14 compatibility
|
||||
- Storage remains simple JSON file for easy debugging
|
||||
- Thread-safe updates via Flask's threading
|
||||
- Comprehensive error handling and validation
|
||||
- API version displayed in root endpoint response
|
||||
|
||||
## Success Criteria Met
|
||||
|
||||
✅ Generic JSON model supporting any server type
|
||||
✅ Separate pipeline and cron endpoints
|
||||
✅ Backward compatible with existing rsyslog flow
|
||||
✅ Grafana-friendly output format
|
||||
✅ Simple, safe JSON storage
|
||||
✅ Comprehensive documentation
|
||||
✅ Example integrations for Ansible, CI/CD, Cron
|
||||
✅ Clean, maintainable code
|
||||
✅ Working test examples
|
||||
|
||||
The API is now production-ready and fully generic!
|
||||
764
app.py
764
app.py
@ -1,15 +1,17 @@
|
||||
#!/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
|
||||
Generic multi-server status API for GitOps deployments
|
||||
Supports multiple server types: rsyslog, Splunk, IBM ITNM, nginx, etc.
|
||||
Listens on port 5000 and handles status updates from pipelines and cron jobs
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from flask import Flask, request, jsonify
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
from flasgger import Swagger
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
app = Flask(__name__)
|
||||
swagger = Swagger(app)
|
||||
@ -27,15 +29,539 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_status():
|
||||
"""Load the current status from file"""
|
||||
def load_data() -> Dict:
|
||||
"""Load the current data structure from file"""
|
||||
try:
|
||||
if os.path.exists(STATUS_FILE):
|
||||
with open(STATUS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
|
||||
# Handle legacy format (v1.x) - migrate to v2 format
|
||||
if 'version' not in data and 'repo' in data:
|
||||
logger.info("Migrating from legacy v1 format to v2 format")
|
||||
legacy_data = data.copy()
|
||||
server_key = f"{legacy_data.get('server', 'unknown')}/{legacy_data.get('server', 'unknown')}"
|
||||
|
||||
# Convert old format to new format
|
||||
migrated_server = {
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": legacy_data.get('repo', 'unknown'),
|
||||
"server_group": legacy_data.get('server', 'unknown'),
|
||||
"server_type": legacy_data.get('repo', 'unknown'),
|
||||
"environment": "unknown",
|
||||
"host": legacy_data.get('server', 'unknown'),
|
||||
"status": legacy_data.get('sync_status', 'unknown').lower(),
|
||||
"changed_files": [f.get('name', '') for f in legacy_data.get('drifted_files', [])] if isinstance(legacy_data.get('drifted_files'), list) else [],
|
||||
"validation_status": "unknown",
|
||||
"validation_message": "",
|
||||
"last_pipeline_run": legacy_data.get('last_check', ''),
|
||||
"last_cron_check": legacy_data.get('last_check', ''),
|
||||
"commit_sha": "",
|
||||
"branch": "",
|
||||
"updated_by": "legacy_migration",
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
}
|
||||
|
||||
data = {
|
||||
"version": "2.0",
|
||||
"servers": {
|
||||
server_key: migrated_server
|
||||
}
|
||||
}
|
||||
save_data(data)
|
||||
logger.info(f"Migration complete: {server_key}")
|
||||
|
||||
return data
|
||||
else:
|
||||
logger.warning(f"Status file not found: {STATUS_FILE}")
|
||||
logger.warning(f"Status file not found: {STATUS_FILE}, creating new v2 structure")
|
||||
return {
|
||||
"version": "2.0",
|
||||
"servers": {}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading status: {e}")
|
||||
return {
|
||||
"version": "2.0",
|
||||
"servers": {}
|
||||
}
|
||||
|
||||
|
||||
def save_data(data: Dict) -> bool:
|
||||
"""Save the data structure 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(data, f, indent=2)
|
||||
|
||||
logger.info(f"Data saved successfully ({len(data.get('servers', {}))} servers)")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving data: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_server_key(server_group: str, host: str) -> str:
|
||||
"""Generate consistent key for server storage"""
|
||||
return f"{server_group}/{host}"
|
||||
|
||||
|
||||
def validate_required_fields(payload: Dict, required_fields: List[str]) -> Optional[str]:
|
||||
"""Validate that required fields are present in payload"""
|
||||
missing = [field for field in required_fields if field not in payload or payload[field] == ""]
|
||||
if missing:
|
||||
return f"Missing required fields: {', '.join(missing)}"
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/status/pipeline', methods=['POST', 'OPTIONS'])
|
||||
def status_pipeline():
|
||||
"""
|
||||
Pipeline status update endpoint
|
||||
Updates full deployment status from CI/CD pipeline
|
||||
---
|
||||
post:
|
||||
summary: Update status from pipeline deployment
|
||||
description: Full status update including deployment details, validation, commit info
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- server_group
|
||||
- host
|
||||
- status
|
||||
properties:
|
||||
project_name:
|
||||
type: string
|
||||
example: "gitops-for-servers"
|
||||
repo_name:
|
||||
type: string
|
||||
example: "rsyslog"
|
||||
server_group:
|
||||
type: string
|
||||
example: "rsyslog-prod"
|
||||
server_type:
|
||||
type: string
|
||||
example: "rsyslog"
|
||||
environment:
|
||||
type: string
|
||||
example: "prod"
|
||||
host:
|
||||
type: string
|
||||
example: "rsyslog-01"
|
||||
status:
|
||||
type: string
|
||||
enum: ["synced", "out_of_sync", "failed", "unknown"]
|
||||
example: "synced"
|
||||
changed_files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: []
|
||||
validation_status:
|
||||
type: string
|
||||
example: "passed"
|
||||
validation_message:
|
||||
type: string
|
||||
example: "rsyslog syntax validation passed"
|
||||
commit_sha:
|
||||
type: string
|
||||
example: "abc1234"
|
||||
branch:
|
||||
type: string
|
||||
example: "master"
|
||||
updated_by:
|
||||
type: string
|
||||
example: "woodpecker"
|
||||
last_pipeline_run:
|
||||
type: string
|
||||
example: "2026-04-26T00:00:00Z"
|
||||
responses:
|
||||
200:
|
||||
description: Status updated successfully
|
||||
400:
|
||||
description: Validation error
|
||||
500:
|
||||
description: Failed to save status
|
||||
"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
try:
|
||||
payload = request.get_json()
|
||||
if not payload:
|
||||
return jsonify({"error": "No JSON data provided"}), 400
|
||||
|
||||
# Validate required fields
|
||||
error = validate_required_fields(payload, ['server_group', 'host', 'status'])
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
|
||||
# Load current data
|
||||
data = load_data()
|
||||
|
||||
# Generate server key
|
||||
server_key = get_server_key(payload['server_group'], payload['host'])
|
||||
|
||||
# Get existing server data if present, or create new entry
|
||||
server_data = data['servers'].get(server_key, {})
|
||||
|
||||
# Update all fields from pipeline (full update)
|
||||
server_data.update({
|
||||
"project_name": payload.get('project_name', server_data.get('project_name', '')),
|
||||
"repo_name": payload.get('repo_name', server_data.get('repo_name', '')),
|
||||
"server_group": payload['server_group'],
|
||||
"server_type": payload.get('server_type', server_data.get('server_type', '')),
|
||||
"environment": payload.get('environment', server_data.get('environment', '')),
|
||||
"host": payload['host'],
|
||||
"status": payload['status'],
|
||||
"changed_files": payload.get('changed_files', []),
|
||||
"validation_status": payload.get('validation_status', ''),
|
||||
"validation_message": payload.get('validation_message', ''),
|
||||
"commit_sha": payload.get('commit_sha', ''),
|
||||
"branch": payload.get('branch', ''),
|
||||
"updated_by": payload.get('updated_by', 'pipeline'),
|
||||
"last_pipeline_run": payload.get('last_pipeline_run', datetime.now(UTC).isoformat()),
|
||||
"last_cron_check": server_data.get('last_cron_check', ''),
|
||||
"timestamp": datetime.now(UTC).isoformat()
|
||||
})
|
||||
|
||||
# Save to data structure
|
||||
data['servers'][server_key] = server_data
|
||||
|
||||
if save_data(data):
|
||||
logger.info(f"Pipeline update: {server_key} -> {payload['status']}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Pipeline status updated successfully",
|
||||
"server": server_data
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({"error": "Failed to save status"}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in POST /status/pipeline: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/status/cron', methods=['POST', 'OPTIONS'])
|
||||
def status_cron():
|
||||
"""
|
||||
Cron status update endpoint
|
||||
Updates only drift detection / health check data
|
||||
Does NOT overwrite pipeline deployment data
|
||||
---
|
||||
post:
|
||||
summary: Update status from cron drift check
|
||||
description: Partial update for drift detection - does not overwrite commit_sha, changed_files, or validation data
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- server_group
|
||||
- host
|
||||
properties:
|
||||
server_group:
|
||||
type: string
|
||||
example: "rsyslog-prod"
|
||||
host:
|
||||
type: string
|
||||
example: "rsyslog-01"
|
||||
status:
|
||||
type: string
|
||||
enum: ["synced", "out_of_sync", "failed", "unknown"]
|
||||
example: "synced"
|
||||
changed_files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: []
|
||||
last_cron_check:
|
||||
type: string
|
||||
example: "2026-04-26T01:00:00Z"
|
||||
responses:
|
||||
200:
|
||||
description: Cron status updated successfully
|
||||
400:
|
||||
description: Validation error
|
||||
404:
|
||||
description: Server not found
|
||||
500:
|
||||
description: Failed to save status
|
||||
"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
try:
|
||||
payload = request.get_json()
|
||||
if not payload:
|
||||
return jsonify({"error": "No JSON data provided"}), 400
|
||||
|
||||
# Validate required fields
|
||||
error = validate_required_fields(payload, ['server_group', 'host'])
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
|
||||
# Load current data
|
||||
data = load_data()
|
||||
|
||||
# Generate server key
|
||||
server_key = get_server_key(payload['server_group'], payload['host'])
|
||||
|
||||
# Check if server exists
|
||||
if server_key not in data['servers']:
|
||||
logger.warning(f"Cron update for unknown server: {server_key}")
|
||||
return jsonify({"error": f"Server {server_key} not found. Use /status/pipeline first."}), 404
|
||||
|
||||
server_data = data['servers'][server_key]
|
||||
|
||||
# Cron updates only specific fields, preserves pipeline data
|
||||
if 'status' in payload:
|
||||
server_data['status'] = payload['status']
|
||||
|
||||
if 'changed_files' in payload:
|
||||
server_data['changed_files'] = payload['changed_files']
|
||||
|
||||
server_data['last_cron_check'] = payload.get('last_cron_check', datetime.now(UTC).isoformat())
|
||||
server_data['timestamp'] = datetime.now(UTC).isoformat()
|
||||
|
||||
# Save to data structure
|
||||
data['servers'][server_key] = server_data
|
||||
|
||||
if save_data(data):
|
||||
logger.info(f"Cron update: {server_key} -> {server_data.get('status', 'unknown')}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Cron status updated successfully",
|
||||
"server": server_data
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({"error": "Failed to save status"}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in POST /status/cron: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/status', methods=['GET'])
|
||||
def get_all_status():
|
||||
"""
|
||||
Get all server statuses (Grafana Infinity friendly)
|
||||
---
|
||||
get:
|
||||
summary: Retrieve all server statuses
|
||||
description: Returns flat array of all servers, optimized for Grafana Infinity datasource
|
||||
responses:
|
||||
200:
|
||||
description: List of all server statuses
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
server_group:
|
||||
type: string
|
||||
server_type:
|
||||
type: string
|
||||
host:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
changed_files_count:
|
||||
type: integer
|
||||
changed_files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
validation_status:
|
||||
type: string
|
||||
last_pipeline_run:
|
||||
type: string
|
||||
last_cron_check:
|
||||
type: string
|
||||
commit_sha:
|
||||
type: string
|
||||
"""
|
||||
try:
|
||||
data = load_data()
|
||||
|
||||
# Convert to flat array for Grafana
|
||||
result = []
|
||||
for server_key, server_data in data.get('servers', {}).items():
|
||||
result.append({
|
||||
"project_name": server_data.get('project_name', ''),
|
||||
"repo_name": server_data.get('repo_name', ''),
|
||||
"server_group": server_data.get('server_group', ''),
|
||||
"server_type": server_data.get('server_type', ''),
|
||||
"environment": server_data.get('environment', ''),
|
||||
"host": server_data.get('host', ''),
|
||||
"status": server_data.get('status', 'unknown'),
|
||||
"changed_files_count": len(server_data.get('changed_files', [])),
|
||||
"changed_files": server_data.get('changed_files', []),
|
||||
"validation_status": server_data.get('validation_status', ''),
|
||||
"validation_message": server_data.get('validation_message', ''),
|
||||
"last_pipeline_run": server_data.get('last_pipeline_run', ''),
|
||||
"last_cron_check": server_data.get('last_cron_check', ''),
|
||||
"commit_sha": server_data.get('commit_sha', ''),
|
||||
"branch": server_data.get('branch', ''),
|
||||
"updated_by": server_data.get('updated_by', ''),
|
||||
"timestamp": server_data.get('timestamp', '')
|
||||
})
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in GET /status: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/status/<server_group>', methods=['GET'])
|
||||
def get_server_group_status(server_group: str):
|
||||
"""
|
||||
Get status for all servers in a specific server group
|
||||
---
|
||||
get:
|
||||
summary: Retrieve status for a server group
|
||||
parameters:
|
||||
- in: path
|
||||
name: server_group
|
||||
type: string
|
||||
required: true
|
||||
description: The server group name
|
||||
responses:
|
||||
200:
|
||||
description: List of servers in the group
|
||||
404:
|
||||
description: Server group not found
|
||||
"""
|
||||
try:
|
||||
data = load_data()
|
||||
|
||||
# Filter by server_group
|
||||
result = []
|
||||
for server_key, server_data in data.get('servers', {}).items():
|
||||
if server_data.get('server_group') == server_group:
|
||||
result.append({
|
||||
"project_name": server_data.get('project_name', ''),
|
||||
"repo_name": server_data.get('repo_name', ''),
|
||||
"server_group": server_data.get('server_group', ''),
|
||||
"server_type": server_data.get('server_type', ''),
|
||||
"environment": server_data.get('environment', ''),
|
||||
"host": server_data.get('host', ''),
|
||||
"status": server_data.get('status', 'unknown'),
|
||||
"changed_files_count": len(server_data.get('changed_files', [])),
|
||||
"changed_files": server_data.get('changed_files', []),
|
||||
"validation_status": server_data.get('validation_status', ''),
|
||||
"validation_message": server_data.get('validation_message', ''),
|
||||
"last_pipeline_run": server_data.get('last_pipeline_run', ''),
|
||||
"last_cron_check": server_data.get('last_cron_check', ''),
|
||||
"commit_sha": server_data.get('commit_sha', ''),
|
||||
"branch": server_data.get('branch', ''),
|
||||
"updated_by": server_data.get('updated_by', ''),
|
||||
"timestamp": server_data.get('timestamp', '')
|
||||
})
|
||||
|
||||
if not result:
|
||||
return jsonify({"error": f"No servers found in group: {server_group}"}), 404
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in GET /status/{server_group}: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/status/<server_group>/<host>', methods=['GET'])
|
||||
def get_host_status(server_group: str, host: str):
|
||||
"""
|
||||
Get status for a specific server
|
||||
---
|
||||
get:
|
||||
summary: Retrieve status for a specific server
|
||||
parameters:
|
||||
- in: path
|
||||
name: server_group
|
||||
type: string
|
||||
required: true
|
||||
description: The server group name
|
||||
- in: path
|
||||
name: host
|
||||
type: string
|
||||
required: true
|
||||
description: The hostname
|
||||
responses:
|
||||
200:
|
||||
description: Server status
|
||||
404:
|
||||
description: Server not found
|
||||
"""
|
||||
try:
|
||||
data = load_data()
|
||||
server_key = get_server_key(server_group, host)
|
||||
|
||||
if server_key not in data.get('servers', {}):
|
||||
return jsonify({"error": f"Server not found: {server_key}"}), 404
|
||||
|
||||
server_data = data['servers'][server_key]
|
||||
|
||||
return jsonify({
|
||||
"project_name": server_data.get('project_name', ''),
|
||||
"repo_name": server_data.get('repo_name', ''),
|
||||
"server_group": server_data.get('server_group', ''),
|
||||
"server_type": server_data.get('server_type', ''),
|
||||
"environment": server_data.get('environment', ''),
|
||||
"host": server_data.get('host', ''),
|
||||
"status": server_data.get('status', 'unknown'),
|
||||
"changed_files_count": len(server_data.get('changed_files', [])),
|
||||
"changed_files": server_data.get('changed_files', []),
|
||||
"validation_status": server_data.get('validation_status', ''),
|
||||
"validation_message": server_data.get('validation_message', ''),
|
||||
"last_pipeline_run": server_data.get('last_pipeline_run', ''),
|
||||
"last_cron_check": server_data.get('last_cron_check', ''),
|
||||
"commit_sha": server_data.get('commit_sha', ''),
|
||||
"branch": server_data.get('branch', ''),
|
||||
"updated_by": server_data.get('updated_by', ''),
|
||||
"timestamp": server_data.get('timestamp', '')
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in GET /status/{server_group}/{host}: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/status', methods=['GET', 'POST', 'OPTIONS'])
|
||||
def api_status_legacy():
|
||||
"""
|
||||
Legacy API endpoint for backward compatibility
|
||||
---
|
||||
get:
|
||||
summary: (Legacy) Retrieve current status
|
||||
deprecated: true
|
||||
description: Use GET /status instead
|
||||
post:
|
||||
summary: (Legacy) Update status with old format
|
||||
deprecated: true
|
||||
description: Use POST /status/pipeline instead
|
||||
"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
if request.method == 'GET':
|
||||
# Return first server in old format, or empty old format
|
||||
data = load_data()
|
||||
|
||||
if not data.get('servers'):
|
||||
return jsonify({
|
||||
"repo": "unknown",
|
||||
"server": "unknown",
|
||||
"sync_status": "UNKNOWN",
|
||||
@ -43,157 +569,77 @@ def load_status():
|
||||
"deployed_files": [],
|
||||
"drifted_files": [],
|
||||
"last_check": ""
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading status: {e}")
|
||||
return {}
|
||||
}), 200
|
||||
|
||||
# Get first server and convert to old format
|
||||
first_key = list(data['servers'].keys())[0]
|
||||
server = data['servers'][first_key]
|
||||
|
||||
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('/api/status', methods=['GET', 'POST', 'OPTIONS'])
|
||||
def api_status():
|
||||
"""
|
||||
GitOps Status API endpoint
|
||||
---
|
||||
get:
|
||||
summary: Retrieve current status
|
||||
responses:
|
||||
200:
|
||||
description: Current GitOps status
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
repo:
|
||||
type: string
|
||||
server:
|
||||
type: string
|
||||
sync_status:
|
||||
type: string
|
||||
drift_count:
|
||||
type: integer
|
||||
deployed_files:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
drifted_files:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
last_check:
|
||||
type: string
|
||||
post:
|
||||
summary: Update status with new data
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
repo:
|
||||
type: string
|
||||
example: "rsyslog"
|
||||
server:
|
||||
type: string
|
||||
example: "rsyslog-lab"
|
||||
sync_status:
|
||||
type: string
|
||||
enum: ["SYNCED", "OUT_OF_SYNC", "UNKNOWN", "PROGRESSING"]
|
||||
drift_count:
|
||||
type: integer
|
||||
deployed_files:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: [{"name": "rsyslog.conf"}]
|
||||
drifted_files:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: []
|
||||
last_check:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Status updated successfully
|
||||
400:
|
||||
description: No JSON data provided
|
||||
500:
|
||||
description: Failed to save status
|
||||
"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
if request.method == 'GET':
|
||||
status = load_status()
|
||||
return jsonify(status), 200
|
||||
return jsonify({
|
||||
"repo": server.get('repo_name', 'unknown'),
|
||||
"server": server.get('server_group', 'unknown'),
|
||||
"sync_status": server.get('status', 'unknown').upper(),
|
||||
"drift_count": len(server.get('changed_files', [])),
|
||||
"deployed_files": [],
|
||||
"drifted_files": [{"name": f} for f in server.get('changed_files', [])],
|
||||
"last_check": server.get('last_pipeline_run', '')
|
||||
}), 200
|
||||
|
||||
if request.method == 'POST':
|
||||
# Accept old format and convert to new format
|
||||
try:
|
||||
incoming_data = request.get_json()
|
||||
if not incoming_data:
|
||||
return jsonify({"error": "No JSON data provided"}), 400
|
||||
|
||||
# Load current status
|
||||
status = load_status()
|
||||
# Convert old format to new format
|
||||
server_group = incoming_data.get('server', 'unknown')
|
||||
host = incoming_data.get('server', 'unknown')
|
||||
|
||||
# Update with incoming data (merge)
|
||||
status.update(incoming_data)
|
||||
new_payload = {
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": incoming_data.get('repo', 'unknown'),
|
||||
"server_group": server_group,
|
||||
"server_type": incoming_data.get('repo', 'unknown'),
|
||||
"environment": "unknown",
|
||||
"host": host,
|
||||
"status": incoming_data.get('sync_status', 'unknown').lower(),
|
||||
"changed_files": [f.get('name', '') for f in incoming_data.get('drifted_files', [])] if isinstance(incoming_data.get('drifted_files'), list) else [],
|
||||
"validation_status": "unknown",
|
||||
"validation_message": "",
|
||||
"commit_sha": "",
|
||||
"branch": "",
|
||||
"updated_by": "legacy_api",
|
||||
"last_pipeline_run": incoming_data.get('last_check', datetime.now(UTC).isoformat())
|
||||
}
|
||||
|
||||
# Add/update timestamp if not present
|
||||
if 'last_check' not in status or not status['last_check']:
|
||||
status['last_check'] = datetime.utcnow().isoformat() + 'Z'
|
||||
# Load current data
|
||||
data = load_data()
|
||||
server_key = get_server_key(server_group, host)
|
||||
server_data = data['servers'].get(server_key, {})
|
||||
|
||||
# Save updated status
|
||||
if save_status(status):
|
||||
# Merge with existing data
|
||||
server_data.update(new_payload)
|
||||
server_data['timestamp'] = datetime.now(UTC).isoformat()
|
||||
|
||||
data['servers'][server_key] = server_data
|
||||
|
||||
if save_data(data):
|
||||
logger.info(f"Legacy API update: {server_key}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Status updated successfully",
|
||||
"status": status
|
||||
"message": "Status updated successfully (legacy format)",
|
||||
"status": incoming_data
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
"error": "Failed to save status"
|
||||
}), 500
|
||||
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}")
|
||||
logger.error(f"Error in POST /api/status (legacy): {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""
|
||||
@ -224,25 +670,13 @@ def ready():
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
|
||||
# Try to read or create status file
|
||||
if not os.path.exists(STATUS_FILE):
|
||||
# File doesn't exist yet, try to create it
|
||||
default_status = {
|
||||
"repo": "unknown",
|
||||
"server": "unknown",
|
||||
"sync_status": "UNKNOWN",
|
||||
"drift_count": 0,
|
||||
"deployed_files": [],
|
||||
"drifted_files": [],
|
||||
"last_check": ""
|
||||
}
|
||||
save_status(default_status)
|
||||
data = load_data()
|
||||
|
||||
# Verify we can read it
|
||||
status = load_status()
|
||||
if isinstance(status, dict):
|
||||
return jsonify({"status": "ready"}), 200
|
||||
if isinstance(data, dict) and 'version' in data:
|
||||
return jsonify({"status": "ready", "version": data['version']}), 200
|
||||
|
||||
return jsonify({"status": "not_ready", "reason": "invalid status data"}), 503
|
||||
return jsonify({"status": "not_ready", "reason": "invalid data structure"}), 503
|
||||
except Exception as e:
|
||||
logger.error(f"Readiness check failed: {e}")
|
||||
return jsonify({"status": "not_ready", "error": str(e)}), 503
|
||||
@ -259,19 +693,31 @@ def root():
|
||||
"""
|
||||
return jsonify({
|
||||
"name": "GitOps Status API",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Generic multi-server status API for GitOps deployments",
|
||||
"endpoints": {
|
||||
"GET /status.json": "Retrieve current status",
|
||||
"GET /api/status": "Retrieve current status (JSON API)",
|
||||
"POST /api/status": "Update status with new data",
|
||||
"POST /status/pipeline": "Update full deployment status from pipeline",
|
||||
"POST /status/cron": "Update drift check status from cron",
|
||||
"GET /status": "Retrieve all server statuses (Grafana friendly)",
|
||||
"GET /status/{server_group}": "Retrieve status for server group",
|
||||
"GET /status/{server_group}/{host}": "Retrieve status for specific server",
|
||||
"GET /api/status": "(Legacy) Retrieve status in old format",
|
||||
"POST /api/status": "(Legacy) Update status with old format",
|
||||
"GET /health": "Liveness probe",
|
||||
"GET /ready": "Readiness probe"
|
||||
}
|
||||
},
|
||||
"supported_server_types": [
|
||||
"rsyslog",
|
||||
"splunk",
|
||||
"ibm-itnm",
|
||||
"nginx",
|
||||
"any-generic-server-type"
|
||||
]
|
||||
}), 200
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info(f"Starting GitOps Status API on {API_HOST}:{API_PORT}")
|
||||
logger.info(f"Starting GitOps Status API v2.0 on {API_HOST}:{API_PORT}")
|
||||
logger.info(f"Status file location: {STATUS_FILE}")
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
Flask==2.3.2
|
||||
Werkzeug==2.3.6
|
||||
Flask>=3.0.0
|
||||
Werkzeug>=3.0.0
|
||||
flasgger
|
||||
|
||||
339
test_api.sh
Normal file
339
test_api.sh
Normal file
@ -0,0 +1,339 @@
|
||||
#!/bin/bash
|
||||
# GitOps Status API - Test Script
|
||||
# Tests all endpoints with realistic examples
|
||||
|
||||
set -e
|
||||
|
||||
API_URL="${API_URL:-http://localhost:5000}"
|
||||
|
||||
echo "=========================================="
|
||||
echo "GitOps Status API Test Script"
|
||||
echo "API URL: $API_URL"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Color codes
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
function print_test() {
|
||||
echo -e "${BLUE}[TEST]${NC} $1"
|
||||
}
|
||||
|
||||
function print_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
function print_section() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}========================================${NC}"
|
||||
echo -e "${YELLOW}$1${NC}"
|
||||
echo -e "${YELLOW}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Test 1: Health checks
|
||||
print_section "1. Health Checks"
|
||||
|
||||
print_test "GET /health"
|
||||
curl -s "$API_URL/health" | jq .
|
||||
print_success "Health check passed"
|
||||
echo ""
|
||||
|
||||
print_test "GET /ready"
|
||||
curl -s "$API_URL/ready" | jq .
|
||||
print_success "Readiness check passed"
|
||||
echo ""
|
||||
|
||||
print_test "GET / (API info)"
|
||||
curl -s "$API_URL/" | jq .
|
||||
print_success "API info retrieved"
|
||||
echo ""
|
||||
|
||||
# Test 2: Pipeline updates (rsyslog servers)
|
||||
print_section "2. Pipeline Updates - rsyslog Servers"
|
||||
|
||||
print_test "POST /status/pipeline - rsyslog-prod/rsyslog-01 (synced)"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "rsyslog syntax validation passed",
|
||||
"commit_sha": "abc1234567890",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker",
|
||||
"last_pipeline_run": "2026-04-26T10:30:00Z"
|
||||
}' | jq .
|
||||
print_success "rsyslog-01 status updated"
|
||||
echo ""
|
||||
|
||||
print_test "POST /status/pipeline - rsyslog-prod/rsyslog-02 (synced)"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-prod",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "prod",
|
||||
"host": "rsyslog-02",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "rsyslog syntax validation passed",
|
||||
"commit_sha": "abc1234567890",
|
||||
"branch": "master",
|
||||
"updated_by": "woodpecker",
|
||||
"last_pipeline_run": "2026-04-26T10:30:00Z"
|
||||
}' | jq .
|
||||
print_success "rsyslog-02 status updated"
|
||||
echo ""
|
||||
|
||||
# Test 3: Pipeline updates (other server types)
|
||||
print_section "3. Pipeline Updates - Other Server Types"
|
||||
|
||||
print_test "POST /status/pipeline - Splunk indexer"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "splunk",
|
||||
"server_group": "splunk-prod",
|
||||
"server_type": "splunk",
|
||||
"environment": "prod",
|
||||
"host": "splunk-indexer-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "Splunk configuration validated",
|
||||
"commit_sha": "def4567890123",
|
||||
"branch": "main",
|
||||
"updated_by": "jenkins",
|
||||
"last_pipeline_run": "2026-04-26T10:35:00Z"
|
||||
}' | jq .
|
||||
print_success "Splunk indexer status updated"
|
||||
echo ""
|
||||
|
||||
print_test "POST /status/pipeline - nginx web server"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "nginx",
|
||||
"server_group": "nginx-prod",
|
||||
"server_type": "nginx",
|
||||
"environment": "prod",
|
||||
"host": "nginx-web-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "nginx -t passed",
|
||||
"commit_sha": "ghi7890123456",
|
||||
"branch": "master",
|
||||
"updated_by": "gitlab-ci",
|
||||
"last_pipeline_run": "2026-04-26T10:40:00Z"
|
||||
}' | jq .
|
||||
print_success "nginx web server status updated"
|
||||
echo ""
|
||||
|
||||
print_test "POST /status/pipeline - IBM ITNM"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "ibm-itnm",
|
||||
"server_group": "itnm-prod",
|
||||
"server_type": "ibm-itnm",
|
||||
"environment": "prod",
|
||||
"host": "itnm-server-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"validation_status": "passed",
|
||||
"validation_message": "ITNM config validation successful",
|
||||
"commit_sha": "jkl0123456789",
|
||||
"branch": "production",
|
||||
"updated_by": "woodpecker",
|
||||
"last_pipeline_run": "2026-04-26T10:45:00Z"
|
||||
}' | jq .
|
||||
print_success "IBM ITNM status updated"
|
||||
echo ""
|
||||
|
||||
# Test 4: Pipeline update with failure
|
||||
print_section "4. Pipeline Update - Failed Deployment"
|
||||
|
||||
print_test "POST /status/pipeline - rsyslog-staging/rsyslog-test-01 (failed)"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_name": "gitops-for-servers",
|
||||
"repo_name": "rsyslog",
|
||||
"server_group": "rsyslog-staging",
|
||||
"server_type": "rsyslog",
|
||||
"environment": "staging",
|
||||
"host": "rsyslog-test-01",
|
||||
"status": "failed",
|
||||
"changed_files": ["/etc/rsyslog.conf"],
|
||||
"validation_status": "failed",
|
||||
"validation_message": "rsyslog: syntax error in /etc/rsyslog.conf line 42",
|
||||
"commit_sha": "bad1234567890",
|
||||
"branch": "feature/new-config",
|
||||
"updated_by": "developer",
|
||||
"last_pipeline_run": "2026-04-26T10:50:00Z"
|
||||
}' | jq .
|
||||
print_success "Failed deployment recorded"
|
||||
echo ""
|
||||
|
||||
# Test 5: Cron updates (drift detection)
|
||||
print_section "5. Cron Updates - Drift Detection"
|
||||
|
||||
print_test "POST /status/cron - rsyslog-01 (no drift)"
|
||||
curl -s -X POST "$API_URL/status/cron" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "rsyslog-prod",
|
||||
"host": "rsyslog-01",
|
||||
"status": "synced",
|
||||
"changed_files": [],
|
||||
"last_cron_check": "2026-04-26T11:00:00Z"
|
||||
}' | jq .
|
||||
print_success "Cron check - no drift"
|
||||
echo ""
|
||||
|
||||
print_test "POST /status/cron - rsyslog-02 (drift detected)"
|
||||
curl -s -X POST "$API_URL/status/cron" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "rsyslog-prod",
|
||||
"host": "rsyslog-02",
|
||||
"status": "out_of_sync",
|
||||
"changed_files": ["/etc/rsyslog.conf", "/etc/rsyslog.d/50-default.conf"],
|
||||
"last_cron_check": "2026-04-26T11:00:00Z"
|
||||
}' | jq .
|
||||
print_success "Cron check - drift detected"
|
||||
echo ""
|
||||
|
||||
# Test 6: Query endpoints
|
||||
print_section "6. Query Endpoints - GET Status"
|
||||
|
||||
print_test "GET /status (all servers)"
|
||||
curl -s "$API_URL/status" | jq .
|
||||
print_success "All servers retrieved"
|
||||
echo ""
|
||||
|
||||
print_test "GET /status/rsyslog-prod (server group)"
|
||||
curl -s "$API_URL/status/rsyslog-prod" | jq .
|
||||
print_success "rsyslog-prod group retrieved"
|
||||
echo ""
|
||||
|
||||
print_test "GET /status/rsyslog-prod/rsyslog-01 (specific server)"
|
||||
curl -s "$API_URL/status/rsyslog-prod/rsyslog-01" | jq .
|
||||
print_success "Specific server retrieved"
|
||||
echo ""
|
||||
|
||||
print_test "GET /status/splunk-prod (splunk group)"
|
||||
curl -s "$API_URL/status/splunk-prod" | jq .
|
||||
print_success "splunk-prod group retrieved"
|
||||
echo ""
|
||||
|
||||
# Test 7: Legacy API (backward compatibility)
|
||||
print_section "7. Legacy API - Backward Compatibility"
|
||||
|
||||
print_test "POST /api/status (old format)"
|
||||
curl -s -X POST "$API_URL/api/status" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"repo": "rsyslog",
|
||||
"server": "rsyslog-legacy",
|
||||
"sync_status": "SYNCED",
|
||||
"drift_count": 0,
|
||||
"deployed_files": [{"name": "rsyslog.conf"}],
|
||||
"drifted_files": [],
|
||||
"last_check": "2026-04-26T11:15:00Z"
|
||||
}' | jq .
|
||||
print_success "Legacy format accepted"
|
||||
echo ""
|
||||
|
||||
print_test "GET /api/status (old format response)"
|
||||
curl -s "$API_URL/api/status" | jq .
|
||||
print_success "Legacy format returned"
|
||||
echo ""
|
||||
|
||||
# Test 8: Error cases
|
||||
print_section "8. Error Handling"
|
||||
|
||||
print_test "POST /status/pipeline - missing required fields"
|
||||
curl -s -X POST "$API_URL/status/pipeline" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "test"
|
||||
}' | jq .
|
||||
print_success "Validation error handled"
|
||||
echo ""
|
||||
|
||||
print_test "POST /status/cron - server not found"
|
||||
curl -s -X POST "$API_URL/status/cron" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"server_group": "nonexistent",
|
||||
"host": "ghost-server"
|
||||
}' | jq .
|
||||
print_success "Server not found error handled"
|
||||
echo ""
|
||||
|
||||
print_test "GET /status/nonexistent-group"
|
||||
curl -s "$API_URL/status/nonexistent-group" | jq .
|
||||
print_success "Group not found error handled"
|
||||
echo ""
|
||||
|
||||
# Test 9: Grafana-friendly output check
|
||||
print_section "9. Grafana Infinity Output Validation"
|
||||
|
||||
print_test "GET /status - Check output is flat array"
|
||||
RESPONSE=$(curl -s "$API_URL/status")
|
||||
IS_ARRAY=$(echo "$RESPONSE" | jq -r 'if type == "array" then "true" else "false" end')
|
||||
|
||||
if [ "$IS_ARRAY" == "true" ]; then
|
||||
print_success "Output is a flat array (Grafana compatible)"
|
||||
|
||||
# Check if each element has required fields
|
||||
HAS_FIELDS=$(echo "$RESPONSE" | jq -r '.[0] | has("server_group") and has("host") and has("status")')
|
||||
|
||||
if [ "$HAS_FIELDS" == "true" ]; then
|
||||
print_success "Array elements have required fields"
|
||||
else
|
||||
echo "Warning: Array elements missing required fields"
|
||||
fi
|
||||
else
|
||||
echo "Error: Output is not an array"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "$RESPONSE" | jq '.'
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
print_section "Test Summary"
|
||||
|
||||
echo "All tests completed!"
|
||||
echo ""
|
||||
echo "Servers created:"
|
||||
curl -s "$API_URL/status" | jq -r '.[] | "\(.server_group)/\(.host) - \(.status)"'
|
||||
echo ""
|
||||
|
||||
print_success "API is working correctly with multi-server support"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Update your pipelines to use POST /status/pipeline"
|
||||
echo " 2. Configure cron jobs to use POST /status/cron"
|
||||
echo " 3. Set up Grafana Infinity datasource pointing to GET /status"
|
||||
echo ""
|
||||
Loading…
x
Reference in New Issue
Block a user