diff --git a/helm/helm-framework/README.md b/helm/helm-framework/README.md index 4bb4e5c..6e9401c 100644 --- a/helm/helm-framework/README.md +++ b/helm/helm-framework/README.md @@ -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 `-` 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. | @@ -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 ). | | 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. | diff --git a/helm/helm-framework/templates/_deployment-global.tpl b/helm/helm-framework/templates/_deployment-global.tpl index d9368f3..b112837 100644 --- a/helm/helm-framework/templates/_deployment-global.tpl +++ b/helm/helm-framework/templates/_deployment-global.tpl @@ -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" .) diff --git a/helm/helm-framework/templates/_deployment.yaml b/helm/helm-framework/templates/_deployment.yaml index 9e46815..4cbaeae 100644 --- a/helm/helm-framework/templates/_deployment.yaml +++ b/helm/helm-framework/templates/_deployment.yaml @@ -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: diff --git a/helm/helm-framework/templates/_service.yaml b/helm/helm-framework/templates/_service.yaml index 3e5da6a..0d67856 100644 --- a/helm/helm-framework/templates/_service.yaml +++ b/helm/helm-framework/templates/_service.yaml @@ -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 @@ -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" ). +*/}} +{{- 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 }} diff --git a/helm/helm-framework/templates/_values-validation.tpl b/helm/helm-framework/templates/_values-validation.tpl index 25e0a06..f4547af 100644 --- a/helm/helm-framework/templates/_values-validation.tpl +++ b/helm/helm-framework/templates/_values-validation.tpl @@ -1,6 +1,7 @@ {{/* 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. */}} @@ -8,6 +9,24 @@ virtualService, authorizationPolicy, and podDisruptionBudget. {{- $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 -}} @@ -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-\" 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 \"-\")." $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 }} diff --git a/helm/helm-framework/values.yaml b/helm/helm-framework/values.yaml index 8bc903a..c622747 100644 --- a/helm/helm-framework/values.yaml +++ b/helm/helm-framework/values.yaml @@ -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 @@ -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 `-` 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: diff --git a/plugins/helm-framework/skills/helm-framework-migration/resources/values-contract.md b/plugins/helm-framework/skills/helm-framework-migration/resources/values-contract.md index 587eceb..da31795 100644 --- a/plugins/helm-framework/skills/helm-framework-migration/resources/values-contract.md +++ b/plugins/helm-framework/skills/helm-framework-migration/resources/values-contract.md @@ -21,6 +21,7 @@ is **not** in this list is not read by the library, full stop. - `envVars` - `envVarsFromSecret` - `externalSecrets` +- `extraServices` - `forceReload` - `fullnameOverride` - `healthChecks` @@ -86,6 +87,7 @@ not for authorizing deletion. - `envVars` - `envVarsFromSecret` - `externalSecrets` +- `extraServices` - `forceReload` - `fullnameOverride` - `healthChecks` @@ -205,6 +207,7 @@ not for authorizing deletion. - `service` - `service.annotations` - `service.externalTrafficPolicy` +- `service.extraPorts` - `service.port` - `service.sessionAffinity` - `service.sessionAffinityConfig` diff --git a/plugins/helm-framework/skills/helm-framework/resources/values-reference.md b/plugins/helm-framework/skills/helm-framework/resources/values-reference.md index 8c04884..7b50c7d 100644 --- a/plugins/helm-framework/skills/helm-framework/resources/values-reference.md +++ b/plugins/helm-framework/skills/helm-framework/resources/values-reference.md @@ -14,6 +14,7 @@ Generated from `helm/helm-framework/README.md` (itself auto-generated by helm-do | 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 `-` 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. | @@ -40,7 +41,7 @@ Generated from `helm/helm-framework/README.md` (itself auto-generated by helm-do | 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 ). | | 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. |