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
3 changes: 2 additions & 1 deletion helm/helm-framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Base Helm framework library
| envVars | list | `[]` | Plain environment variables (list of {name, value}), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per name. Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. |
| envVarsFromSecret | object | `{}` | Environment variables sourced from a Secret (map of key: value), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per key. |
| externalSecrets | object | `{}` | External Secrets Operator ExternalSecret entries (map; one ExternalSecret resource per key, named after the map key). |
| extraServices | list | `[]` | Additional Services in front of the same pods (list; one entry per Service). Each entry renders `<fullname>-<name>` with this chart's standard selector, so it always targets the same workload as the main Service — use it for a headless Service for pod-level discovery, or a second Service with different annotations or type. Omitting `ports` mirrors the main Service's ports (main container + `service.extraPorts` + sidecars); set `ports` explicitly to expose a different subset. A `name` is required on every port once an entry exposes more than one. |
| forceReload | bool | `false` | When true, adds a randomized pod annotation on each render so the Deployment restarts its pods, even when nothing else changed. |
| fullnameOverride | string | `""` | Override the full release name used for resource names. |
| healthChecks | object | `{"enabled":true,"livenessProbe":{"failureThreshold":10,"initialDelaySeconds":1,"path":"/health/liveness","periodSeconds":10,"successThreshold":1,"timeoutSeconds":3},"readinessProbe":{"failureThreshold":3,"initialDelaySeconds":1,"path":"/health/readiness","periodSeconds":10,"successThreshold":1,"timeoutSeconds":3},"startupProbe":{"failureThreshold":30,"initialDelaySeconds":10,"path":"/health/startup","periodSeconds":5,"successThreshold":1,"timeoutSeconds":3}}` | Startup/liveness/readiness probes for the main container. Set enabled=false to omit all probes. |
Expand All @@ -42,7 +43,7 @@ Base Helm framework library
| resources | object | `{}` | Container resource requests/limits. Rendered as-is UNLESS verticalPodAutoscaler is enabled with an active updatePolicy.updateMode (i.e. anything other than the default "Off") for this container — in which case whichever field VPA actually manages is dropped from here and deferred to it, per the effective containerPolicies[].controlledValues for this container ("RequestsAndLimits", VPA's own default, or "RequestsOnly"): - RequestsAndLimits (default): `requests` renders from here, `limits` is deferred to VPA (VPA sets both). - RequestsOnly: `limits` renders from here (VPA never touches limits in this mode, so this is the only place they can come from), `requests` is deferred to VPA. With updateMode "Off" (recommendation-only, the default) VPA never mutates the pod at all, so both requests and limits set here always apply as-is. |
| secretStore | object | `{}` | External Secrets Operator SecretStore (one per release, named <fullname>). |
| securityContext | object | `{}` | Container-level security context (applied to all containers). Same no-defaults behavior as podSecurityContext above — renders exactly what you set, nothing merged in. Set fields explicitly as needed, e.g.: |
| service | object | `{"annotations":{},"externalTrafficPolicy":"","port":80,"sessionAffinity":"","sessionAffinityConfig":{"clientIPTimeoutSeconds":0},"targetPort":8080,"targetScheme":"HTTP","type":"ClusterIP"}` | Service exposing the workload. `sessionAffinity: "ClientIP"` enables sticky sessions, optionally tuned with `sessionAffinityConfig.clientIPTimeoutSeconds` (Kubernetes defaults to 10800s/3h when ClientIP affinity is set without a timeout). `externalTrafficPolicy` controls client-IP preservation for `NodePort`/`LoadBalancer` service types. |
| service | object | `{"annotations":{},"externalTrafficPolicy":"","extraPorts":[],"port":80,"sessionAffinity":"","sessionAffinityConfig":{"clientIPTimeoutSeconds":0},"targetPort":8080,"targetScheme":"HTTP","type":"ClusterIP"}` | Service exposing the workload. `sessionAffinity: "ClientIP"` enables sticky sessions, optionally tuned with `sessionAffinityConfig.clientIPTimeoutSeconds` (Kubernetes defaults to 10800s/3h when ClientIP affinity is set without a timeout). `externalTrafficPolicy` controls client-IP preservation for `NodePort`/`LoadBalancer` service types. `extraPorts` exposes additional ports of the *main* container on this same Service (sidecar ports come from `sidecars[].service.port` instead); each entry needs a unique `name` and `port`, and a matching `containerPort` is declared on the main container whenever `targetPort` resolves to a number. Every port on the Service — main, `extraPorts`, and sidecars — must have a unique name and number. |
| serviceAccount | object | `{"annotations":{},"automount":false,"create":false,"name":""}` | ServiceAccount creation and reference. `automount` shown here (false) is only the fallback: the template overrides it to true whenever a job is flagged `waitForIt: true`, since the wait-for-job init container needs the ServiceAccount token to call the Kubernetes API (see the auto-provisioned RBAC Role/RoleBinding under `rbac` below). Set `automount` explicitly to force one behavior or the other regardless of `waitForIt`. |
| sidecars | list | `[]` | Additional containers in the pod (list; one entry per sidecar). Each entry requires a unique `name`, used to derive the container, port, and Secret names. Set a distinct `service.port` per sidecar if more than one needs a Service port. |
| strategy | object | `{}` | Deployment update strategy (spec.strategy). Empty uses the cluster default. |
Expand Down
1 change: 1 addition & 0 deletions helm/helm-framework/templates/_deployment-global.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
(include "helm-framework.deployment.authorizationPolicy" .)
(include "helm-framework.deployment.ingress" .)
(include "helm-framework.deployment.service" .)
(include "helm-framework.deployment.extra-services" .)
(include "helm-framework.deployment.virtual-service" .)
(include "helm-framework.deployment.httproute" .)
(include "helm-framework.deployment.serviceAccount" .)
Expand Down
9 changes: 9 additions & 0 deletions helm/helm-framework/templates/_deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ spec:
- name: http
containerPort: {{ (include "helm-framework.values.service.targetPort" .) }}
protocol: TCP
{{- /* Declare a container port for every service.extraPorts entry that resolves to a number. A string targetPort references a port declared elsewhere, so nothing is added for it. */}}
{{- range $p := (.Values.service).extraPorts }}
{{- $targetPort := $p.targetPort | default $p.port }}
{{- if not (kindIs "string" $targetPort) }}
- name: {{ $p.name }}
containerPort: {{ $targetPort }}
protocol: {{ $p.protocol | default "TCP" }}
{{- end }}
{{- end }}
{{- include "helm-framework.probes" (dict "healthChecks" .Values.healthChecks "portName" "http" "scheme" (include "helm-framework.values.service.targetScheme" .)) | nindent 10 }}
{{- with .Values.envVars }}
env:
Expand Down
117 changes: 106 additions & 11 deletions helm/helm-framework/templates/_service.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
{{/*
Renders an arbitrary list of Service port entries. `name` is optional (a
single-port Service may leave it out); _values-validation.tpl requires it once
a Service exposes more than one port.
*/}}
{{- define "helm-framework.service.portList" -}}
{{- range $p := .ports }}
- port: {{ $p.port }}
targetPort: {{ $p.targetPort | default $p.port }}
protocol: {{ $p.protocol | default "TCP" }}
{{- with $p.name }}
name: {{ . }}
{{- end }}
{{- with $p.appProtocol }}
appProtocol: {{ . }}
{{- end }}
{{- with $p.nodePort }}
nodePort: {{ . }}
{{- end }}
{{- end }}
{{- end }}

{{/*
The canonical port list for this chart's workload: the main container's `http`
port, then `service.extraPorts`, then one port per enabled sidecar. Both the
main Service and any `extraServices` entry that omits `ports` render from here,
so the two can't drift apart.
*/}}
{{- define "helm-framework.service.ports" -}}
- port: {{ (include "helm-framework.values.service.port" .) }}
targetPort: http
protocol: TCP
name: http
{{- with (.Values.service).extraPorts }}
{{ include "helm-framework.service.portList" (dict "ports" .) | trim }}
{{- end }}
{{- range $sc := .Values.sidecars }}
{{- if $sc.enabled }}
- port: {{ include "helm-framework.values.service.port" $sc }}
targetPort: {{ include "helm-framework.sidecar.portName" $sc }}
protocol: TCP
name: {{ include "helm-framework.sidecar.portName" $sc }}
{{- end }}
{{- end }}
{{- end }}

{{- define "helm-framework.deployment.service" -}}
apiVersion: v1
kind: Service
Expand Down Expand Up @@ -25,18 +71,67 @@ spec:
{{- end }}
{{- end }}
ports:
- port: {{ (include "helm-framework.values.service.port" .) }}
targetPort: http
protocol: TCP
name: http
{{- range $sc := .Values.sidecars }}
{{- if $sc.enabled }}
- port: {{ include "helm-framework.values.service.port" $sc }}
targetPort: {{ include "helm-framework.sidecar.portName" $sc }}
protocol: TCP
name: {{ include "helm-framework.sidecar.portName" $sc }}
{{- include "helm-framework.service.ports" . | trim | nindent 4 }}
selector:
{{- include "helm-framework.selectorLabels" . | nindent 4 }}
{{- end }}

{{/*
One additional Service, selecting the same pods as the main Service. Called per
enabled `extraServices` entry with (dict "root" $ "svc" <entry>).
*/}}
{{- define "helm-framework.service.extra" -}}
{{- $root := .root -}}
{{- $svc := .svc -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "helm-framework.fullname" $root }}-{{ $svc.name }}
{{- with $svc.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "helm-framework.labels" $root | nindent 4 }}
{{- with $svc.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ $svc.type | default "ClusterIP" }}
{{- with $svc.clusterIP }}
clusterIP: {{ . }}
{{- end }}
{{- if $svc.publishNotReadyAddresses }}
publishNotReadyAddresses: true
{{- end }}
{{- with $svc.externalTrafficPolicy }}
externalTrafficPolicy: {{ . }}
{{- end }}
{{- with $svc.sessionAffinity }}
sessionAffinity: {{ . }}
{{- if and (eq . "ClientIP") (($svc.sessionAffinityConfig).clientIPTimeoutSeconds) }}
sessionAffinityConfig:
clientIP:
timeoutSeconds: {{ $svc.sessionAffinityConfig.clientIPTimeoutSeconds }}
{{- end }}
{{- end }}
ports:
{{- if $svc.ports }}
{{- include "helm-framework.service.portList" (dict "ports" $svc.ports) | trim | nindent 4 }}
{{- else }}
{{- include "helm-framework.service.ports" $root | trim | nindent 4 }}
{{- end }}
selector:
{{- include "helm-framework.selectorLabels" . | nindent 4 }}
{{- include "helm-framework.selectorLabels" $root | nindent 4 }}
{{- end }}

{{- define "helm-framework.deployment.extra-services" -}}
{{- $root := . -}}
{{- $rendered := list -}}
{{- range $svc := .Values.extraServices }}
{{- if $svc.enabled }}
{{- $rendered = append $rendered (trim (include "helm-framework.service.extra" (dict "root" $root "svc" $svc))) -}}
{{- end }}
{{- end }}
{{- $rendered | join "\n---\n" -}}
{{- end }}
65 changes: 64 additions & 1 deletion helm/helm-framework/templates/_values-validation.tpl
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
{{/*
Cross-cutting values validation for mechanisms not covered by
_keda-validation.tpl / _autoscaling-validation.tpl: sidecars, jobs, ingress,
_keda-validation.tpl / _autoscaling-validation.tpl: Service ports (main,
service.extraPorts, sidecars), extraServices, sidecars, jobs, ingress,
virtualService, authorizationPolicy, and podDisruptionBudget.
*/}}

{{- define "helm-framework.values.validate" -}}
{{- $svcPort := (include "helm-framework.values.service.port" .) -}}
{{- $seenNames := dict -}}
{{- $seenPorts := dict (toString $svcPort) "service.port (main container)" -}}
{{- $seenPortNames := dict "http" "the main container's Service port" -}}
{{- range $index, $p := (.Values.service).extraPorts }}
{{- if not $p.name }}
{{- fail (printf "service.extraPorts[%d] has no name: every entry must be named, since adding one makes the Service expose more than one port." $index) }}
{{- end }}
{{- if not $p.port }}
{{- fail (printf "service.extraPorts[%d] (%s) has no port: set the port the Service should expose." $index $p.name) }}
{{- end }}
{{- if hasKey $seenPortNames $p.name }}
{{- fail (printf "service.extraPorts[%d]'s name %q collides with %s: each port exposed on the chart's Service must have a unique name." $index $p.name (get $seenPortNames $p.name)) }}
{{- end }}
{{- $_ := set $seenPortNames $p.name (printf "service.extraPorts %q" $p.name) -}}
{{- $port := toString $p.port -}}
{{- if hasKey $seenPorts $port }}
{{- fail (printf "service.extraPorts[%d] (%s) port %s collides with %s: each port exposed on the chart's Service must be unique." $index $p.name $port (get $seenPorts $port)) }}
{{- end }}
{{- $_ := set $seenPorts $port (printf "service.extraPorts %q" $p.name) -}}
{{- end }}
{{- range .Values.sidecars }}
{{- if .enabled }}
{{- $name := .name -}}
Expand All @@ -20,6 +39,50 @@ virtualService, authorizationPolicy, and podDisruptionBudget.
{{- fail (printf "Sidecar %q's service.port %s collides with %s: each port exposed on the chart's Service must be unique." $name $port (get $seenPorts $port)) }}
{{- end }}
{{- $_ := set $seenPorts $port (printf "sidecar %q" $name) -}}
{{- $portName := include "helm-framework.sidecar.portName" . -}}
{{- if hasKey $seenPortNames $portName }}
{{- fail (printf "Sidecar %q's derived Service port name %q collides with %s: each port exposed on the chart's Service must have a unique name (sidecar port names are \"sc-<name>\" truncated to 15 characters)." $name $portName (get $seenPortNames $portName)) }}
{{- end }}
{{- $_ := set $seenPortNames $portName (printf "sidecar %q" $name) -}}
{{- end }}
{{- end }}

{{- $seenExtraServiceNames := dict -}}
{{- range $index, $svc := .Values.extraServices }}
{{- if $svc.enabled }}
{{- $svcName := $svc.name -}}
{{- if not $svcName }}
{{- fail (printf "extraServices[%d] has no name: extraServices entries require a name (used to derive the Service resource name \"<fullname>-<name>\")." $index) }}
{{- end }}
{{- if hasKey $seenExtraServiceNames $svcName }}
{{- fail (printf "Duplicate extraServices name %q: extraServices names must be unique (used to derive the Service resource name)." $svcName) }}
{{- end }}
{{- $_ := set $seenExtraServiceNames $svcName true -}}
{{- if and (($svc.sessionAffinityConfig).clientIPTimeoutSeconds) (ne ($svc.sessionAffinity | default "") "ClientIP") }}
{{- fail (printf "extraServices[%d] (%s): sessionAffinityConfig.clientIPTimeoutSeconds is set but sessionAffinity is not \"ClientIP\": the timeout has no effect without ClientIP affinity." $index $svcName) }}
{{- end }}
{{- $seenSvcPorts := dict -}}
{{- $seenSvcPortNames := dict -}}
{{- $multiPort := gt (len ($svc.ports | default list)) 1 -}}
{{- range $portIndex, $p := $svc.ports }}
{{- if not $p.port }}
{{- fail (printf "extraServices[%d] (%s) ports[%d] has no port: set the port the Service should expose." $index $svcName $portIndex) }}
{{- end }}
{{- if and $multiPort (not $p.name) }}
{{- fail (printf "extraServices[%d] (%s) ports[%d] has no name: a Service exposing more than one port must name every port." $index $svcName $portIndex) }}
{{- end }}
{{- with $p.name }}
{{- if hasKey $seenSvcPortNames . }}
{{- fail (printf "extraServices[%d] (%s) ports[%d]: duplicate port name %q — port names must be unique within a Service." $index $svcName $portIndex .) }}
{{- end }}
{{- $_ := set $seenSvcPortNames . true -}}
{{- end }}
{{- $port := toString $p.port -}}
{{- if hasKey $seenSvcPorts $port }}
{{- fail (printf "extraServices[%d] (%s) ports[%d]: duplicate port %s — ports must be unique within a Service." $index $svcName $portIndex $port) }}
{{- end }}
{{- $_ := set $seenSvcPorts $port true -}}
{{- end }}
{{- end }}
{{- end }}

Expand Down
45 changes: 44 additions & 1 deletion helm/helm-framework/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,12 @@ hostAliases: []
# `sessionAffinityConfig.clientIPTimeoutSeconds` (Kubernetes defaults to
# 10800s/3h when ClientIP affinity is set without a timeout).
# `externalTrafficPolicy` controls client-IP preservation for
# `NodePort`/`LoadBalancer` service types.
# `NodePort`/`LoadBalancer` service types. `extraPorts` exposes additional
# ports of the *main* container on this same Service (sidecar ports come from
# `sidecars[].service.port` instead); each entry needs a unique `name` and
# `port`, and a matching `containerPort` is declared on the main container
# whenever `targetPort` resolves to a number. Every port on the Service — main,
# `extraPorts`, and sidecars — must have a unique name and number.
service:
type: ClusterIP
port: 80
Expand All @@ -189,6 +194,44 @@ service:
sessionAffinityConfig:
clientIPTimeoutSeconds: 0
externalTrafficPolicy: ""
extraPorts: []
# - name: grpc
# port: 9090
# targetPort: 9090 # optional, defaults to `port`; a string is treated
# # as a container port name declared elsewhere
# protocol: TCP # optional, defaults to TCP
# appProtocol: kubernetes.io/h2c # optional
# nodePort: 30090 # optional, NodePort/LoadBalancer only

# -- Additional Services in front of the same pods (list; one entry per
# Service). Each entry renders `<fullname>-<name>` with this chart's standard
# selector, so it always targets the same workload as the main Service — use it
# for a headless Service for pod-level discovery, or a second Service with
# different annotations or type. Omitting `ports` mirrors the main Service's
# ports (main container + `service.extraPorts` + sidecars); set `ports`
# explicitly to expose a different subset. A `name` is required on every port
# once an entry exposes more than one.
extraServices: []
# - name: headless
# enabled: true
# type: ClusterIP
# clusterIP: None # headless
# publishNotReadyAddresses: true # advertise pods before they're Ready
# annotations: {}
# labels: {}
# # ports omitted -> same ports as the main Service
# - name: internal
# enabled: true
# type: LoadBalancer
# annotations:
# service.beta.kubernetes.io/azure-load-balancer-internal: "true"
# externalTrafficPolicy: Local
# sessionAffinity: ClientIP
# sessionAffinityConfig:
# clientIPTimeoutSeconds: 3600
# ports:
# - port: 443
# targetPort: http # container port name or number

# -- Kubernetes Ingress configuration.
ingress:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ is **not** in this list is not read by the library, full stop.
- `envVars`
- `envVarsFromSecret`
- `externalSecrets`
- `extraServices`
- `forceReload`
- `fullnameOverride`
- `healthChecks`
Expand Down Expand Up @@ -86,6 +87,7 @@ not for authorizing deletion.
- `envVars`
- `envVarsFromSecret`
- `externalSecrets`
- `extraServices`
- `forceReload`
- `fullnameOverride`
- `healthChecks`
Expand Down Expand Up @@ -205,6 +207,7 @@ not for authorizing deletion.
- `service`
- `service.annotations`
- `service.externalTrafficPolicy`
- `service.extraPorts`
- `service.port`
- `service.sessionAffinity`
- `service.sessionAffinityConfig`
Expand Down
Loading
Loading