Skip to content

Commit 31bf105

Browse files
feat(metadata): Adds site deploy metadata to cluster-metadata ConfigMap
1 parent 1151cd2 commit 31bf105

7 files changed

Lines changed: 339 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{{- if eq (include "understack.isEnabled" (list $.Values.site "cluster_metadata")) "true" }}
2+
---
3+
apiVersion: argoproj.io/v1alpha1
4+
kind: Application
5+
metadata:
6+
name: {{ printf "%s-%s" $.Release.Name "cluster-metadata" }}
7+
finalizers:
8+
- resources-finalizer.argocd.argoproj.io
9+
{{- include "understack.appLabelsBlock" $ | nindent 2 }}
10+
spec:
11+
destination:
12+
namespace: openstack
13+
server: {{ $.Values.cluster_server }}
14+
project: understack
15+
sources:
16+
- path: components/cluster-metadata
17+
repoURL: {{ include "understack.understack_url" $ }}
18+
targetRevision: {{ include "understack.understack_ref" $ }}
19+
syncPolicy:
20+
automated:
21+
prune: true
22+
selfHeal: true
23+
syncOptions:
24+
- ServerSideApply=true
25+
- RespectIgnoreDifferences=true
26+
- ApplyOutOfSyncOnly=true
27+
{{- end }}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{{- if or (eq (include "understack.isEnabled" (list $.Values.global "prometheus_pushgateway")) "true") (eq (include "understack.isEnabled" (list $.Values.site "prometheus_pushgateway")) "true") }}
2+
---
3+
apiVersion: argoproj.io/v1alpha1
4+
kind: Application
5+
metadata:
6+
name: {{ printf "%s-%s" $.Release.Name "prometheus-pushgateway" }}
7+
annotations:
8+
argocd.argoproj.io/compare-options: ServerSideDiff=true,IncludeMutationWebhook=true
9+
{{- include "understack.appLabelsBlock" $ | nindent 2 }}
10+
spec:
11+
destination:
12+
namespace: monitoring
13+
server: {{ $.Values.cluster_server }}
14+
project: understack-operators
15+
sources:
16+
- chart: prometheus-pushgateway
17+
helm:
18+
ignoreMissingValueFiles: true
19+
releaseName: prometheus-pushgateway
20+
valueFiles:
21+
- $understack/operators/prometheus-pushgateway/values.yaml
22+
repoURL: https://prometheus-community.github.io/helm-charts
23+
targetRevision: 3.2.0
24+
- ref: understack
25+
repoURL: {{ include "understack.understack_url" $ }}
26+
targetRevision: {{ include "understack.understack_ref" $ }}
27+
syncPolicy:
28+
automated:
29+
prune: true
30+
selfHeal: true
31+
syncOptions:
32+
- CreateNamespace=true
33+
- ServerSideApply=true
34+
- RespectIgnoreDifferences=true
35+
- ApplyOutOfSyncOnly=true
36+
{{- end }}

charts/argocd-understack/values.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ site:
508508
# @default -- false
509509
enabled: false
510510

