Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ jobs:
# --target-branch defaults to ``main``; explicit for clarity.
run: |
ct lint \
--charts charts/promforecast,charts/promforecast-stack \
--charts charts/promforecast,charts/promforecast-stack,charts/promforecast-metrics-adapter \
--validate-maintainers=false \
--check-version-increment=false \
--target-branch ${{ github.event.repository.default_branch }}
Expand All @@ -176,6 +176,7 @@ jobs:
run: |
helm lint --strict charts/promforecast
helm lint --strict charts/promforecast-stack
helm lint --strict charts/promforecast-metrics-adapter

- name: Install kubeconform
run: |
Expand All @@ -186,8 +187,9 @@ jobs:

- name: kubeconform
run: |
helm template charts/promforecast | kubeconform -strict -summary -ignore-missing-schemas
helm template charts/promforecast-stack | kubeconform -strict -summary -ignore-missing-schemas
helm template charts/promforecast | kubeconform -strict -summary -ignore-missing-schemas
helm template charts/promforecast-stack | kubeconform -strict -summary -ignore-missing-schemas
helm template charts/promforecast-metrics-adapter | kubeconform -strict -summary -ignore-missing-schemas
# Also validate the bundled dashboards + PrometheusRule path so CI
# catches regressions in the templated assets, not only the default
# off path.
Expand Down
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ chart-deps: ## Resolve umbrella subchart dependencies.
chart-lint: chart-deps ## helm lint + kubeconform on rendered manifests.
helm lint $(CHART_DIR)/promforecast
helm lint $(CHART_DIR)/promforecast-stack
helm template $(CHART_DIR)/promforecast | kubeconform -strict -summary -ignore-missing-schemas
helm template $(CHART_DIR)/promforecast-stack | kubeconform -strict -summary -ignore-missing-schemas
helm lint $(CHART_DIR)/promforecast-metrics-adapter
helm template $(CHART_DIR)/promforecast | kubeconform -strict -summary -ignore-missing-schemas
helm template $(CHART_DIR)/promforecast-stack | kubeconform -strict -summary -ignore-missing-schemas
helm template $(CHART_DIR)/promforecast-metrics-adapter | kubeconform -strict -summary -ignore-missing-schemas
helm template $(CHART_DIR)/promforecast-stack \
--set dashboards.enabled=true \
--set prometheusRules.enabled=true \
Expand Down
24 changes: 24 additions & 0 deletions charts/promforecast-metrics-adapter/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: v2
name: promforecast-metrics-adapter
description: Kubernetes custom-metrics adapter that exposes promforecast forecast metrics for HorizontalPodAutoscaler.
type: application
version: 0.1.0
appVersion: "0.1.0"
kubeVersion: ">=1.27.0-0"
home: https://github.com/esops-dev/promforecast
sources:
- https://github.com/esops-dev/promforecast
keywords:
- prometheus
- forecasting
- kubernetes
- autoscaling
- hpa
- custom-metrics
maintainers:
- name: esops-dev
url: https://github.com/esops-dev
icon: https://raw.githubusercontent.com/esops-dev/promforecast/main/docs/assets/logo.png
annotations:
artifacthub.io/license: Apache-2.0
artifacthub.io/category: monitoring
131 changes: 131 additions & 0 deletions charts/promforecast-metrics-adapter/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{{/*
Common helpers for the promforecast-metrics-adapter chart.
*/}}

{{- define "promforecast-metrics-adapter.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "promforecast-metrics-adapter.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 -}}

