From 76e0883595845c66bb7d7e26cefadac98a73e56e Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sat, 28 Mar 2026 22:27:25 +0100 Subject: [PATCH] (feature): improved and more standard helm chart --- src/helm/templates/NOTES.txt | 48 ++++--- src/helm/templates/_helpers.tpl | 19 +++ src/helm/templates/configmap-appsettings.yaml | 24 +++- src/helm/templates/deployments.yaml | 59 ++++---- src/helm/templates/pvc.yaml | 1 + src/helm/templates/secret-appsecrets.yaml | 23 +++- src/helm/templates/services.yaml | 63 ++------- src/helm/values.yaml | 129 +++++++++++------- 8 files changed, 199 insertions(+), 167 deletions(-) diff --git a/src/helm/templates/NOTES.txt b/src/helm/templates/NOTES.txt index a5978bd..8a6ae6b 100644 --- a/src/helm/templates/NOTES.txt +++ b/src/helm/templates/NOTES.txt @@ -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. - ------------------------------------------------------------- \ No newline at end of file + helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/src/helm/templates/_helpers.tpl b/src/helm/templates/_helpers.tpl index 3e0f862..ea84c4b 100644 --- a/src/helm/templates/_helpers.tpl +++ b/src/helm/templates/_helpers.tpl @@ -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 }} diff --git a/src/helm/templates/configmap-appsettings.yaml b/src/helm/templates/configmap-appsettings.yaml index 59ecf8c..0233bd7 100644 --- a/src/helm/templates/configmap-appsettings.yaml +++ b/src/helm/templates/configmap-appsettings.yaml @@ -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 }} -{{- end }} \ No newline at end of file diff --git a/src/helm/templates/deployments.yaml b/src/helm/templates/deployments.yaml index e204e9c..1476e83 100644 --- a/src/helm/templates/deployments.yaml +++ b/src/helm/templates/deployments.yaml @@ -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 }} diff --git a/src/helm/templates/pvc.yaml b/src/helm/templates/pvc.yaml index 233754d..cb4fd61 100644 --- a/src/helm/templates/pvc.yaml +++ b/src/helm/templates/pvc.yaml @@ -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 }} diff --git a/src/helm/templates/secret-appsecrets.yaml b/src/helm/templates/secret-appsecrets.yaml index 5712e50..329a9dc 100644 --- a/src/helm/templates/secret-appsecrets.yaml +++ b/src/helm/templates/secret-appsecrets.yaml @@ -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 }} diff --git a/src/helm/templates/services.yaml b/src/helm/templates/services.yaml index 1f005e9..c66ee75 100644 --- a/src/helm/templates/services.yaml +++ b/src/helm/templates/services.yaml @@ -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 }} +{{- end }} +{{- end }} diff --git a/src/helm/values.yaml b/src/helm/values.yaml index feeca9f..89bb7e8 100644 --- a/src/helm/values.yaml +++ b/src/helm/values.yaml @@ -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: | { - "Auth": { - "Secret": "", - "Pepper": "" - }, - "Agent": { - "AgentKey": "" - }, + "Configuration": { + "Auth": { + "Secret": {{ .Values.certsServerSecrets.authSecret | toJson }}, + "Pepper": {{ .Values.certsServerSecrets.authPepper | toJson }} + }, + "Agent": { + "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:///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 \ No newline at end of file