511+
# -- Cluster metadata sync to Prometheus via Pushgateway
512+
cluster_metadata:
513+
# -- Enable/disable syncing cluster-metadata ConfigMap to Prometheus
514+
# @default -- true
515+
enabled: true
516+
517+
# -- Prometheus Pushgateway for batch job metrics
518+
prometheus_pushgateway:
519+
# -- Enable/disable deploying Prometheus Pushgateway
520+
# @default -- true
521+
enabled: true
522+
511523
# -- OpenEBS
512524
openebs:
513525
# -- Enable/disable deploying OpenEBS
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
apiVersion: v1
3+
kind: ConfigMap
4+
metadata:
5+
name: cluster-metadata-sync-script
6+
namespace: openstack
7+
labels:
8+
app.kubernetes.io/name: cluster-metadata
9+
app.kubernetes.io/component: prometheus-sync
10+
data:
11+
sync-cluster-metadata.py: |
12+
#!/usr/bin/env python3
13+
"""Sync cluster-metadata ConfigMap data to Prometheus Pushgateway.
14+
15+
Reads the cluster-metadata ConfigMap mounted as files and pushes
16+
its key/value pairs as labels on an info-style gauge metric to the
17+
Prometheus Pushgateway. This makes cluster identity information
18+
(partition, site, etc.) available for Prometheus queries and joins.
19+
20+
Files ending in .yaml/.yml are parsed and their top-level scalar
21+
keys are flattened into individual labels. Nested structures are
22+
skipped since Prometheus labels must be simple strings.
23+
"""
24+
25+
import os
26+
import sys
27+
import urllib.request
28+
29+
# PyYAML is not available in the slim image, so we use a minimal
30+
# subset parser for simple key: value YAML files.
31+
import json
32+
import re
33+
34+
METADATA_DIR = "/etc/cluster-metadata"
35+
PUSHGATEWAY_URL = os.environ.get(
36+
"PUSHGATEWAY_URL",
37+
"http://prometheus-pushgateway.monitoring.svc.cluster.local:9091",
38+
)
39+
METRIC_JOB_NAME = os.environ.get("METRIC_JOB_NAME", "cluster_metadata")
40+
41+
42+
def parse_yaml_scalars(content, prefix=""):
43+
"""Extract key/value pairs from a YAML file, flattening nested mappings.
44+
45+
Top-level scalar values become labels directly.
46+
Nested mappings get their parent key prepended as a prefix
47+
(e.g., site.argo_events.enabled -> site_argo_events_enabled).
48+
"""
49+
results = {}
50+
lines = content.splitlines()
51+
i = 0
52+
# Track indentation-based hierarchy
53+
stack = [] # list of (indent_level, key_prefix)
54+
55+
while i < len(lines):
56+
line = lines[i]
57+
stripped = line.rstrip()
58+
if not stripped or stripped.lstrip().startswith("#") or stripped.strip() == "---":
59+
i += 1
60+
continue
61+
62+
# Determine indentation level
63+
indent = len(line) - len(line.lstrip())
64+
65+
# Pop stack entries that are at same or deeper indent
66+
while stack and stack[-1][0] >= indent:
67+
stack.pop()
68+
69+
# Build current prefix from stack
70+
current_prefix = "_".join(s[1] for s in stack)
71+
if current_prefix and prefix:
72+
current_prefix = prefix + "_" + current_prefix
73+
elif prefix:
74+
current_prefix = prefix
75+
# If no stack and no prefix, current_prefix stays ""
76+
77+
match = re.match(r"^(\s*)([A-Za-z_][A-Za-z0-9_.\-]*)\s*:\s*(.*)", line)
78+
if match:
79+
key = match.group(2)
80+
value = match.group(3).strip()
81+
82+
# If value is empty, it's a mapping parent — push onto stack
83+
if value == "" or value == "|" or value == ">":
84+
stack.append((indent, key))
85+
i += 1
86+
continue
87+
88+
# Strip inline comments
89+
if " #" in value:
90+
value = value[:value.index(" #")].rstrip()
91+
92+
# Strip surrounding quotes if present
93+
if (value.startswith("'") and value.endswith("'")) or \
94+
(value.startswith('"') and value.endswith('"')):
95+
value = value[1:-1]
96+
97+
full_key = f"{current_prefix}_{key}" if current_prefix else key
98+
results[full_key] = value
99+
i += 1
100+
return results
101+
102+
103+
def read_metadata():
104+
"""Read all key/value pairs from the mounted ConfigMap directory.
105+
106+
Plain files become a single key=value pair.
107+
YAML files (.yaml/.yml) are parsed and top-level scalar keys are
108+
extracted as individual entries.
109+
"""
110+
data = {}
111+
for filename in os.listdir(METADATA_DIR):
112+
filepath = os.path.join(METADATA_DIR, filename)
113+
if not os.path.isfile(filepath) or filename.startswith(".."):
114+
continue
115+
with open(filepath) as f:
116+
content = f.read().strip()
117+
118+
if filename.endswith((".yaml", ".yml")):
119+
# Parse YAML and flatten all scalar keys with hierarchy as prefix
120+
scalars = parse_yaml_scalars(content)
121+
for key, value in scalars.items():
122+
data[key] = value
123+
else:
124+
data[filename] = content
125+
return data
126+
127+
128+
def format_pushgateway_payload(cm_data):
129+
"""Format ConfigMap data as Prometheus exposition format.
130+
131+
Creates an info-style gauge metric 'cluster_metadata_info' with value 1
132+
and all ConfigMap keys as labels.
133+
"""
134+
labels = {}
135+
for key, value in cm_data.items():
136+
label_name = key.lower().replace("-", "_").replace(".", "_")
137+
label_value = (
138+
value.replace("\\", "\\\\")
139+
.replace("\n", "\\n")
140+
.replace('"', '\\"')
141+
)
142+
labels[label_name] = label_value
143+
144+
if not labels:
145+
print("WARNING: ConfigMap has no data keys, nothing to push")
146+
return None
147+
148+
label_str = ",".join(f'{k}="{v}"' for k, v in sorted(labels.items()))
149+
lines = [
150+
"# HELP cluster_metadata_info Cluster metadata from ConfigMap as labels.",
151+
"# TYPE cluster_metadata_info gauge",
152+
f"cluster_metadata_info{{{label_str}}} 1",
153+
"",
154+
]
155+
return "\n".join(lines)
156+
157+
158+
def push_to_gateway(payload):
159+
"""Push metrics payload to the Prometheus Pushgateway."""
160+
url = f"{PUSHGATEWAY_URL}/metrics/job/{METRIC_JOB_NAME}"
161+
data = payload.encode("utf-8")
162+
req = urllib.request.Request(
163+
url,
164+
data=data,
165+
method="PUT",
166+
headers={"Content-Type": "text/plain; version=0.0.4"},
167+
)
168+
with urllib.request.urlopen(req) as resp:
169+
if resp.status not in (200, 202):
170+
print(f"ERROR: Pushgateway returned status {resp.status}")
171+
sys.exit(1)
172+
173+
174+
def main():
175+
print("Reading cluster-metadata from mounted ConfigMap...")
176+
cm_data = read_metadata()
177+
print(f"Found {len(cm_data)} keys: {list(cm_data.keys())}")
178+
179+
payload = format_pushgateway_payload(cm_data)
180+
if payload is None:
181+
sys.exit(0)
182+
183+
print(f"Pushing cluster_metadata_info metric to {PUSHGATEWAY_URL}...")
184+
push_to_gateway(payload)
185+
print("Successfully pushed cluster metadata to Prometheus Pushgateway")
186+
187+
188+
if __name__ == "__main__":
189+
main()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
apiVersion: batch/v1
3+
kind: Job
4+
metadata:
5+
name: sync-cluster-metadata-to-prometheus
6+
namespace: openstack
7+
labels:
8+
app.kubernetes.io/name: cluster-metadata
9+
app.kubernetes.io/component: prometheus-sync
10+
annotations:
11+
argocd.argoproj.io/hook: PostSync
12+
argocd.argoproj.io/sync-wave: "5"
13+
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
14+
spec:
15+
backoffLimit: 3
16+
ttlSecondsAfterFinished: 3600
17+
template:
18+
spec:
19+
securityContext:
20+
runAsNonRoot: true
21+
runAsUser: 65534
22+
fsGroup: 65534
23+
seccompProfile:
24+
type: RuntimeDefault
25+
restartPolicy: OnFailure
26+
containers:
27+
- name: sync-metadata
28+
image: python:3-slim
29+
imagePullPolicy: IfNotPresent
30+
command:
31+
- python
32+
- /scripts/sync-cluster-metadata.py
33+
resources:
34+
requests:
35+
cpu: "50m"
36+
memory: "64Mi"
37+
limits:
38+
cpu: "200m"
39+
memory: "128Mi"
40+
securityContext:
41+
allowPrivilegeEscalation: false
42+
capabilities:
43+
drop:
44+
- ALL
45+
readOnlyRootFilesystem: false
46+
env:
47+
- name: PUSHGATEWAY_URL
48+
value: http://prometheus-pushgateway.monitoring.svc.cluster.local:9091
49+
- name: METRIC_JOB_NAME
50+
value: cluster_metadata
51+
volumeMounts:
52+
- name: scripts
53+
mountPath: /scripts
54+
readOnly: true
55+
- name: cluster-metadata
56+
mountPath: /etc/cluster-metadata
57+
readOnly: true
58+
volumes:
59+
- name: scripts
60+
configMap:
61+
name: cluster-metadata-sync-script
62+
- name: cluster-metadata
63+
configMap:
64+
name: cluster-metadata
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
apiVersion: kustomize.config.k8s.io/v1beta1
3+
kind: Kustomization
4+
5+
resources:
6+
- configmap-cluster-metadata-sync-script.yaml
7+
- job-sync-cluster-metadata.yaml
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
serviceMonitor:
2+
enabled: true
3+
additionalLabels:
4+
release: monitoring

0 commit comments

Comments
 (0)