Add open-meteo-service app

This commit is contained in:
dvirlabs 2026-02-15 21:13:12 +02:00
parent d38225e866
commit 93f9f13251
18 changed files with 716 additions and 0 deletions

View File

@ -0,0 +1,21 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: open-meteo-service
namespace: argocd
spec:
project: sandbox
source:
repoURL: https://git.dvirlabs.com/dvirlabs/sandbox.git
targetRevision: HEAD
path: charts/open-meteo-service
helm:
valueFiles:
- ../../manifests/open-meteo-service/values.yaml
destination:
server: https://kubernetes.default.svc
namespace: sandbox
syncPolicy:
automated:
prune: true
selfHeal: true

View File

@ -0,0 +1,6 @@
apiVersion: v2
name: open-meteo-service
description: A FastAPI service that provides city coordinates with Prometheus metrics
type: application
version: 0.1.0
appVersion: "1.0.0"

View File

@ -0,0 +1,107 @@
{
"uid": "open-meteo-service",
"title": "Open-Meteo Service",
"timezone": "browser",
"schemaVersion": 38,
"version": 1,
"refresh": "10s",
"time": {
"from": "now-15m",
"to": "now"
},
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Request Rate",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (endpoint, method)",
"legendFormat": "{{endpoint}} {{method}}",
"refId": "A"
}
]
},
{
"id": 2,
"type": "timeseries",
"title": "Request Duration p95",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, endpoint, method))",
"legendFormat": "{{endpoint}} {{method}}",
"refId": "A"
}
]
},
{
"id": 3,
"type": "timeseries",
"title": "Cache Hits vs Misses",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": { "x": 0, "y": 8, "w": 12, "h": 8 },
"targets": [
{
"expr": "rate(coordinates_cache_hits_total[5m])",
"legendFormat": "hits",
"refId": "A"
},
{
"expr": "rate(coordinates_cache_misses_total[5m])",
"legendFormat": "misses",
"refId": "B"
}
]
},
{
"id": 4,
"type": "timeseries",
"title": "Open-Meteo Calls by City",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 },
"targets": [
{
"expr": "sum(rate(openmeteo_api_calls_total[5m])) by (city)",
"legendFormat": "{{city}}",
"refId": "A"
}
]
},
{
"id": 5,
"type": "timeseries",
"title": "Requests by Status",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": { "x": 0, "y": 16, "w": 24, "h": 8 },
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (status)",
"legendFormat": "{{status}}",
"refId": "A"
}
]
}
],
"templating": {
"list": []
}
}

View File

@ -0,0 +1,10 @@
apiVersion: 1
providers:
- name: default
type: file
disableDeletion: false
editable: true
updateIntervalSeconds: 10
options:
path: /var/lib/grafana/dashboards

View File

