diff --git a/argocd-apps/gitops-status-server.yaml b/argocd-apps/gitops-status-server.yaml new file mode 100644 index 0000000..986e830 --- /dev/null +++ b/argocd-apps/gitops-status-server.yaml @@ -0,0 +1,29 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitops-status-server + namespace: argocd +spec: + project: observability-stack + source: + repoURL: ssh://git@gitea-ssh.dev-tools.svc.cluster.local:2222/dvirlabs/observability-stack.git + targetRevision: HEAD + path: charts/gitops-status-server + helm: + valueFiles: + - ../../manifests/gitops-status-server/values.yaml + destination: + server: https://kubernetes.default.svc + namespace: observability-stack + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m \ No newline at end of file diff --git a/charts/gitops-status-server/.helmignore b/charts/gitops-status-server/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/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/charts/gitops-status-server/Chart.yaml b/charts/gitops-status-server/Chart.yaml new file mode 100644 index 0000000..43fc4d3 --- /dev/null +++ b/charts/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/charts/gitops-status-server/README.md b/charts/gitops-status-server/README.md new file mode 100644 index 0000000..8616465 --- /dev/null +++ b/charts/gitops-status-server/README.md @@ -0,0 +1,228 @@ +# GitOps Status Server Helm Chart + +A minimal HTTP server that serves GitOps status information as JSON for monitoring and observability purposes. + +## Overview + +This chart deploys a lightweight nginx-based server that exposes a single endpoint (`/status.json`) containing GitOps synchronization status, drift information, and changed files. It's designed to be consumed by Grafana's Infinity datasource or other monitoring tools. + +## Features + +- **Minimal footprint**: Uses nginx-unprivileged with minimal resource requirements +- **Secure by default**: Runs as non-root with read-only root filesystem +- **Internal only**: ClusterIP service for cluster-internal access +- **ConfigMap-based**: JSON content stored in ConfigMap for easy updates +- **ArgoCD compatible**: Automatically rolls deployment when ConfigMap changes +- **Production-ready**: Includes health checks, security contexts, and resource limits + +## Installation + +### Using Helm + +```bash +# Install with default values +helm install gitops-status ./charts/gitops-status-server + +# Install with custom namespace +helm install gitops-status ./charts/gitops-status-server -n monitoring --create-namespace + +# Install with custom values +helm install gitops-status ./charts/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: charts/gitops-status-server + helm: + values: | + 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/charts/gitops-status-server/templates/_helpers.tpl b/charts/gitops-status-server/templates/_helpers.tpl new file mode 100644 index 0000000..7585170 --- /dev/null +++ b/charts/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/charts/gitops-status-server/templates/configmap.yaml b/charts/gitops-status-server/templates/configmap.yaml new file mode 100644 index 0000000..21d073b --- /dev/null +++ b/charts/gitops-status-server/templates/configmap.yaml @@ -0,0 +1,19 @@ +{{/* +ConfigMap containing the status.json file +This file will be mounted into the nginx container +*/}} +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: + # The status.json file that will be served by nginx + # This can be updated by your GitOps pipeline or ArgoCD hooks + status.json: | + {{- .Values.statusJson | toJson | nindent 4 }} diff --git a/charts/gitops-status-server/templates/deployment.yaml b/charts/gitops-status-server/templates/deployment.yaml new file mode 100644 index 0000000..ee48f89 --- /dev/null +++ b/charts/gitops-status-server/templates/deployment.yaml @@ -0,0 +1,103 @@ +{{/* +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 }} + containers: + - 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 status.json file from ConfigMap + # We mount it as a subPath to avoid overwriting the entire directory + - name: status-json + mountPath: /usr/share/nginx/html/status.json + subPath: status.json + readOnly: true + # nginx-unprivileged needs writable directories for cache and run + - name: cache + mountPath: /var/cache/nginx + - name: run + mountPath: /var/run + volumes: + # ConfigMap volume containing the status.json + - name: status-json + configMap: + name: {{ include "gitops-status-server.fullname" . }} + items: + - key: status.json + path: status.json + # Empty directories for nginx runtime + - name: cache + emptyDir: {} + - name: run + 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/charts/gitops-status-server/templates/service.yaml b/charts/gitops-status-server/templates/service.yaml new file mode 100644 index 0000000..6de2897 --- /dev/null +++ b/charts/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/charts/gitops-status-server/templates/serviceaccount.yaml b/charts/gitops-status-server/templates/serviceaccount.yaml new file mode 100644 index 0000000..48517d8 --- /dev/null +++ b/charts/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/charts/gitops-status-server/values.yaml b/charts/gitops-status-server/values.yaml new file mode 100644 index 0000000..50bea72 --- /dev/null +++ b/charts/gitops-status-server/values.yaml @@ -0,0 +1,93 @@ +# 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" + +# 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: "" diff --git a/manifests/gitops-status-server/values.yaml b/manifests/gitops-status-server/values.yaml new file mode 100644 index 0000000..1df7370 --- /dev/null +++ b/manifests/gitops-status-server/values.yaml @@ -0,0 +1,26 @@ +# Minimal values for gitops-status-server +# Override default chart values as needed + +# Status JSON content +# Update this with your actual GitOps status information +statusJson: + repo: "observability-stack" + server: "rsyslog-lab" + sync_status: "UNKNOWN" + drift_count: 0 + files: [] + last_check: "" + +# Resource limits (optional override) +# resources: +# limits: +# cpu: 100m +# memory: 64Mi +# requests: +# cpu: 50m +# memory: 32Mi + +# Service configuration (optional override) +# service: +# type: ClusterIP +# port: 80