(feature): improved and more standard helm chart

This commit is contained in:
Maksym Sadovnychyy 2026-03-28 22:27:25 +01:00
parent 05821bdea5
commit 76e0883595
8 changed files with 199 additions and 167 deletions

View File

@ -1,39 +1,37 @@
Thank you for installing **{{ .Chart.Name }}**!
Thank you for installing **{{ .Chart.Name }}**.
This chart deploys the MaksIT CertsUI tool for automated Let's Encrypt HTTPS certificate renewal.
Release: {{ .Release.Name }} / namespace {{ .Release.Namespace }}
Services use ClusterIP; expose via ingress, gateway, or kubectl port-forward.
------------------------------------------------------------
## Components
- **Server**: Handles certificate requests and renewal logic.
- **Client**: Web UI for managing and viewing certificate status.
- **Reverse Proxy**: Exposes the UI and API endpoints.
- server: {{ include "certs-ui.fullname" . }}-server:{{ .Values.components.server.service.port }}
- client: {{ include "certs-ui.fullname" . }}-client:{{ .Values.components.client.service.port }}
- reverseproxy: {{ include "certs-ui.fullname" . }}-reverseproxy:{{ .Values.components.reverseproxy.service.port }}
Port-forward API example:
kubectl port-forward svc/{{ include "certs-ui.fullname" . }}-server {{ .Values.components.server.service.port }}:{{ .Values.components.server.service.port }} -n {{ .Release.Namespace }}
------------------------------------------------------------
## Configuration
## Images
- **Secrets**:
The server uses a Kubernetes Secret (`appsecrets.json`) for sensitive data.
Image tag: `components.*.image.tag`, then `global.image.tag`, then Chart `appVersion`. Change tag and run `helm upgrade` to roll out.
- **ConfigMap**:
The server uses a ConfigMap (`appsettings.json`) for application settings.
`pullPolicy: Always` helps with a moving tag (e.g. latest); pinned tags often use `IfNotPresent`.
- **Persistence**:
PVCs are created for `/acme`, `/cache` and `/data` directories.
Pod annotation `rollme` tracks Helm release revision.
------------------------------------------------------------
## Config
Root keys `certsServerConfig`, `certsServerSecrets`, `certsClientRuntime` feed templated `configMapFile` / `secretsFile` content when `tpl: true`.
Use `existingConfigMap` / `existingSecret` to mount resources created outside the chart. With `keep: true`, existing objects are not replaced on upgrade if already present.
------------------------------------------------------------
## Uninstall
To remove all resources created by this chart:
```
helm uninstall {{ .Release.Name }} -n {{ .Release.Name }}
```
------------------------------------------------------------
## Notes
- Certificates are renewed automatically using Let's Encrypt.
- You can customize settings in `values.yaml` before installation.
- For advanced configuration, see the chart documentation and templates.
------------------------------------------------------------
helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }}

View File

@ -37,3 +37,22 @@ imagePullSecrets:
{{- end }}
{{- end }}
{{- end }}
{{- /* image tag: component, global.image.tag, Chart.AppVersion */ -}}
{{- define "certs-ui.component.imageTag" -}}
{{- $root := .root }}
{{- $comp := .comp }}
{{- $g := default dict $root.Values.global.image }}
{{- $comp.image.tag | default $g.tag | default $root.Chart.AppVersion }}
{{- end }}
{{- define "certs-ui.podLabels" -}}
{{- $root := .root }}
{{- $compName := .component }}
{{- $imageTag := .imageTag }}
app.kubernetes.io/name: {{ include "certs-ui.name" $root }}
app.kubernetes.io/instance: {{ $root.Release.Name }}
app.kubernetes.io/version: {{ $imageTag | quote }}
helm.sh/chart: {{ include "certs-ui.chart" $root }}
app.kubernetes.io/component: {{ $compName }}
{{- end }}

View File