@ -0,0 +1,27 @@
{{- define "open-meteo-service.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "open-meteo-service.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "open-meteo-service.labels" -}}
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "open-meteo-service.prometheusFullname" -}}
{{- printf "%s-prometheus" (include "open-meteo-service.fullname" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "open-meteo-service.grafanaFullname" -}}
{{- printf "%s-grafana" (include "open-meteo-service.fullname" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}

View File

@ -0,0 +1,67 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "open-meteo-service.fullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
{{- include "open-meteo-service.labels" . | nindent 8 }}
annotations:
{{- toYaml .Values.podAnnotations | nindent 8 }}
spec:
containers:
- name: {{ include "open-meteo-service.name" . }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: CACHE_FILE
value: {{ .Values.env.cacheFile | quote }}
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: cache-data
mountPath: /data
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: cache-data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "open-meteo-service.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,24 @@
{{- if .Values.grafana.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "open-meteo-service.grafanaFullname" . }}-config
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: grafana
data:
datasource.yml: |-
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://{{ include "open-meteo-service.prometheusFullname" . }}:{{ .Values.prometheus.service.port }}
isDefault: true
uid: prometheus
dashboard.yml: |-
{{ .Files.Get "files/grafana/provisioning/dashboards/dashboard.yml" | nindent 4 }}
open-meteo-service.json: |-
{{ .Files.Get "files/grafana/dashboards/open-meteo-service.json" | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,59 @@
{{- if .Values.grafana.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "open-meteo-service.grafanaFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: grafana
spec:
replicas: {{ .Values.grafana.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: grafana
template:
metadata:
labels:
{{- include "open-meteo-service.labels" . | nindent 8 }}
app.kubernetes.io/component: grafana
spec:
containers:
- name: grafana
image: {{ .Values.grafana.image | quote }}
ports:
- name: http
containerPort: {{ .Values.grafana.service.port }}
protocol: TCP
env:
- name: GF_SECURITY_ADMIN_USER
value: {{ .Values.grafana.adminUser | quote }}
- name: GF_SECURITY_ADMIN_PASSWORD
value: {{ .Values.grafana.adminPassword | quote }}
volumeMounts:
- name: grafana-config
mountPath: /etc/grafana/provisioning/datasources/datasource.yml
subPath: datasource.yml
- name: grafana-config
mountPath: /etc/grafana/provisioning/dashboards/dashboard.yml
subPath: dashboard.yml
- name: grafana-config
mountPath: /var/lib/grafana/dashboards/open-meteo-service.json
subPath: open-meteo-service.json
- name: grafana-data
mountPath: /var/lib/grafana
resources:
{{- toYaml .Values.grafana.resources | nindent 12 }}
volumes:
- name: grafana-config
configMap:
name: {{ include "open-meteo-service.grafanaFullname" . }}-config
- name: grafana-data
{{- if .Values.grafana.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "open-meteo-service.grafanaFullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}

View File

@ -0,0 +1,20 @@
{{- if .Values.grafana.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "open-meteo-service.grafanaFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: grafana
spec:
type: {{ .Values.grafana.service.type }}
ports:
- port: {{ .Values.grafana.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: grafana
{{- end }}

View File

@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "open-meteo-service.fullname" . }}
{{- if .Values.ingress.className }}
annotations:
kubernetes.io/ingress.class: {{ .Values.ingress.className }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "open-meteo-service.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,20 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "open-meteo-service.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT/docs
{{- else if contains "LoadBalancer" .Values.service.type }}
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "open-meteo-service.fullname" . }} -o jsonpath="{.status.loadBalancer.ingress[0].ip}")
echo http://$SERVICE_IP:{{ .Values.service.port }}/docs
{{- else }}
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "open-meteo-service.fullname" . }} 8080:{{ .Values.service.port }}
echo http://127.0.0.1:8080/docs
{{- end }}
2. Prometheus (if enabled):
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "open-meteo-service.prometheusFullname" . }} 9090:{{ .Values.prometheus.service.port }}
echo http://127.0.0.1:9090
3. Grafana (if enabled):
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "open-meteo-service.grafanaFullname" . }} 3000:{{ .Values.grafana.service.port }}
echo http://127.0.0.1:3000 ({{ .Values.grafana.adminUser }}/{{ .Values.grafana.adminPassword }})

View File

@ -0,0 +1,23 @@
{{- if .Values.prometheus.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "open-meteo-service.prometheusFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: prometheus
data:
prometheus.yml: |-
global:
scrape_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["{{ include "open-meteo-service.prometheusFullname" . }}:{{ .Values.prometheus.service.port }}"]
- job_name: "open-meteo-service"
metrics_path: "/metrics"
static_configs:
- targets: ["{{ include "open-meteo-service.fullname" . }}:{{ .Values.service.port }}"]
{{- end }}

View File

@ -0,0 +1,54 @@
{{- if .Values.prometheus.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "open-meteo-service.prometheusFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: prometheus
spec:
replicas: {{ .Values.prometheus.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: prometheus
template:
metadata:
labels:
{{- include "open-meteo-service.labels" . | nindent 8 }}
app.kubernetes.io/component: prometheus
spec:
containers:
- name: prometheus
image: {{ .Values.prometheus.image | quote }}
ports:
- name: http
containerPort: {{ .Values.prometheus.service.port }}
protocol: TCP
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
{{- range .Values.prometheus.extraArgs }}
- {{ . | quote }}
{{- end }}
volumeMounts:
- name: prometheus-config
mountPath: /etc/prometheus/prometheus.yml
subPath: prometheus.yml
- name: prometheus-data
mountPath: /prometheus
resources:
{{- toYaml .Values.prometheus.resources | nindent 12 }}
volumes:
- name: prometheus-config
configMap:
name: {{ include "open-meteo-service.prometheusFullname" . }}
- name: prometheus-data
{{- if .Values.prometheus.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "open-meteo-service.prometheusFullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}

View File

@ -0,0 +1,20 @@
{{- if .Values.prometheus.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "open-meteo-service.prometheusFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: prometheus
spec:
type: {{ .Values.prometheus.service.type }}
ports:
- port: {{ .Values.prometheus.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: prometheus
{{- end }}

View File

@ -0,0 +1,57 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "open-meteo-service.fullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClassName }}
storageClassName: {{ .Values.persistence.storageClassName | quote }}
{{- end }}
{{- end }}
{{- if .Values.prometheus.persistence.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "open-meteo-service.prometheusFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: prometheus
spec:
accessModes:
{{- toYaml .Values.prometheus.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.prometheus.persistence.size }}
{{- if .Values.prometheus.persistence.storageClassName }}
storageClassName: {{ .Values.prometheus.persistence.storageClassName | quote }}
{{- end }}
{{- end }}
{{- if .Values.grafana.persistence.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "open-meteo-service.grafanaFullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
app.kubernetes.io/component: grafana
spec:
accessModes:
{{- toYaml .Values.grafana.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.grafana.persistence.size }}
{{- if .Values.grafana.persistence.storageClassName }}
storageClassName: {{ .Values.grafana.persistence.storageClassName | quote }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "open-meteo-service.fullname" . }}
labels:
{{- include "open-meteo-service.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "open-meteo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}

View File

@ -0,0 +1,76 @@
replicaCount: 1
image:
repository: open-meteo-service
tag: "local"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8000
ingress:
enabled: false
className: ""
hosts:
- host: open-meteo.local
paths:
- path: /
pathType: Prefix
tls: []
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "8000"
env:
cacheFile: /data/coordinates_cache.json
persistence:
enabled: false
accessModes:
- ReadWriteOnce
size: 1Gi
storageClassName: ""
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
prometheus:
enabled: true
replicaCount: 1
image: prom/prometheus:latest
service:
type: ClusterIP
port: 9090
persistence:
enabled: false
accessModes:
- ReadWriteOnce
size: 5Gi
storageClassName: ""
resources: {}
extraArgs: []
grafana:
enabled: true
replicaCount: 1
image: grafana/grafana:latest
service:
type: ClusterIP
port: 3000
adminUser: admin
adminPassword: admin
persistence:
enabled: false
accessModes:
- ReadWriteOnce
size: 5Gi
storageClassName: ""
resources: {}

View File

@ -0,0 +1,76 @@
replicaCount: 1
image:
repository: open-meteo-service
tag: "local"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8000
ingress:
enabled: false
className: "traefik"
hosts:
- host: open-meteo.dvirlabs.com
paths:
- path: /
pathType: Prefix
tls: []
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "8000"
env:
cacheFile: /data/coordinates_cache.json
persistence:
enabled: false
accessModes:
- ReadWriteOnce
size: 1Gi
storageClassName: ""
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
prometheus:
enabled: true
replicaCount: 1
image: prom/prometheus:latest
service:
type: ClusterIP
port: 9090
persistence:
enabled: false
accessModes:
- ReadWriteOnce
size: 5Gi
storageClassName: ""
resources: {}
extraArgs: []
grafana:
enabled: true
replicaCount: 1
image: grafana/grafana:latest
service:
type: ClusterIP
port: 3000
adminUser: admin
adminPassword: admin
persistence:
enabled: false
accessModes:
- ReadWriteOnce
size: 5Gi
storageClassName: ""
resources: {}