diff --git a/gitops-status-server/.helmignore b/gitops-status-server/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/gitops-status-server/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/gitops-status-server/Chart.yaml b/gitops-status-server/Chart.yaml new file mode 100644 index 0000000..43fc4d3 --- /dev/null +++ b/gitops-status-server/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: gitops-status-server +description: A minimal HTTP server that serves GitOps status information as JSON +type: application +version: 1.0.0 +appVersion: "1.25.5" +keywords: + - gitops + - status + - monitoring + - nginx +maintainers: + - name: DevOps Team +home: https://github.com/your-org/observability-stack diff --git a/gitops-status-server/README.md b/gitops-status-server/README.md new file mode 100644 index 0000000..785d491 --- /dev/null +++ b/gitops-status-server/README.md @@ -0,0 +1,478 @@ +# GitOps Status Server Helm Chart + +A dual-container HTTP server that receives GitOps status updates via POST API and serves status information as JSON for monitoring and observability purposes. + +## Overview + +This chart deploys a two-container pod: +1. **Nginx** - Serves `/status.json` endpoint for monitoring tools and handles API routing +2. **Flask API** - Processes POST requests to `/api/status` and updates the status JSON + +It's designed to be consumed by Grafana's Infinity datasource or other monitoring tools, and to receive updates from CI/CD pipelines like Woodpecker. + +## Architecture + +``` +CI/CD Pipeline (Woodpecker) + ↓ + POST /api/status + ↓ +Kubernetes Service (port 80) + ↓ +Nginx (port 8080) + ├─→ /api/status → Proxies to Flask (localhost:5000) + └─→ /status.json → Serves static file + ↓ +Shared Volume (emptyDir) + ├─→ status.json (updated by Flask API) + └─→ Read by Nginx + ↓ +Grafana Infinity Datasource + Reads /status.json +``` + +## Features + +- **API-driven updates**: POST endpoint for CI/CD pipelines to update status +- **Read-only serving**: Grafana-friendly JSON endpoint +- **Minimal footprint**: nginx-unprivileged + Python-Alpine with minimal resources +- **Secure by default**: Runs as non-root with restricted filesystems +- **Internal only**: ClusterIP service for cluster-internal access +- **ArgoCD compatible**: Init container auto-initializes status from ConfigMap +- **Production-ready**: Includes health checks, security contexts, and resource limits + +## Installation + +### Using Helm + +```bash +# Install with default values +helm install gitops-status ./gitops-status-server + +# Install with custom namespace +helm install gitops-status ./gitops-status-server -n observability-stack --create-namespace + +# Install with custom values +helm install gitops-status ./gitops-status-server -f custom-values.yaml +``` + +### Using ArgoCD + +Create an Application manifest: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitops-status-server + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/your-org/observability-stack + targetRevision: main + path: gitops-status-server + helm: + values: | + replicaCount: 1 + statusJson: + repo: "rsyslog" + server: "rsyslog-lab" + sync_status: "UNKNOWN" +``` + +## API Endpoints + +### GET /status.json +Returns the current status JSON + +```bash +curl http://gitops-status-server.observability-stack.svc.cluster.local:80/status.json +``` + +Response: +```json +{ + "repo": "rsyslog", + "server": "rsyslog-lab", + "sync_status": "SYNCED", + "drift_count": 0, + "files": [], + "last_check": "2026-04-21T10:30:00Z" +} +``` + +### POST /api/status +Updates the status with new data + +```bash +curl -X POST http://gitops-status-server.observability-stack.svc.cluster.local:80/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:30:00Z" + }' +``` + +Response (HTTP 200): +```json +{ + "success": true, + "message": "Status updated successfully", + "status": { ... } +} +``` + +### GET /health +Health check endpoint (returns HTTP 200) + +```bash +curl http://gitops-status-server.observability-stack.svc.cluster.local:80/health +``` + +### GET /ready +Readiness check (verifies status file is readable) + +```bash +curl http://gitops-status-server.observability-stack.svc.cluster.local:80/ready +``` + +## Integration with Woodpecker + +The rsyslog CI/CD pipeline can update status by POSTing to the `/api/status` endpoint: + +```bash +#!/bin/bash + +GITOPS_STATUS_SERVER_URL="http://gitops-status-server.observability-stack.svc.cluster.local:80" + +STATUS_JSON='{ + "repo": "rsyslog", + "server": "rsyslog-lab", + "sync_status": "SYNCED", + "drift_count": 0, + "files": [], + "last_check": "2026-04-21T10:30:00Z" +}' + +curl -X POST "$GITOPS_STATUS_SERVER_URL/api/status" \ + -H "Content-Type: application/json" \ + -d "$STATUS_JSON" +``` + +## Service Discovery + +### Internal Kubernetes URL +``` +http://gitops-status-server.observability-stack.svc.cluster.local:80/status.json +``` + +### Port Forwarding (for local testing) +```bash +kubectl port-forward -n observability-stack svc/gitops-status-server 8080:80 +# Then access at http://localhost:8080/status.json +``` + +### NodePort (if service type is changed) +```bash +kubectl patch service -n observability-stack gitops-status-server -p '{"spec":{"type":"NodePort"}}' +# Then access at http://:/status.json +``` + +## Configuration + +See `values.yaml` for all configuration options: + +- `replicaCount`: Number of replicas +- `image.repository`: Container image +- `image.tag`: Image tag +- `service.type`: Service type (ClusterIP, NodePort, LoadBalancer) +- `service.port`: Service port (default 80) +- `service.targetPort`: Container port (default 8080) +- `resources`: CPU/memory limits and requests +- `statusJson`: Default status JSON values +- `api.image.*`: Python/Flask image configuration + +## Grafana Integration + +### Infinity Datasource Configuration + +1. Install Infinity datasource plugin: + ```bash + grafana-cli plugins install yesoreyeram-infinity-datasource + ``` + +2. Add datasource with URL: + ``` + http://gitops-status-server.observability-stack.svc.cluster.local:80/status.json + ``` + +3. Create panels to visualize: + - `sync_status`: Current synchronization state + - `drift_count`: Number of drifted files + - `files[]`: List of changed files + - `last_check`: Timestamp of last check + +### Example Query + +```json +{ + "url": "http://gitops-status-server.observability-stack.svc.cluster.local:80/status.json", + "format": "json" +} +``` + +## Security + +- Runs as non-root user (UID 101) +- Read-only root filesystem (except for /tmp, /var/cache/nginx, /var/run) +- No privileged capabilities +- Network policies recommended for production +- Service Account with minimal RBAC + +## Troubleshooting + +### POST Request Returns 400 Error + +**Issue**: "Invalid JSON" error + +**Solution**: Verify JSON formatting with: +```bash +echo '{...}' | jq '.' +``` + +### POST Updates Not Appearing in GET Response + +**Issue**: Update endpoint returns 200 but status.json isn't updated + +**Possible causes**: +- Shared volume permission issue +- API container crashed after POST +- Status file permissions + +**Debug**: +```bash +# Check logs +kubectl logs -f deployment/gitops-status-server -c api +kubectl logs -f deployment/gitops-status-server -c nginx + +# Check shared volume +kubectl exec deployment/gitops-status-server -c nginx -- ls -la /usr/share/nginx/html/ + +# Test API directly (port-forward to 5000 first) +kubectl port-forward deployment/gitops-status-server 5000:5000 +curl -X POST http://localhost:5000/api/status -H "Content-Type: application/json" -d '{...}' +``` + +### Connection Refused to gitops-status-server + +**Issue**: Woodpecker can't reach the service + +**Possible causes**: +- Service in different namespace +- Network policies blocking traffic +- Woodpecker outside cluster +- Service DNS name incorrect + +**Solutions**: +- Verify service exists: `kubectl get svc gitops-status-server -n observability-stack` +- Use NodePort for external access (update service type in values) +- Use port-forward as a temporary solution +- Verify network policies allow traffic + +## Performance + +- **CPU**: 150m limit (100m nginx + 100m API) +- **Memory**: 192Mi limit (64Mi nginx + 128Mi API) +- **Startup time**: ~5 seconds (Flask app install + startup) +- **Update latency**: <100ms (direct file write) +- **Read performance**: <10ms (static file serving) + +## License + +Same as observability-stack repository + statusJson: + repo: "my-repo" + server: "my-server" + sync_status: "SYNCED" + drift_count: 0 + files: [] + last_check: "2026-04-21T10:00:00Z" + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true +``` + +## Configuration + +### Key Values + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of replicas | `1` | +| `image.repository` | Container image repository | `nginxinc/nginx-unprivileged` | +| `image.tag` | Container image tag | `1.25-alpine` | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Service port | `80` | +| `service.targetPort` | Container target port | `8080` | +| `resources.limits.cpu` | CPU limit | `100m` | +| `resources.limits.memory` | Memory limit | `64Mi` | +| `statusJson` | JSON content to serve | See values.yaml | + +### Custom Status JSON + +Override the status JSON content in your values: + +```yaml +statusJson: + repo: "production-apps" + server: "prod-cluster-01" + sync_status: "SYNCED" + drift_count: 2 + files: + - "deployment.yaml" + - "service.yaml" + last_check: "2026-04-21T12:30:00Z" +``` + +## Usage + +### Access the Status Endpoint + +From inside the cluster: + +```bash +# Using the service DNS name +curl http://gitops-status-server/status.json + +# With namespace +curl http://gitops-status-server.monitoring.svc.cluster.local/status.json +``` + +### Grafana Infinity Datasource Configuration + +1. Add an Infinity datasource in Grafana +2. Configure URL: `http://gitops-status-server.monitoring.svc.cluster.local/status.json` +3. Parser: JSON +4. Use fields from the JSON response in your dashboard + +Example query fields: +- `sync_status` - Current sync status +- `drift_count` - Number of drifted resources +- `files` - List of changed files +- `last_check` - Timestamp of last check + +## Updating Status Data + +### Manual Update + +Edit the ConfigMap directly: + +```bash +kubectl edit configmap gitops-status-server -n monitoring +``` + +The deployment will automatically roll out with the new content due to the ConfigMap checksum annotation. + +### Automated Update via Pipeline + +Use `kubectl` in your CI/CD pipeline: + +```bash +kubectl create configmap gitops-status-server \ + --from-file=status.json=./status.json \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +### ArgoCD Hook (Advanced) + +Create a PostSync hook that updates the ConfigMap with current sync status: + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: update-status + annotations: + argocd.argoproj.io/hook: PostSync +spec: + template: + spec: + containers: + - name: update + image: bitnami/kubectl + command: + - /bin/sh + - -c + - | + # Update status.json with current sync status + kubectl patch configmap gitops-status-server \ + --patch '{"data":{"status.json":"..."}}' + restartPolicy: Never +``` + +## Security Considerations + +- Runs as non-root user (UID 101) +- Read-only root filesystem +- No privilege escalation +- Minimal capabilities (all dropped) +- No external network access required +- ClusterIP only (no external exposure) + +## Resource Requirements + +Minimal resource footprint suitable for small clusters: +- CPU: 50m request / 100m limit +- Memory: 32Mi request / 64Mi limit + +## Troubleshooting + +### Check pod status + +```bash +kubectl get pods -l app.kubernetes.io/name=gitops-status-server +``` + +### View logs + +```bash +kubectl logs -l app.kubernetes.io/name=gitops-status-server +``` + +### Test endpoint + +```bash +kubectl run -it --rm curl --image=curlimages/curl --restart=Never -- \ + curl http://gitops-status-server/status.json +``` + +### Common Issues + +**Pod not starting**: Check security context compatibility with your cluster's PSP/PSA policies. + +**Empty response**: Verify the ConfigMap is mounted correctly: +```bash +kubectl describe pod -l app.kubernetes.io/name=gitops-status-server +``` + +**Service not accessible**: Ensure you're accessing from within the cluster and using the correct namespace. + +## License + +This chart is part of the observability-stack project. + +## Maintainers + +- DevOps Team diff --git a/gitops-status-server/templates/_helpers.tpl b/gitops-status-server/templates/_helpers.tpl new file mode 100644 index 0000000..7585170 --- /dev/null +++ b/gitops-status-server/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "gitops-status-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "gitops-status-server.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "gitops-status-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "gitops-status-server.labels" -}} +helm.sh/chart: {{ include "gitops-status-server.chart" . }} +{{ include "gitops-status-server.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.labels }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "gitops-status-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gitops-status-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "gitops-status-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "gitops-status-server.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/gitops-status-server/templates/api-app.yaml b/gitops-status-server/templates/api-app.yaml new file mode 100644 index 0000000..25a47fd --- /dev/null +++ b/gitops-status-server/templates/api-app.yaml @@ -0,0 +1,142 @@ +{{/* +ConfigMap containing the API backend Python script +Handles POST requests to /api/status and updates the status.json file +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitops-status-server.fullname" . }}-api + labels: + {{- include "gitops-status-server.labels" . | nindent 4 }} +data: + app.py: | + #!/usr/bin/env python3 + """ + Simple Flask API for updating status.json + Listens on port 5000 and handles POST requests to /api/status + """ + import os + import json + import logging + from flask import Flask, request, jsonify + from datetime import datetime + + app = Flask(__name__) + + # Configuration + STATUS_FILE = '/usr/share/nginx/html/status.json' + API_PORT = int(os.environ.get('API_PORT', 5000)) + API_HOST = os.environ.get('API_HOST', '127.0.0.1') + + # Setup logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + logger = logging.getLogger(__name__) + + def load_status(): + """Load the current status from file""" + try: + if os.path.exists(STATUS_FILE): + with open(STATUS_FILE, 'r') as f: + return json.load(f) + else: + # Default status if file doesn't exist + return { + "repo": "unknown", + "server": "unknown", + "sync_status": "UNKNOWN", + "drift_count": 0, + "files": [], + "last_check": "" + } + except Exception as e: + logger.error(f"Error loading status: {e}") + return {} + + def save_status(status): + """Save the status to file""" + try: + # Ensure directory exists (should already exist from mount) + 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 saved successfully: {status['repo']}/{status['server']} -> {status['sync_status']}") + 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(): + """ + GET: Retrieve current status + POST: Update status with new data + """ + if request.method == 'OPTIONS': + return '', 204 + + if request.method == 'GET': + status = load_status() + return jsonify(status), 200 + + if request.method == 'POST': + try: + # Parse incoming JSON + incoming_data = request.get_json() + if not incoming_data: + return jsonify({"error": "No JSON data provided"}), 400 + + # Load current status + status = load_status() + + # Update with incoming data (merge) + status.update(incoming_data) + + # Ensure required fields exist + if 'last_check' not in status or not status['last_check']: + status['last_check'] = datetime.utcnow().isoformat() + 'Z' + + # Save updated status + if save_status(status): + return jsonify({ + "success": True, + "message": "Status updated successfully", + "status": status + }), 200 + else: + 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 processing POST request: {e}") + return jsonify({"error": str(e)}), 500 + + @app.route('/health', methods=['GET']) + def health(): + """Health check endpoint""" + return jsonify({"status": "healthy"}), 200 + + @app.route('/ready', methods=['GET']) + def ready(): + """Readiness check - verify status file is accessible""" + try: + status = load_status() + if status: + return jsonify({"status": "ready"}), 200 + else: + return jsonify({"status": "not_ready", "reason": "status file empty"}), 503 + except Exception as e: + return jsonify({"status": "not_ready", "error": str(e)}), 503 + + if __name__ == '__main__': + logger.info(f"Starting gitops-status-server API on {API_HOST}:{API_PORT}") + logger.info(f"Status file: {STATUS_FILE}") + app.run(host=API_HOST, port=API_PORT, debug=False) diff --git a/gitops-status-server/templates/configmap.yaml b/gitops-status-server/templates/configmap.yaml new file mode 100644 index 0000000..7b4cc5c --- /dev/null +++ b/gitops-status-server/templates/configmap.yaml @@ -0,0 +1,22 @@ +{{/* +ConfigMap for default status.json values +Used by init container to set up initial status if file doesn't exist +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitops-status-server.fullname" . }} + labels: + {{- include "gitops-status-server.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +data: + # Default status.json values (used for initialization) + # This is not mounted directly; instead it's used by the init container + # to set up the initial status.json in the shared emptyDir volume. + # The actual status.json is stored on the emptyDir and updated via the API. + status.json: | + {{- .Values.statusJson | toJson | nindent 4 }} + diff --git a/gitops-status-server/templates/deployment.yaml b/gitops-status-server/templates/deployment.yaml new file mode 100644 index 0000000..c96ad1e --- /dev/null +++ b/gitops-status-server/templates/deployment.yaml @@ -0,0 +1,190 @@ +{{/* +Deployment for the gitops-status-server +Runs nginx-unprivileged to serve the status.json file +*/}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gitops-status-server.fullname" . }} + labels: + {{- include "gitops-status-server.labels" . | nindent 4 }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "gitops-status-server.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + # Automatically roll deployment when ConfigMap changes + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gitops-status-server.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "gitops-status-server.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + + # Init container to set up initial status.json from ConfigMap + initContainers: + - name: init-status + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + command: + - sh + - -c + - | + if [ ! -f /usr/share/nginx/html/status.json ]; then + cat > /usr/share/nginx/html/status.json <<'EOF' + {{- .Values.statusJson | toJson | nindent 10 }} + EOF + fi + volumeMounts: + - name: shared-data + mountPath: /usr/share/nginx/html + + containers: + - name: api + image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}" + imagePullPolicy: {{ .Values.api.image.pullPolicy }} + command: + - sh + - -c + - | + pip install --no-cache-dir Flask==2.3.2 >/dev/null 2>&1 + exec python3 /app/app.py + ports: + - name: api + containerPort: 5000 + protocol: TCP + env: + - name: API_HOST + value: "127.0.0.1" + - name: API_PORT + value: "5000" + - name: FLASK_ENV + value: "production" + livenessProbe: + httpGet: + path: /health + port: api + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /ready + port: api + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 2 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + volumeMounts: + - name: shared-data + mountPath: /usr/share/nginx/html + - name: api-code + mountPath: /app + readOnly: true + + - name: nginx + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + # Health checks + livenessProbe: + httpGet: + path: /status.json + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /status.json + port: http + initialDelaySeconds: 2 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 2 + resources: + {{- toYaml .Values.resources | nindent 10 }} + volumeMounts: + # Mount the nginx config + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + # Mount the shared data directory (status.json is writable here) + - name: shared-data + mountPath: /usr/share/nginx/html + # nginx-unprivileged needs writable directories for cache and run + - name: cache + mountPath: /var/cache/nginx + - name: run + mountPath: /var/run + # nginx needs writable /tmp for proxy buffers + - name: tmp + mountPath: /tmp + volumes: + # ConfigMap volume containing the nginx configuration + - name: nginx-config + configMap: + name: {{ include "gitops-status-server.fullname" . }}-nginx-config + items: + - key: nginx.conf + path: nginx.conf + # ConfigMap volume containing the API application code + - name: api-code + configMap: + name: {{ include "gitops-status-server.fullname" . }}-api + defaultMode: 0755 + items: + - key: app.py + path: app.py + # Shared data volume for status.json (writable emptyDir) + - name: shared-data + emptyDir: + sizeLimit: 1Mi + # Empty directories for nginx runtime + - name: cache + emptyDir: {} + - name: run + emptyDir: {} + - name: tmp + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/gitops-status-server/templates/nginx-config.yaml b/gitops-status-server/templates/nginx-config.yaml new file mode 100644 index 0000000..0f6b2dc --- /dev/null +++ b/gitops-status-server/templates/nginx-config.yaml @@ -0,0 +1,96 @@ +{{/* +ConfigMap containing the nginx configuration +Enables serving status.json via GET and updating via POST requests +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitops-status-server.fullname" . }}-nginx-config + labels: + {{- include "gitops-status-server.labels" . | nindent 4 }} +data: + nginx.conf: | + # Minimal nginx config for serving and updating status.json + user nginx; + worker_processes auto; + error_log /var/log/nginx/error.log warn; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 1M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_types text/plain text/css text/xml text/javascript + application/x-javascript application/xml+rss + application/json; + + upstream api_backend { + server 127.0.0.1:5000; + keepalive 32; + } + + server { + listen 8080 default_server; + server_name _; + + # Serve status.json as read-only + location /status.json { + alias /usr/share/nginx/html/status.json; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Proxy POST requests to the API backend (Python Flask) + location /api/ { + proxy_pass http://api_backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Buffer settings for POST requests + proxy_request_buffering off; + proxy_buffering off; + + # Timeouts + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # Catch-all for root + location / { + return 301 /status.json; + } + } + } diff --git a/gitops-status-server/templates/service.yaml b/gitops-status-server/templates/service.yaml new file mode 100644 index 0000000..6de2897 --- /dev/null +++ b/gitops-status-server/templates/service.yaml @@ -0,0 +1,24 @@ +{{/* +Service for the gitops-status-server +Exposes the nginx server inside the cluster (ClusterIP) +This allows Grafana to query the status.json endpoint +*/}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitops-status-server.fullname" . }} + labels: + {{- include "gitops-status-server.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "gitops-status-server.selectorLabels" . | nindent 4 }} diff --git a/gitops-status-server/templates/serviceaccount.yaml b/gitops-status-server/templates/serviceaccount.yaml new file mode 100644 index 0000000..48517d8 --- /dev/null +++ b/gitops-status-server/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{/* +ServiceAccount for the gitops-status-server +*/}} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "gitops-status-server.serviceAccountName" . }} + labels: + {{- include "gitops-status-server.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/gitops-status-server/values.yaml b/gitops-status-server/values.yaml new file mode 100644 index 0000000..cadb501 --- /dev/null +++ b/gitops-status-server/values.yaml @@ -0,0 +1,103 @@ +# Default values for gitops-status-server +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Number of replicas for the deployment +replicaCount: 1 + +# Container image configuration +image: + # Use nginx-unprivileged for better security (runs as non-root) + repository: nginxinc/nginx-unprivileged + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion + tag: "1.25-alpine" + +# API backend container configuration (handles POST requests) +api: + image: + # Python Flask API for handling status updates + repository: python + pullPolicy: IfNotPresent + tag: "3.11-alpine" + # Pre-install Flask via pip before running the app + pip_packages: "Flask==2.3.2" + +# Image pull secrets for private registries +imagePullSecrets: [] + +# Override the name of the chart +nameOverride: "" +fullnameOverride: "" + +# Service configuration +service: + # Service type - ClusterIP for internal-only access + type: ClusterIP + # Port where the service will be exposed + port: 80 + # Target port on the container (nginx default) + targetPort: 8080 + # Annotations to add to the service + annotations: {} + +# Resource limits and requests +resources: + limits: + cpu: 100m + memory: 64Mi + requests: + cpu: 50m + memory: 32Mi + +# Node selector for pod assignment +nodeSelector: {} + +# Tolerations for pod assignment +tolerations: [] + +# Affinity rules for pod assignment +affinity: {} + +# Security context for the pod +podSecurityContext: + runAsNonRoot: true + runAsUser: 101 + fsGroup: 101 + +# Security context for the container +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + +# Status JSON content +# This can be overridden in your values to customize the status information +statusJson: + repo: "rsyslog" + server: "rsyslog-lab" + sync_status: "UNKNOWN" + drift_count: 0 + files: [] + last_check: "" + +# Labels to add to all resources +labels: {} + +# Annotations to add to all resources +annotations: {} + +# Pod annotations +podAnnotations: {} + +# Service account configuration +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: ""