@ -1,27 +1,37 @@
{{- $root := . -}}
{{- range $compName, $comp := .Values.components }}
{{- if $comp.configMapFile }}
{{- $cf := $comp.configMapFile -}}
{{- $cmName := printf "%s-%s-configmap" (include "certs-ui.fullname" $root) $compName -}}
{{- $existing := lookup "v1" "ConfigMap" $root.Release.Namespace $cmName -}}
{{- $cf := $comp.configMapFile }}
{{- if ne ($cf.existingConfigMap | default "") "" }}
{{- else if not $cf.key }}
{{- else if not (hasKey $cf "content") }}
{{- fail (printf "components.%s.configMapFile.content is required when configMapFile.key is set (or set existingConfigMap)" $compName) }}
{{- else }}
{{- $cmName := printf "%s-%s-configmap" (include "certs-ui.fullname" $root) $compName }}
{{- $existing := lookup "v1" "ConfigMap" $root.Release.Namespace $cmName }}
{{- if and $cf.keep $existing }}
{{/* keep=true and ConfigMap exists -> render nothing */}}
{{- else }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ $cmName }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "certs-ui.labels" $root | nindent 4 }}
app.kubernetes.io/component: {{ $compName }}
{{- if $cf.keep }}
annotations:
"helm.sh/resource-policy": keep
helm.sh/resource-policy: keep
{{- end }}
data:
{{ $cf.key }}: |
{{ $cf.content | indent 4 }}
{{- if default false $cf.tpl }}
{{ tpl ($cf.content | toString) $root | nindent 4 }}
{{- else }}
{{ $cf.content | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -1,17 +1,28 @@
{{- $roll := ((.Values.rollme | default (now | unixEpoch)) | toString) -}}
{{- $root := . -}}
{{- range $compName, $comp := .Values.components }}
{{- $imageTag := include "certs-ui.component.imageTag" (dict "root" $root "comp" $comp) }}
{{- $cf := default dict $comp.configMapFile }}
{{- $sf := default dict $comp.secretsFile }}
{{- $cmName := ternary $cf.existingConfigMap (printf "%s-%s-configmap" (include "certs-ui.fullname" $root) $compName) (ne ($cf.existingConfigMap | default "") "") }}
{{- $secretName := ternary $sf.existingSecret (printf "%s-%s-secrets" (include "certs-ui.fullname" $root) $compName) (ne ($sf.existingSecret | default "") "") }}
{{- $hasCm := or (ne ($cf.existingConfigMap | default "") "") (and $cf.key (hasKey $cf "content")) }}
{{- $hasSecret := or (ne ($sf.existingSecret | default "") "") (and $sf.key (hasKey $sf "content")) }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "certs-ui.labels" $root | nindent 4 }}
app.kubernetes.io/component: {{ $compName }}
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/instance: {{ $root.Release.Name }}
@ -20,15 +31,14 @@ spec:
template:
metadata:
labels:
{{- include "certs-ui.labels" $root | nindent 8 }}
app.kubernetes.io/component: {{ $compName }}
{{ include "certs-ui.podLabels" (dict "root" $root "component" $compName "imageTag" $imageTag) | indent 8 }}
annotations:
rollme: "{{$roll}}"
rollme: {{ $root.Release.Revision | quote }}
spec:
{{- include "certs-ui.imagePullSecrets" $root | nindent 6 }}
containers:
- name: {{ $compName }}
image: "{{ $comp.image.registry }}/{{ $comp.image.repository }}:{{ $.Chart.AppVersion }}"
image: "{{ $comp.image.registry }}/{{ $comp.image.repository }}:{{ $imageTag }}"
imagePullPolicy: {{ default "IfNotPresent" $comp.image.pullPolicy }}
{{ $svc := default dict $comp.service }}
{{ $tgt := default 8080 $svc.targetPort }}
@ -37,8 +47,6 @@ spec:
containerPort: {{ $tgt }}
{{- if $comp.env }}
env:
- name: ROLLOUT_TOKEN
value: "{{$roll}}"
{{- range $comp.env }}
- name: {{ .name }}
value: {{ .value | quote }}
@ -47,25 +55,26 @@ spec:
{{- $p := default dict $comp.persistence -}}
{{- $vols := default (list) $p.volumes -}}
{{- $hasVols := gt (len $vols) 0 -}}
{{- $hasSecret := (hasKey $comp "secretsFile") -}}
{{- if or $hasVols $hasSecret $comp.configMapFile }}
{{- if or $hasVols $hasSecret $hasCm }}
volumeMounts:
{{- range $vol := $vols }}
- name: {{ $compName }}-{{ $vol.name }}
mountPath: {{ $vol.mountPath }}
{{- end }}
{{- if $comp.secretsFile }}
{{- if $hasSecret }}
- name: {{ $compName }}-secrets
mountPath: {{ $comp.secretsFile.mountPath }}
subPath: {{ base $comp.secretsFile.mountPath }}
mountPath: {{ $sf.mountPath }}
subPath: {{ base $sf.mountPath }}
readOnly: true
{{- end }}
{{- if $comp.configMapFile }}
{{- if $hasCm }}
- name: {{ $compName }}-configmap
mountPath: {{ $comp.configMapFile.mountPath }}
subPath: {{ base $comp.configMapFile.mountPath }}
mountPath: {{ $cf.mountPath }}
subPath: {{ base $cf.mountPath }}
readOnly: true
{{- end }}
{{- end }}
{{- if or $hasVols $hasSecret $comp.configMapFile }}
{{- if or $hasVols $hasSecret $hasCm }}
volumes:
{{- range $vol := $vols }}
- name: {{ $compName }}-{{ $vol.name }}
@ -78,21 +87,21 @@ spec:
emptyDir: {}
{{- end }}
{{- end }}
{{- if $comp.secretsFile }}
{{- if $hasSecret }}
- name: {{ $compName }}-secrets
secret:
secretName: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-secrets
secretName: {{ $secretName }}
items:
- key: {{ $comp.secretsFile.key }}
path: {{ base $comp.secretsFile.mountPath }}
- key: {{ $sf.key }}
path: {{ base $sf.mountPath }}
{{- end }}
{{- if $comp.configMapFile }}
{{- if $hasCm }}
- name: {{ $compName }}-configmap
configMap:
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-configmap
name: {{ $cmName }}
items:
- key: {{ $comp.configMapFile.key }}
path: {{ base $comp.configMapFile.mountPath }}
- key: {{ $cf.key }}
path: {{ base $cf.mountPath }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -9,6 +9,7 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-{{ $vol.name }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "certs-ui.labels" $root | nindent 4 }}
app.kubernetes.io/component: {{ $compName }}

View File

@ -1,28 +1,39 @@
{{- $root := . -}}
{{- range $compName, $comp := .Values.components }}
{{- if $comp.secretsFile }}
{{- $sf := $comp.secretsFile -}}
{{- $secretName := printf "%s-%s-secrets" (include "certs-ui.fullname" $root) $compName -}}
{{- $existing := lookup "v1" "Secret" $root.Release.Namespace $secretName -}}
{{- $sf := $comp.secretsFile }}
{{- if ne ($sf.existingSecret | default "") "" }}
{{- else if not $sf.key }}
{{- fail (printf "components.%s.secretsFile.key is required unless secretsFile.existingSecret is set" $compName) }}
{{- else if not (hasKey $sf "content") }}
{{- fail (printf "components.%s.secretsFile.content is required unless secretsFile.existingSecret is set" $compName) }}
{{- else }}
{{- $secretName := printf "%s-%s-secrets" (include "certs-ui.fullname" $root) $compName }}
{{- $existing := lookup "v1" "Secret" $root.Release.Namespace $secretName }}
{{- if and $sf.keep $existing }}
{{/* keep=true and Secret exists -> render nothing */}}
{{- else }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ $secretName }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "certs-ui.labels" $root | nindent 4 }}
app.kubernetes.io/component: {{ $compName }}
{{- if $sf.keep }}
annotations:
"helm.sh/resource-policy": keep
helm.sh/resource-policy: keep
{{- end }}
type: Opaque
stringData:
{{ $sf.key }}: |
{{ $sf.content | indent 4 }}
{{- if default false $sf.tpl }}
{{ tpl ($sf.content | toString) $root | nindent 4 }}
{{- else }}
{{ $sf.content | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -2,72 +2,25 @@
{{- range $compName, $comp := .Values.components }}
{{- $svc := default dict $comp.service }}
{{- if and $svc $svc.enabled }}
{{- $stype := default "ClusterIP" $svc.type }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "certs-ui.labels" $root | nindent 4 }}
app.kubernetes.io/component: {{ $compName }}
{{- if $svc.labels }}
{{ toYaml $svc.labels | nindent 4 }}
{{- end }}
{{- if $svc.annotations }}
annotations:
{{ toYaml $svc.annotations | nindent 4 }}
{{- end }}
spec:
type: {{ $stype }}
{{- if $svc.clusterIP }}
clusterIP: {{ $svc.clusterIP }}
{{- end }}
{{- if $svc.loadBalancerClass }}
loadBalancerClass: {{ $svc.loadBalancerClass }}
{{- end }}
{{- if and (or (eq $stype "LoadBalancer") (eq $stype "NodePort")) ($svc.allocateLoadBalancerNodePorts | default nil) }}
allocateLoadBalancerNodePorts: {{ $svc.allocateLoadBalancerNodePorts }}
{{- end }}
{{- if and (eq $stype "LoadBalancer") $svc.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{ toYaml $svc.loadBalancerSourceRanges | nindent 4 }}
{{- end }}
{{- if and (eq $stype "LoadBalancer") $svc.ipFamilies }}
ipFamilies:
{{ toYaml $svc.ipFamilies | nindent 4 }}
{{- end }}
{{- if and (eq $stype "LoadBalancer") $svc.ipFamilyPolicy }}
ipFamilyPolicy: {{ $svc.ipFamilyPolicy }}
{{- end }}
{{- if and (eq $stype "LoadBalancer") $svc.loadBalancerIP }}
loadBalancerIP: {{ $svc.loadBalancerIP }}
{{- end }}
type: {{ default "ClusterIP" $svc.type }}
ports:
- name: http
port: {{ default 80 $svc.port }}
targetPort: {{ default 80 $svc.targetPort }}
{{- if eq $stype "NodePort" }}
{{- if $svc.nodePort }}
nodePort: {{ $svc.nodePort }}
{{- end }}
{{- end }}
- port: {{ default 80 $svc.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/instance: {{ $root.Release.Name }}
app.kubernetes.io/name: {{ include "certs-ui.name" $root }}
app.kubernetes.io/component: {{ $compName }}
{{- if and (ne $stype "ClusterIP") $svc.externalTrafficPolicy }}
externalTrafficPolicy: {{ $svc.externalTrafficPolicy }}
{{- end }}
{{- if and (eq $stype "LoadBalancer") $svc.healthCheckNodePort }}
healthCheckNodePort: {{ $svc.healthCheckNodePort }}
{{- end }}
{{- if and (typeIs "string" $svc.sessionAffinity) $svc.sessionAffinity }}
sessionAffinity: {{ $svc.sessionAffinity }}
{{- if and (eq $svc.sessionAffinity "ClientIP") (typeIs "map" $svc.sessionAffinityConfig) }}
sessionAffinityConfig:
{{ toYaml $svc.sessionAffinityConfig | nindent 4 }}
{{- end }}
{{- end }}
{{ end }}
{{ end }}

View File

@ -1,14 +1,51 @@
global:
imagePullSecrets: [] # Keep empty
# imagePullSecrets:
# - name: cr-maksit-pull
imagePullSecrets: []
image:
tag: "" # used if component image.tag is empty; else Chart appVersion
nameOverride: ""
fullnameOverride: ""
# Server ConfigMap (appsettings.json); referenced from components.server.configMapFile when tpl: true
certsServerConfig:
allowedHosts: "*"
logging:
default: Information
microsoftAspNetCore: Warning
configuration:
auth:
issuer: ""
audience: ""
expiration: 15
refreshExpiration: 180
agent:
agentHostname: ""
agentPort: 5000
serviceToReload: haproxy
production: "https://acme-v02.api.letsencrypt.org/directory"
staging: "https://acme-staging-v02.api.letsencrypt.org/directory"
acmeFolder: /acme
cacheFolder: /cache
dataFolder: /data
settingsFile: /data/settings.json
# Server Secret (appsecrets.json); referenced from components.server.secretsFile when tpl: true
certsServerSecrets:
authSecret: changeme-generate-a-long-random-string
authPepper: ""
agentKey: ""
# Client ConfigMap (config.js); referenced when tpl: true
certsClientRuntime:
apiUrl: "http://certs-ui.example.com/api"
components:
server:
image:
registry: cr.maks-it.com
repository: certs-ui/server
pullPolicy: Always
tag: ""
pullPolicy: IfNotPresent
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
@ -46,59 +83,63 @@ components:
secretsFile:
key: appsecrets.json
mountPath: /secrets/appsecrets.json
tpl: true
keep: true
existingSecret: ""
content: |
{
"Configuration": {
"Auth": {
"Secret": "",
"Pepper": ""
"Secret": {{ .Values.certsServerSecrets.authSecret | toJson }},
"Pepper": {{ .Values.certsServerSecrets.authPepper | toJson }}
},
"Agent": {
"AgentKey": ""
},
"AgentKey": {{ .Values.certsServerSecrets.agentKey | toJson }}
}
}
}
keep: true
configMapFile:
key: appsettings.json
mountPath: /configMap/appsettings.json
tpl: true
keep: true
existingConfigMap: ""
content: |
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": {{ .Values.certsServerConfig.logging.default | toJson }},
"Microsoft.AspNetCore": {{ .Values.certsServerConfig.logging.microsoftAspNetCore | toJson }}
}
},
"AllowedHosts": {{ .Values.certsServerConfig.allowedHosts | toJson }},
"Configuration": {
"Auth": {
"Issuer": "",
"Audience": "",
"Expiration": 15, // Access token lifetime in minutes (default: 15 minutes)
"RefreshExpiration": 180, // Refresh token lifetime in days (default: 180 days)
"Issuer": {{ .Values.certsServerConfig.configuration.auth.issuer | toJson }},
"Audience": {{ .Values.certsServerConfig.configuration.auth.audience | toJson }},
"Expiration": {{ .Values.certsServerConfig.configuration.auth.expiration }},
"RefreshExpiration": {{ .Values.certsServerConfig.configuration.auth.refreshExpiration }}
},
"Agent": {
"AgentHostname": "http://websrv0001.corp.maks-it.com",
"AgentPort": 5000,
"ServiceToReload": "haproxy"
"AgentHostname": {{ .Values.certsServerConfig.configuration.agent.agentHostname | toJson }},
"AgentPort": {{ .Values.certsServerConfig.configuration.agent.agentPort }},
"ServiceToReload": {{ .Values.certsServerConfig.configuration.agent.serviceToReload | toJson }}
},
"Production": "https://acme-v02.api.letsencrypt.org/directory",
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
"CacheFolder": "/cache",
"AcmeFolder": "/acme",
"DataFolder": "/data",
"SettingsFile": "/data/settings.json",
"Production": {{ .Values.certsServerConfig.configuration.production | toJson }},
"Staging": {{ .Values.certsServerConfig.configuration.staging | toJson }},
"CacheFolder": {{ .Values.certsServerConfig.configuration.cacheFolder | toJson }},
"AcmeFolder": {{ .Values.certsServerConfig.configuration.acmeFolder | toJson }},
"DataFolder": {{ .Values.certsServerConfig.configuration.dataFolder | toJson }},
"SettingsFile": {{ .Values.certsServerConfig.configuration.settingsFile | toJson }}
}
}
keep: true
client:
image:
registry: cr.maks-it.com
repository: certs-ui/client
pullPolicy: Always
tag: ""
pullPolicy: IfNotPresent
service:
enabled: true
type: ClusterIP
@ -107,17 +148,20 @@ components:
configMapFile:
key: config.js
mountPath: /app/dist/config.js
tpl: true
keep: true
existingConfigMap: ""
content: |
window.RUNTIME_CONFIG = {
API_URL: "http://<your-server-hostname>/api"
API_URL: {{ .Values.certsClientRuntime.apiUrl | toJson }}
};
keep: true
reverseproxy:
image:
registry: cr.maks-it.com
repository: certs-ui/reverseproxy
pullPolicy: Always
tag: ""
pullPolicy: IfNotPresent
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
@ -128,16 +172,3 @@ components:
type: ClusterIP
port: 8080
targetPort: 8080
# type: LoadBalancer
# port: 8080
# targetPort: 8080
# loadBalancerIP: "172.16.0.5"
# annotations:
# lbipam.cilium.io/ips: "172.16.0.5"
# labels:
# export: "bgp"
# externalTrafficPolicy: Local
# sessionAffinity: ClientIP
# sessionAffinityConfig:
# clientIP:
# timeoutSeconds: 10800