{{- define "promforecast-metrics-adapter.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "promforecast-metrics-adapter.labels" -}}
helm.sh/chart: {{ include "promforecast-metrics-adapter.chart" . }}
{{ include "promforecast-metrics-adapter.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: promforecast
app.kubernetes.io/component: metrics-adapter
{{- end -}}

{{- define "promforecast-metrics-adapter.selectorLabels" -}}
app.kubernetes.io/name: {{ include "promforecast-metrics-adapter.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

{{- define "promforecast-metrics-adapter.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "promforecast-metrics-adapter.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}

{{- define "promforecast-metrics-adapter.image" -}}
{{- $tag := default .Chart.AppVersion .Values.image.tag -}}
{{- printf "%s:%s" .Values.image.repository $tag -}}
{{- end -}}

{{/*
TLS certificate (self-signed when tls.generate=true).

Stored in a ``hiddenSecret`` flavour so re-rendering on each
``helm upgrade`` regenerates the cert. Stable across upgrades within
the same release because the chart pins the CN to the service DNS
name; only the validity period rolls.
*/}}
{{- define "promforecast-metrics-adapter.tlsSecretName" -}}
{{- if .Values.tls.existingSecret -}}
{{- .Values.tls.existingSecret -}}
{{- else -}}
{{- printf "%s-tls" (include "promforecast-metrics-adapter.fullname" .) -}}
{{- end -}}
{{- end -}}

{{/*
TLS material generation.

``genCA`` / ``genSignedCert`` are non-deterministic across calls,
so the cert is generated *once* per Helm render and stashed on
``$.Values`` for the duration of that render. Subsequent calls
(e.g. one for the Secret, one for the APIService) pull the
already-generated material instead of producing a fresh CA.

On ``helm upgrade`` against a live cluster, ``lookup`` finds the
chart-managed Secret from the previous install and the helper
returns *that* material — so the cert + CA bundle stays stable
across upgrades and the APIService caBundle doesn't churn (which
would otherwise force a rolling restart on every upgrade via the
``checksum/tls`` annotation and break in-flight HPA polls). The
fresh-generate path only fires when ``lookup`` returns nothing:
first install, or offline ``helm template`` (which is fine — the
golden-file render is deterministic per process but irrelevant to
a real cluster).

Returns a dict with ``ca``, ``cert``, ``key`` (PEM strings). For
``tls.existingSecret`` installs the helper uses ``lookup`` against
the operator-named Secret and returns its data instead.
*/}}
{{- define "promforecast-metrics-adapter.tlsMaterial" -}}
{{- $fullname := include "promforecast-metrics-adapter.fullname" . -}}
{{- if .Values.tls.existingSecret -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.tls.existingSecret -}}
{{- if $secret -}}
{{- dict "ca" (index $secret.data "ca.crt" | b64dec) "cert" (index $secret.data "tls.crt" | b64dec) "key" (index $secret.data "tls.key" | b64dec) | toYaml -}}
{{- else -}}
{{- /*
The operator set ``tls.existingSecret`` but the Secret isn't in the
cluster (or we're rendering offline via ``helm template`` which
returns nothing from ``lookup``). Failing loud here is the right
default for a production install — the APIService would otherwise
fall back to ``insecureSkipTLSVerify: true`` and silently accept any
upstream cert, which defeats the existingSecret contract.

For offline rendering (CI's ``helm template`` golden-file path),
either pass ``--set tls.generate=true`` to exercise the self-signed
path, or pre-create the Secret in a kind cluster and let the lookup
populate.
*/}}
{{- fail (printf "tls.existingSecret=%q is set but Secret %q was not found in namespace %q. Either create the Secret first (it must carry ca.crt/tls.crt/tls.key), or unset existingSecret and use tls.generate=true. Note: ``helm template`` cannot resolve ``lookup`` against a live cluster; pass --set tls.generate=true to render offline." .Values.tls.existingSecret .Values.tls.existingSecret .Release.Namespace) -}}
{{- end -}}
{{- else -}}
{{- if not (hasKey .Values "_tlsMaterial") -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace (printf "%s-tls" $fullname) -}}
{{- if and $existing (index $existing.data "ca.crt") (index $existing.data "tls.crt") (index $existing.data "tls.key") -}}
{{- $_ := set .Values "_tlsMaterial" (dict "ca" (index $existing.data "ca.crt" | b64dec) "cert" (index $existing.data "tls.crt" | b64dec) "key" (index $existing.data "tls.key" | b64dec)) -}}
{{- else -}}
{{- $svcDNS := printf "%s.%s.svc" $fullname .Release.Namespace -}}
{{- $svcDNSFull := printf "%s.%s.svc.cluster.local" $fullname .Release.Namespace -}}
{{- $altNames := list $svcDNS $svcDNSFull $fullname -}}
{{- $ca := genCA (printf "%s-ca" $fullname) (int .Values.tls.validityDays) -}}
{{- $cert := genSignedCert $svcDNS nil $altNames (int .Values.tls.validityDays) $ca -}}
{{- $_ := set .Values "_tlsMaterial" (dict "ca" $ca.Cert "cert" $cert.Cert "key" $cert.Key) -}}
{{- end -}}
{{- end -}}
{{- .Values._tlsMaterial | toYaml -}}
{{- end -}}
{{- end -}}
45 changes: 45 additions & 0 deletions charts/promforecast-metrics-adapter/templates/apiservice.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{{- if .Values.apiService.enabled -}}
{{- $fullname := include "promforecast-metrics-adapter.fullname" . -}}
{{- $tls := include "promforecast-metrics-adapter.tlsMaterial" . | fromYaml -}}
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.external.metrics.k8s.io
labels:
{{- include "promforecast-metrics-adapter.labels" . | nindent 4 }}
spec:
service:
name: {{ $fullname }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.service.port }}
group: external.metrics.k8s.io
version: v1beta1
versionPriority: 100
groupPriorityMinimum: 100
{{- if $tls.ca }}
caBundle: {{ $tls.ca | b64enc }}
{{- else }}
insecureSkipTLSVerify: true
{{- end }}
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.custom.metrics.k8s.io
labels:
{{- include "promforecast-metrics-adapter.labels" . | nindent 4 }}
spec:
service:
name: {{ $fullname }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.service.port }}
group: custom.metrics.k8s.io
version: v1beta1
versionPriority: 100
groupPriorityMinimum: 100
{{- if $tls.ca }}
caBundle: {{ $tls.ca | b64enc }}
{{- else }}
insecureSkipTLSVerify: true
{{- end }}
{{- end }}
19 changes: 19 additions & 0 deletions charts/promforecast-metrics-adapter/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "promforecast-metrics-adapter.fullname" . }}
labels:
{{- include "promforecast-metrics-adapter.labels" . | nindent 4 }}
data:
adapter.yaml: |
# promforecast-metrics-adapter configuration.
# Schema: docs/predictive-autoscaling.md
datasource_url: {{ .Values.datasourceUrl | quote }}
listen: ":6443"
query_timeout_seconds: 5.0
metrics:
{{- range .Values.metrics }}
- external_metric_name: {{ .externalMetricName | quote }}
promql: {{ .promql | quote }}
namespaced: {{ default true .namespaced }}
{{- end }}
94 changes: 94 additions & 0 deletions charts/promforecast-metrics-adapter/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "promforecast-metrics-adapter.fullname" . }}
labels:
{{- include "promforecast-metrics-adapter.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "promforecast-metrics-adapter.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "promforecast-metrics-adapter.selectorLabels" . | nindent 8 }}
annotations:
# Re-trigger pod rollout when the config or the cert
# changes — Helm's checksum/config + checksum/secret
# annotations are the standard pattern. The TLS material
# helper looks up the chart-managed Secret on upgrade so the
# cert is *stable* across `helm upgrade` and this checksum
# only flips on a real rotation (initial install, the Secret
# being deleted out-of-band, or ``existingSecret`` swapped to
# different material) — uvicorn loads ssl_certfile once at
# boot and never reloads, so the rolling restart is the only
# way to pick up a new cert.
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/tls: {{ include "promforecast-metrics-adapter.tlsMaterial" . | sha256sum }}
spec:
serviceAccountName: {{ include "promforecast-metrics-adapter.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.securityContext | nindent 8 }}
containers:
- name: adapter
image: {{ include "promforecast-metrics-adapter.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- adapter
- --config=/etc/promforecast-adapter/adapter.yaml
- --tls-cert=/var/run/promforecast-adapter/tls.crt
- --tls-key=/var/run/promforecast-adapter/tls.key
ports:
- name: https
containerPort: 6443
protocol: TCP
readinessProbe:
httpGet:
path: /healthz
port: https
scheme: HTTPS
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
livenessProbe:
httpGet:
path: /healthz
port: https
scheme: HTTPS
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/promforecast-adapter
readOnly: true
- name: tls
mountPath: /var/run/promforecast-adapter
readOnly: true
volumes:
- name: config
configMap:
name: {{ include "promforecast-metrics-adapter.fullname" . }}
- name: tls
secret:
secretName: {{ include "promforecast-metrics-adapter.tlsSecretName" . }}
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
Loading