From 325132d48e9adbc188d0e6b80a5c0cbb41f0f89b Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 12:31:40 +0300 Subject: [PATCH 01/22] test(cozy-lib): add TLS certificate helper tests Add helm-unittest tests for the upcoming cozy-lib.tls.certificate helper. Tests cover: required fields, dnsNames, issuerRef, labels, default and custom duration/renewBefore, default and custom usages, secretName defaulting, and both ClusterIssuer and Issuer kinds. Signed-off-by: Arsolitt Signed-off-by: Arsolitt --- .../cozy-lib-tests/templates/tests/tls.yaml | 12 + .../tests/cozy-lib-tests/tests/tls_test.yaml | 225 ++++++++++++++++++ .../cozy-lib-tests/tests/tls_values.yaml | 12 + 3 files changed, 249 insertions(+) create mode 100644 packages/tests/cozy-lib-tests/templates/tests/tls.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_test.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_values.yaml diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml new file mode 100644 index 00000000..86afae87 --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml @@ -0,0 +1,12 @@ +{{- if .Values.tls }} +{{- include "cozy-lib.tls.certificate" (dict + "Release" .Release + "name" .Values.tls.name + "secretName" (default .Values.tls.name .Values.tls.secretName) + "dnsNames" .Values.tls.dnsNames + "issuerRef" .Values.tls.issuerRef + "duration" (.Values.tls.duration | default "") + "renewBefore" (.Values.tls.renewBefore | default "") + "usages" (.Values.tls.usages | default list) +) }} +{{- end }} diff --git a/packages/tests/cozy-lib-tests/tests/tls_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_test.yaml new file mode 100644 index 00000000..cc5e771d --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_test.yaml @@ -0,0 +1,225 @@ +suite: TLS certificate helper + +templates: + - templates/tests/tls.yaml + +tests: + # Certificate CR renders with all required fields + - it: renders a Certificate CR with all required fields + values: + - tls_values.yaml + release: + name: test-release + namespace: tenant-test + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Certificate + - isAPIVersion: + of: cert-manager.io/v1 + - equal: + path: metadata.name + value: my-redis-tls + - equal: + path: spec.secretName + value: my-redis-tls-secret + - equal: + path: spec.privateKey.algorithm + value: RSA + - equal: + path: spec.privateKey.size + value: 2048 + + # dnsNames are correctly set + - it: sets dnsNames correctly + values: + - tls_values.yaml + release: + name: test-release + namespace: tenant-test + asserts: + - equal: + path: spec.dnsNames + value: + - redis-svc + - redis-svc.tenant-test.svc + + # issuerRef is correctly set + - it: sets issuerRef correctly + values: + - tls_values.yaml + release: + name: test-release + namespace: tenant-test + asserts: + - equal: + path: spec.issuerRef.name + value: selfsigned-cluster-issuer + - equal: + path: spec.issuerRef.kind + value: ClusterIssuer + - equal: + path: spec.issuerRef.group + value: cert-manager.io + + # Labels include release info + - it: includes standard labels + values: + - tls_values.yaml + release: + name: test-release + namespace: tenant-test + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: test-release + - equal: + path: metadata.labels["app.kubernetes.io/managed-by"] + value: Helm + + # Default duration and renewBefore are used when not provided + - it: uses default duration and renewBefore + values: + - tls_values.yaml + release: + name: test-release + namespace: tenant-test + asserts: + - equal: + path: spec.duration + value: 8760h + - equal: + path: spec.renewBefore + value: 720h + + # Custom duration and renewBefore work when provided + - it: uses custom duration and renewBefore when provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: custom-tls + secretName: custom-tls-secret + dnsNames: + - svc.example + issuerRef: + name: my-issuer + kind: Issuer + duration: 43800h + renewBefore: 360h + asserts: + - equal: + path: spec.duration + value: 43800h + - equal: + path: spec.renewBefore + value: 360h + + # Default usages is server auth + - it: uses server auth as default usage + values: + - tls_values.yaml + release: + name: test-release + namespace: tenant-test + asserts: + - equal: + path: spec.usages + value: + - server auth + + # Custom usages work when provided + - it: uses custom usages when provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: client-tls + secretName: client-tls-secret + dnsNames: + - svc.example + issuerRef: + name: my-issuer + kind: ClusterIssuer + usages: + - server auth + - client auth + asserts: + - equal: + path: spec.usages + value: + - server auth + - client auth + + # secretName defaults to name when not provided + - it: uses name as secretName when secretName not provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: my-tls-cert + dnsNames: + - svc.example + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + asserts: + - equal: + path: metadata.name + value: my-tls-cert + - equal: + path: spec.secretName + value: my-tls-cert + + # Issuer kind defaults to ClusterIssuer + - it: defaults issuerRef kind to ClusterIssuer + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: test-tls + dnsNames: + - svc.example + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + asserts: + - equal: + path: spec.issuerRef.kind + value: ClusterIssuer + + # Namespace-scoped Issuer also works + - it: supports namespace-scoped Issuer + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: test-tls + dnsNames: + - svc.example + issuerRef: + name: my-namespace-issuer + kind: Issuer + asserts: + - equal: + path: spec.issuerRef.kind + value: Issuer + - equal: + path: spec.issuerRef.name + value: my-namespace-issuer diff --git a/packages/tests/cozy-lib-tests/tests/tls_values.yaml b/packages/tests/cozy-lib-tests/tests/tls_values.yaml new file mode 100644 index 00000000..6673dda1 --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_values.yaml @@ -0,0 +1,12 @@ +tls: + name: my-redis-tls + secretName: my-redis-tls-secret + dnsNames: + - redis-svc + - redis-svc.tenant-test.svc + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + +_cluster: {} +_namespace: {} From cb932d17f4a22f7751614f96f55e58ac8ec59352 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 12:32:14 +0300 Subject: [PATCH 02/22] feat(cozy-lib): add TLS certificate helper Add cozy-lib.tls.certificate template that renders a cert-manager Certificate CR for services needing TLS. This provides a shared foundation so each chart does not duplicate cert-manager boilerplate. Also adds cozy-lib.tls.secretName helper for resolving a TLS secret name from user input or generating one from the release name. Signed-off-by: Arsolitt Signed-off-by: Arsolitt --- packages/library/cozy-lib/templates/_tls.tpl | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 packages/library/cozy-lib/templates/_tls.tpl diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl new file mode 100644 index 00000000..c40c637f --- /dev/null +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -0,0 +1,79 @@ +{{/* +TLS certificate helpers for cert-manager integration. +These helpers render cert-manager Certificate CRs for services that need TLS. +*/}} + +{{/* +Render a cert-manager Certificate CR. + +Usage: + {{- include "cozy-lib.tls.certificate" (dict + "Release" .Release + "name" "my-redis-tls" + "secretName" "my-redis-tls-secret" + "dnsNames" (list "redis-svc" "redis-svc.ns.svc") + "issuerRef" (dict "name" "selfsigned-cluster-issuer" "kind" "ClusterIssuer") + ) }} + +Parameters: + - Release (required) - Helm release object, used for labels + - name (required) - Certificate CR metadata.name + - secretName (required) - TLS secret name to create (can be same as name) + - dnsNames (required) - list of DNS SANs + - issuerRef (required) - dict with "name" and "kind" (e.g. ClusterIssuer, Issuer) + - duration (optional) - certificate duration, defaults to 8760h (1 year) + - renewBefore(optional) - renewal window, defaults to 720h (30 days) + - usages (optional) - list of key usages, defaults to ["server auth"] +*/}} +{{- define "cozy-lib.tls.certificate" -}} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .name }} + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + secretName: {{ .secretName }} + duration: {{ .duration | default "8760h" }} + renewBefore: {{ .renewBefore | default "720h" }} + privateKey: + algorithm: RSA + size: 2048 + usages: + {{- if .usages }} + {{- range .usages }} + - {{ . }} + {{- end }} + {{- else }} + - server auth + {{- end }} + dnsNames: + {{- range .dnsNames }} + - {{ . }} + {{- end }} + issuerRef: + name: {{ .issuerRef.name }} + kind: {{ .issuerRef.kind }} + group: cert-manager.io +{{- end }} + +{{/* +Return a TLS secret name — either the user-provided value or a generated default. +Useful in conditionals to decide whether to mount a TLS secret or render a Certificate CR. + +Usage: + {{- $tlsSecret := include "cozy-lib.tls.secretName" (dict "Release" .Release "suffix" "tls" "secretName" .Values.tls.secretName) }} + +Parameters: + - Release (required) - Helm release object + - suffix (required) - suffix for generated name (e.g. "tls", "server-tls") + - secretName (optional) - user-provided secret name; if set, returned as-is +*/}} +{{- define "cozy-lib.tls.secretName" -}} +{{- if .secretName -}} +{{- .secretName -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name .suffix -}} +{{- end -}} +{{- end }} From 2e1fc384051d357163d7ef5332cb4e93a7802766 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 13:37:14 +0300 Subject: [PATCH 03/22] fix(cozy-lib-tls): fix empty string defeating duration/renewBefore defaults and add missing tests Signed-off-by: Arsolitt --- packages/library/cozy-lib/templates/_tls.tpl | 1 + .../templates/tests/tls-secretname.yaml | 7 ++ .../cozy-lib-tests/templates/tests/tls.yaml | 14 +-- .../tests/tls_secretname_test.yaml | 32 +++++++ .../tests/cozy-lib-tests/tests/tls_test.yaml | 88 +++++++++++++++++++ 5 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 packages/tests/cozy-lib-tests/templates/tests/tls-secretname.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl index c40c637f..abfcca9b 100644 --- a/packages/library/cozy-lib/templates/_tls.tpl +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -59,6 +59,7 @@ spec: {{- end }} {{/* +Convenience helper for chart consumers. Return a TLS secret name — either the user-provided value or a generated default. Useful in conditionals to decide whether to mount a TLS secret or render a Certificate CR. diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls-secretname.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls-secretname.yaml new file mode 100644 index 00000000..dc63eee9 --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls-secretname.yaml @@ -0,0 +1,7 @@ +{{- $secretName := include "cozy-lib.tls.secretName" (dict "Release" .Release "suffix" .Values.tlsSuffix "secretName" .Values.tlsSecretName) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-secretname +data: + secretName: {{ $secretName | quote }} diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml index 86afae87..d519beab 100644 --- a/packages/tests/cozy-lib-tests/templates/tests/tls.yaml +++ b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml @@ -1,12 +1,6 @@ {{- if .Values.tls }} -{{- include "cozy-lib.tls.certificate" (dict - "Release" .Release - "name" .Values.tls.name - "secretName" (default .Values.tls.name .Values.tls.secretName) - "dnsNames" .Values.tls.dnsNames - "issuerRef" .Values.tls.issuerRef - "duration" (.Values.tls.duration | default "") - "renewBefore" (.Values.tls.renewBefore | default "") - "usages" (.Values.tls.usages | default list) -) }} +{{- $certArgs := dict "Release" .Release "name" .Values.tls.name "secretName" (default .Values.tls.name .Values.tls.secretName) "dnsNames" .Values.tls.dnsNames "issuerRef" .Values.tls.issuerRef "usages" (.Values.tls.usages | default list) -}} +{{- if .Values.tls.duration }}{{- $_ := set $certArgs "duration" .Values.tls.duration }}{{- end -}} +{{- if .Values.tls.renewBefore }}{{- $_ := set $certArgs "renewBefore" .Values.tls.renewBefore }}{{- end -}} +{{- include "cozy-lib.tls.certificate" $certArgs -}} {{- end }} diff --git a/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml new file mode 100644 index 00000000..7125b976 --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml @@ -0,0 +1,32 @@ +suite: TLS secretName helper + +templates: + - templates/tests/tls-secretname.yaml + +tests: + - it: returns user-provided secretName when set + release: + name: my-app + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tlsSecretName: my-custom-tls-secret + tlsSuffix: tls + asserts: + - equal: + path: data.secretName + value: my-custom-tls-secret + + - it: generates default secretName from release name and suffix when secretName is omitted + release: + name: my-app + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tlsSuffix: tls + asserts: + - equal: + path: data.secretName + value: my-app-tls diff --git a/packages/tests/cozy-lib-tests/tests/tls_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_test.yaml index cc5e771d..7b40e9b2 100644 --- a/packages/tests/cozy-lib-tests/tests/tls_test.yaml +++ b/packages/tests/cozy-lib-tests/tests/tls_test.yaml @@ -223,3 +223,91 @@ tests: - equal: path: spec.issuerRef.name value: my-namespace-issuer + + # TLS disabled produces zero documents + - it: produces zero documents when tls is not set + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + asserts: + - hasDocuments: + count: 0 + + # TLS disabled when tls is explicitly null + - it: produces zero documents when tls is null + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: null + asserts: + - hasDocuments: + count: 0 + + # Empty dnsNames renders null in YAML + - it: renders null dnsNames when empty list provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: empty-dns-tls + dnsNames: [] + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.dnsNames + value: null + + # Missing issuerRef name renders null + - it: renders null issuerRef name when not provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: missing-issuer-tls + dnsNames: + - svc.example + issuerRef: + kind: ClusterIssuer + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.issuerRef.name + value: null + + # Missing issuerRef kind renders null + - it: renders null issuerRef kind when not provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: missing-kind-tls + dnsNames: + - svc.example + issuerRef: + name: my-issuer + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.issuerRef.kind + value: null From 2aa4863cdd9471c5c9111c4961be6a4c9bb6fe75 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 13:56:35 +0300 Subject: [PATCH 04/22] fix(cozy-lib-tls): fix ternary argument order and strengthen input validation Signed-off-by: Arsolitt --- packages/library/cozy-lib/templates/_tls.tpl | 21 ++++++-- .../cozy-lib-tests/templates/tests/tls.yaml | 4 +- .../tests/tls_secretname_test.yaml | 14 +++++ .../tests/cozy-lib-tests/tests/tls_test.yaml | 54 ++++++++++++------- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl index abfcca9b..6e1c17c6 100644 --- a/packages/library/cozy-lib/templates/_tls.tpl +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -21,11 +21,26 @@ Parameters: - secretName (required) - TLS secret name to create (can be same as name) - dnsNames (required) - list of DNS SANs - issuerRef (required) - dict with "name" and "kind" (e.g. ClusterIssuer, Issuer) - - duration (optional) - certificate duration, defaults to 8760h (1 year) + - duration (optional) - certificate duration, defaults to 8760h (1 year); empty strings are treated as unset - renewBefore(optional) - renewal window, defaults to 720h (30 days) - usages (optional) - list of key usages, defaults to ["server auth"] */}} {{- define "cozy-lib.tls.certificate" -}} +{{- if not .name -}} +{{- fail "ERROR: \"name\" is required for cozy-lib.tls.certificate. It sets the Certificate CR metadata.name." -}} +{{- end -}} +{{- if not .dnsNames -}} +{{- fail "ERROR: \"dnsNames\" is required for cozy-lib.tls.certificate. Provide at least one DNS name as a list." -}} +{{- end -}} +{{- if not .issuerRef -}} +{{- fail "ERROR: \"issuerRef\" is required for cozy-lib.tls.certificate. Provide a dict with \"name\" and \"kind\"." -}} +{{- end -}} +{{- if not .issuerRef.name -}} +{{- fail "ERROR: \"issuerRef.name\" is required for cozy-lib.tls.certificate. Specify the ClusterIssuer or Issuer name." -}} +{{- end -}} +{{- if not .issuerRef.kind -}} +{{- fail "ERROR: \"issuerRef.kind\" is required for cozy-lib.tls.certificate. Specify \"ClusterIssuer\" or \"Issuer\"." -}} +{{- end -}} apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -35,8 +50,8 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} spec: secretName: {{ .secretName }} - duration: {{ .duration | default "8760h" }} - renewBefore: {{ .renewBefore | default "720h" }} + duration: {{ ternary "8760h" .duration (empty .duration) }} + renewBefore: {{ ternary "720h" .renewBefore (empty .renewBefore) }} privateKey: algorithm: RSA size: 2048 diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml index d519beab..3d38d7dc 100644 --- a/packages/tests/cozy-lib-tests/templates/tests/tls.yaml +++ b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml @@ -1,6 +1,6 @@ {{- if .Values.tls }} {{- $certArgs := dict "Release" .Release "name" .Values.tls.name "secretName" (default .Values.tls.name .Values.tls.secretName) "dnsNames" .Values.tls.dnsNames "issuerRef" .Values.tls.issuerRef "usages" (.Values.tls.usages | default list) -}} -{{- if .Values.tls.duration }}{{- $_ := set $certArgs "duration" .Values.tls.duration }}{{- end -}} -{{- if .Values.tls.renewBefore }}{{- $_ := set $certArgs "renewBefore" .Values.tls.renewBefore }}{{- end -}} +{{- if and .Values.tls.duration (ne .Values.tls.duration "") }}{{- $_ := set $certArgs "duration" .Values.tls.duration }}{{- end -}} +{{- if and .Values.tls.renewBefore (ne .Values.tls.renewBefore "") }}{{- $_ := set $certArgs "renewBefore" .Values.tls.renewBefore }}{{- end -}} {{- include "cozy-lib.tls.certificate" $certArgs -}} {{- end }} diff --git a/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml index 7125b976..94386838 100644 --- a/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml +++ b/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml @@ -30,3 +30,17 @@ tests: - equal: path: data.secretName value: my-app-tls + + - it: generates default secretName when secretName is an empty string + release: + name: my-app + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tlsSecretName: "" + tlsSuffix: tls + asserts: + - equal: + path: data.secretName + value: my-app-tls diff --git a/packages/tests/cozy-lib-tests/tests/tls_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_test.yaml index 7b40e9b2..1fd18e6f 100644 --- a/packages/tests/cozy-lib-tests/tests/tls_test.yaml +++ b/packages/tests/cozy-lib-tests/tests/tls_test.yaml @@ -249,8 +249,8 @@ tests: - hasDocuments: count: 0 - # Empty dnsNames renders null in YAML - - it: renders null dnsNames when empty list provided + # Empty dnsNames triggers a validation error + - it: errors when dnsNames is empty release: name: test-release namespace: tenant-test @@ -264,14 +264,11 @@ tests: name: selfsigned-cluster-issuer kind: ClusterIssuer asserts: - - hasDocuments: - count: 1 - - equal: - path: spec.dnsNames - value: null + - failedTemplate: + errorMessage: 'ERROR: "dnsNames" is required for cozy-lib.tls.certificate. Provide at least one DNS name as a list.' - # Missing issuerRef name renders null - - it: renders null issuerRef name when not provided + # Missing issuerRef name triggers a validation error + - it: errors when issuerRef name is not provided release: name: test-release namespace: tenant-test @@ -285,14 +282,11 @@ tests: issuerRef: kind: ClusterIssuer asserts: - - hasDocuments: - count: 1 - - equal: - path: spec.issuerRef.name - value: null + - failedTemplate: + errorMessage: 'ERROR: "issuerRef.name" is required for cozy-lib.tls.certificate. Specify the ClusterIssuer or Issuer name.' - # Missing issuerRef kind renders null - - it: renders null issuerRef kind when not provided + # Missing issuerRef kind triggers a validation error + - it: errors when issuerRef kind is not provided release: name: test-release namespace: tenant-test @@ -305,9 +299,33 @@ tests: - svc.example issuerRef: name: my-issuer + asserts: + - failedTemplate: + errorMessage: 'ERROR: "issuerRef.kind" is required for cozy-lib.tls.certificate. Specify "ClusterIssuer" or "Issuer".' + + # Empty string duration should fall back to default 8760h + - it: uses default duration when duration is an empty string + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: empty-duration-tls + dnsNames: + - svc.example + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + duration: "" + renewBefore: "" asserts: - hasDocuments: count: 1 - equal: - path: spec.issuerRef.kind - value: null + path: spec.duration + value: 8760h + - equal: + path: spec.renewBefore + value: 720h From ab6d7ad6d55e98f575e9a836e19551e1624417f4 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 14:06:57 +0300 Subject: [PATCH 05/22] fix(cozy-lib-tls): add secretName validation and fix misleading test name Signed-off-by: Arsolitt --- packages/library/cozy-lib/templates/_tls.tpl | 3 +++ .../tests/tls-missing-secretname.yaml | 6 +++++ .../tests/tls_missing_secretname_test.yaml | 24 +++++++++++++++++++ .../tests/cozy-lib-tests/tests/tls_test.yaml | 4 ++-- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 packages/tests/cozy-lib-tests/templates/tests/tls-missing-secretname.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_missing_secretname_test.yaml diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl index 6e1c17c6..5f62abda 100644 --- a/packages/library/cozy-lib/templates/_tls.tpl +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -29,6 +29,9 @@ Parameters: {{- if not .name -}} {{- fail "ERROR: \"name\" is required for cozy-lib.tls.certificate. It sets the Certificate CR metadata.name." -}} {{- end -}} +{{- if not .secretName -}} +{{- fail "ERROR: \"secretName\" is required for cozy-lib.tls.certificate. Specify the TLS secret name to create." -}} +{{- end -}} {{- if not .dnsNames -}} {{- fail "ERROR: \"dnsNames\" is required for cozy-lib.tls.certificate. Provide at least one DNS name as a list." -}} {{- end -}} diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls-missing-secretname.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls-missing-secretname.yaml new file mode 100644 index 00000000..92e28bcc --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls-missing-secretname.yaml @@ -0,0 +1,6 @@ +{{/* +Test-only template: calls cozy-lib.tls.certificate without secretName +to verify the validation guard triggers. +*/}} +{{- $certArgs := dict "Release" .Release "name" .Values.tls.name "dnsNames" .Values.tls.dnsNames "issuerRef" .Values.tls.issuerRef -}} +{{- include "cozy-lib.tls.certificate" $certArgs -}} diff --git a/packages/tests/cozy-lib-tests/tests/tls_missing_secretname_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_missing_secretname_test.yaml new file mode 100644 index 00000000..14cee109 --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_missing_secretname_test.yaml @@ -0,0 +1,24 @@ +suite: TLS certificate helper - secretName validation + +templates: + - templates/tests/tls-missing-secretname.yaml + +tests: + # Missing secretName triggers a validation error + - it: errors when secretName is not provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: missing-secret-tls + dnsNames: + - svc.example + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + asserts: + - failedTemplate: + errorMessage: 'ERROR: "secretName" is required for cozy-lib.tls.certificate. Specify the TLS secret name to create.' diff --git a/packages/tests/cozy-lib-tests/tests/tls_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_test.yaml index 1fd18e6f..71df810b 100644 --- a/packages/tests/cozy-lib-tests/tests/tls_test.yaml +++ b/packages/tests/cozy-lib-tests/tests/tls_test.yaml @@ -181,8 +181,8 @@ tests: path: spec.secretName value: my-tls-cert - # Issuer kind defaults to ClusterIssuer - - it: defaults issuerRef kind to ClusterIssuer + # Issuer kind is passed through to Certificate CR + - it: passes issuerRef kind to Certificate CR release: name: test-release namespace: tenant-test From 6dca3f52b6a4401ec462de092048a6478226f335 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 14:12:15 +0300 Subject: [PATCH 06/22] fix(cozy-lib-tls): add issuerRef.kind enum validation, Release guard, and improve docs Signed-off-by: Arsolitt --- packages/library/cozy-lib/templates/_tls.tpl | 12 ++++++--- .../templates/tests/tls-missing-release.yaml | 6 +++++ .../tests/tls_missing_release_test.yaml | 25 +++++++++++++++++++ .../tests/cozy-lib-tests/tests/tls_test.yaml | 19 ++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 packages/tests/cozy-lib-tests/templates/tests/tls-missing-release.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_missing_release_test.yaml diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl index 5f62abda..13a80cf8 100644 --- a/packages/library/cozy-lib/templates/_tls.tpl +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -18,14 +18,17 @@ Usage: Parameters: - Release (required) - Helm release object, used for labels - name (required) - Certificate CR metadata.name - - secretName (required) - TLS secret name to create (can be same as name) + - secretName (required) - TLS secret name to create (can be same as name). Required when calling directly. When using through tls.yaml, defaults to name. - dnsNames (required) - list of DNS SANs - issuerRef (required) - dict with "name" and "kind" (e.g. ClusterIssuer, Issuer) - duration (optional) - certificate duration, defaults to 8760h (1 year); empty strings are treated as unset - renewBefore(optional) - renewal window, defaults to 720h (30 days) - - usages (optional) - list of key usages, defaults to ["server auth"] + - usages (optional) - list of key usages, defaults to ["server auth"] (not cert-manager's default of "digital signature, key encipherment"; this is a deliberate choice for TLS server certificates) */}} {{- define "cozy-lib.tls.certificate" -}} +{{- if not .Release -}} +{{- fail "ERROR: \"Release\" is required for cozy-lib.tls.certificate. Pass the Helm release object via the context dict." -}} +{{- end -}} {{- if not .name -}} {{- fail "ERROR: \"name\" is required for cozy-lib.tls.certificate. It sets the Certificate CR metadata.name." -}} {{- end -}} @@ -44,6 +47,9 @@ Parameters: {{- if not .issuerRef.kind -}} {{- fail "ERROR: \"issuerRef.kind\" is required for cozy-lib.tls.certificate. Specify \"ClusterIssuer\" or \"Issuer\"." -}} {{- end -}} +{{- if not (or (eq .issuerRef.kind "Issuer") (eq .issuerRef.kind "ClusterIssuer")) -}} +{{- fail "ERROR: issuerRef.kind must be \"Issuer\" or \"ClusterIssuer\"" -}} +{{- end -}} apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -90,7 +96,7 @@ Parameters: - secretName (optional) - user-provided secret name; if set, returned as-is */}} {{- define "cozy-lib.tls.secretName" -}} -{{- if .secretName -}} +{{- if not (empty .secretName) -}} {{- .secretName -}} {{- else -}} {{- printf "%s-%s" .Release.Name .suffix -}} diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls-missing-release.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls-missing-release.yaml new file mode 100644 index 00000000..ad51a301 --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls-missing-release.yaml @@ -0,0 +1,6 @@ +{{/* +Test-only template: calls cozy-lib.tls.certificate without Release +to verify the validation guard triggers. +*/}} +{{- $certArgs := dict "name" .Values.tls.name "secretName" .Values.tls.secretName "dnsNames" .Values.tls.dnsNames "issuerRef" .Values.tls.issuerRef -}} +{{- include "cozy-lib.tls.certificate" $certArgs -}} diff --git a/packages/tests/cozy-lib-tests/tests/tls_missing_release_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_missing_release_test.yaml new file mode 100644 index 00000000..5cfe9f5c --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_missing_release_test.yaml @@ -0,0 +1,25 @@ +suite: TLS certificate helper - Release validation + +templates: + - templates/tests/tls-missing-release.yaml + +tests: + # Missing Release triggers a validation error + - it: errors when Release is not provided + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: missing-release-tls + secretName: missing-release-tls-secret + dnsNames: + - svc.example + issuerRef: + name: selfsigned-cluster-issuer + kind: ClusterIssuer + asserts: + - failedTemplate: + errorMessage: 'ERROR: "Release" is required for cozy-lib.tls.certificate. Pass the Helm release object via the context dict.' diff --git a/packages/tests/cozy-lib-tests/tests/tls_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_test.yaml index 71df810b..a1b33587 100644 --- a/packages/tests/cozy-lib-tests/tests/tls_test.yaml +++ b/packages/tests/cozy-lib-tests/tests/tls_test.yaml @@ -303,6 +303,25 @@ tests: - failedTemplate: errorMessage: 'ERROR: "issuerRef.kind" is required for cozy-lib.tls.certificate. Specify "ClusterIssuer" or "Issuer".' + # Invalid issuerRef kind triggers a validation error + - it: errors when issuerRef kind is not Issuer or ClusterIssuer + release: + name: test-release + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tls: + name: invalid-kind-tls + dnsNames: + - svc.example + issuerRef: + name: my-issuer + kind: InvalidKind + asserts: + - failedTemplate: + errorMessage: 'ERROR: issuerRef.kind must be "Issuer" or "ClusterIssuer"' + # Empty string duration should fall back to default 8760h - it: uses default duration when duration is an empty string release: From cdc0e2183118318fa9ab6a9c47ba2cbc31cdbb99 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 24 Apr 2026 15:43:23 +0300 Subject: [PATCH 07/22] style(cozy-lib-tls): fix comment formatting and add secretName helper validation Add missing space in renewBefore parameter description and add fail guards for missing Release and suffix in the cozy-lib.tls.secretName helper. Signed-off-by: Arsolitt --- packages/library/cozy-lib/templates/_tls.tpl | 8 +++++++- .../tests/tls-secretname-missing-release.yaml | 11 +++++++++++ .../tests/tls-secretname-missing-suffix.yaml | 11 +++++++++++ .../tls_secretname_missing_release_test.yaml | 17 +++++++++++++++++ .../tls_secretname_missing_suffix_test.yaml | 17 +++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-release.yaml create mode 100644 packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-suffix.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_secretname_missing_release_test.yaml create mode 100644 packages/tests/cozy-lib-tests/tests/tls_secretname_missing_suffix_test.yaml diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl index 13a80cf8..310ccfbb 100644 --- a/packages/library/cozy-lib/templates/_tls.tpl +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -22,7 +22,7 @@ Parameters: - dnsNames (required) - list of DNS SANs - issuerRef (required) - dict with "name" and "kind" (e.g. ClusterIssuer, Issuer) - duration (optional) - certificate duration, defaults to 8760h (1 year); empty strings are treated as unset - - renewBefore(optional) - renewal window, defaults to 720h (30 days) + - renewBefore (optional) - renewal window, defaults to 720h (30 days) - usages (optional) - list of key usages, defaults to ["server auth"] (not cert-manager's default of "digital signature, key encipherment"; this is a deliberate choice for TLS server certificates) */}} {{- define "cozy-lib.tls.certificate" -}} @@ -96,6 +96,12 @@ Parameters: - secretName (optional) - user-provided secret name; if set, returned as-is */}} {{- define "cozy-lib.tls.secretName" -}} +{{- if not .Release -}} +{{- fail "ERROR: \"Release\" is required for cozy-lib.tls.secretName. Pass the Helm release object via the context dict." -}} +{{- end -}} +{{- if not .suffix -}} +{{- fail "ERROR: \"suffix\" is required for cozy-lib.tls.secretName. Provide a suffix for the generated secret name (e.g. \"tls\", \"server-tls\")." -}} +{{- end -}} {{- if not (empty .secretName) -}} {{- .secretName -}} {{- else -}} diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-release.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-release.yaml new file mode 100644 index 00000000..b361c611 --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-release.yaml @@ -0,0 +1,11 @@ +{{/* +Test-only template: calls cozy-lib.tls.secretName without Release +to verify the validation guard triggers. +*/}} +{{- $secretName := include "cozy-lib.tls.secretName" (dict "suffix" "tls" "secretName" .Values.tlsSecretName) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-secretname-missing-release +data: + secretName: {{ $secretName | quote }} diff --git a/packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-suffix.yaml b/packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-suffix.yaml new file mode 100644 index 00000000..199d7d6d --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls-secretname-missing-suffix.yaml @@ -0,0 +1,11 @@ +{{/* +Test-only template: calls cozy-lib.tls.secretName without suffix +to verify the validation guard triggers. +*/}} +{{- $secretName := include "cozy-lib.tls.secretName" (dict "Release" .Release "secretName" .Values.tlsSecretName) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-secretname-missing-suffix +data: + secretName: {{ $secretName | quote }} diff --git a/packages/tests/cozy-lib-tests/tests/tls_secretname_missing_release_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_secretname_missing_release_test.yaml new file mode 100644 index 00000000..ecfcb9ea --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_secretname_missing_release_test.yaml @@ -0,0 +1,17 @@ +suite: TLS secretName helper - Release validation + +templates: + - templates/tests/tls-secretname-missing-release.yaml + +tests: + - it: errors when Release is not provided + release: + name: my-app + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tlsSecretName: my-custom-tls-secret + asserts: + - failedTemplate: + errorMessage: 'ERROR: "Release" is required for cozy-lib.tls.secretName. Pass the Helm release object via the context dict.' diff --git a/packages/tests/cozy-lib-tests/tests/tls_secretname_missing_suffix_test.yaml b/packages/tests/cozy-lib-tests/tests/tls_secretname_missing_suffix_test.yaml new file mode 100644 index 00000000..586d0cee --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_secretname_missing_suffix_test.yaml @@ -0,0 +1,17 @@ +suite: TLS secretName helper - suffix validation + +templates: + - templates/tests/tls-secretname-missing-suffix.yaml + +tests: + - it: errors when suffix is not provided + release: + name: my-app + namespace: tenant-test + set: + _cluster: {} + _namespace: {} + tlsSecretName: "" + asserts: + - failedTemplate: + errorMessage: 'ERROR: "suffix" is required for cozy-lib.tls.secretName. Provide a suffix for the generated secret name (e.g. "tls", "server-tls").' From d482d1e56ae6afd0f49673d320a53bcb5655e6bd Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:32:55 +0300 Subject: [PATCH 08/22] test(redis): add helm unit tests for TLS support Signed-off-by: Arsolitt --- .../tests/dashboard-resourcemap-tls_test.yaml | 29 +++++++ .../redis/tests/redisfailover-tls_test.yaml | 84 +++++++++++++++++++ .../apps/redis/tests/service-tls_test.yaml | 48 +++++++++++ .../redis/tests/tls-certificate_test.yaml | 56 +++++++++++++ .../redis/tests/tls-nginx-config_test.yaml | 44 ++++++++++ 5 files changed, 261 insertions(+) create mode 100644 packages/apps/redis/tests/dashboard-resourcemap-tls_test.yaml create mode 100644 packages/apps/redis/tests/redisfailover-tls_test.yaml create mode 100644 packages/apps/redis/tests/service-tls_test.yaml create mode 100644 packages/apps/redis/tests/tls-certificate_test.yaml create mode 100644 packages/apps/redis/tests/tls-nginx-config_test.yaml diff --git a/packages/apps/redis/tests/dashboard-resourcemap-tls_test.yaml b/packages/apps/redis/tests/dashboard-resourcemap-tls_test.yaml new file mode 100644 index 00000000..f984b051 --- /dev/null +++ b/packages/apps/redis/tests/dashboard-resourcemap-tls_test.yaml @@ -0,0 +1,29 @@ +suite: Dashboard RBAC includes TLS secret +templates: + - templates/dashboard-resourcemap.yaml +release: + namespace: tenant-test +tests: + - it: TLS secret not in RBAC when tls.enabled is false + set: + tls: + enabled: false + documentSelector: + path: kind + value: Role + asserts: + - notContains: + path: rules[?(@.resources[0]=='secrets')].resourceNames + content: RELEASE-NAME-redis-tls + + - it: TLS secret added to RBAC when tls.enabled is true + set: + tls: + enabled: true + documentSelector: + path: kind + value: Role + asserts: + - contains: + path: rules[?(@.resources[0]=='secrets')].resourceNames + content: RELEASE-NAME-redis-tls diff --git a/packages/apps/redis/tests/redisfailover-tls_test.yaml b/packages/apps/redis/tests/redisfailover-tls_test.yaml new file mode 100644 index 00000000..52b279e6 --- /dev/null +++ b/packages/apps/redis/tests/redisfailover-tls_test.yaml @@ -0,0 +1,84 @@ +suite: RedisFailover TLS sidecar injection +templates: + - templates/redisfailover.yaml +tests: + - it: does not inject extraContainers when tls.enabled is false + set: + tls: + enabled: false + documentSelector: + path: kind + value: RedisFailover + asserts: + - notExists: + path: spec.redis.extraContainers + - notExists: + path: spec.redis.extraVolumes + + - it: injects nginx tls-proxy sidecar when tls.enabled is true + set: + tls: + enabled: true + documentSelector: + path: kind + value: RedisFailover + asserts: + - equal: + path: spec.redis.extraContainers[0].name + value: tls-proxy + - matchRegex: + path: spec.redis.extraContainers[0].image + pattern: "nginx" + - equal: + path: spec.redis.extraContainers[0].ports[0].containerPort + value: 6380 + - equal: + path: spec.redis.extraContainers[0].ports[0].name + value: redis-tls + + - it: extraVolumes reference auto-generated TLS secret when secretName empty + set: + tls: + enabled: true + secretName: "" + documentSelector: + path: kind + value: RedisFailover + asserts: + - contains: + path: spec.redis.extraVolumes + content: + name: tls-certs + secret: + secretName: RELEASE-NAME-redis-tls + + - it: extraVolumes reference user-provided secret when secretName is set + set: + tls: + enabled: true + secretName: my-custom-secret + documentSelector: + path: kind + value: RedisFailover + asserts: + - contains: + path: spec.redis.extraVolumes + content: + name: tls-certs + secret: + secretName: my-custom-secret + + - it: extraVolumes include nginx config ConfigMap + set: + tls: + enabled: true + documentSelector: + path: kind + value: RedisFailover + asserts: + - contains: + path: spec.redis.extraVolumes + content: + name: nginx-tls-config + configMap: + name: RELEASE-NAME-redis-tls-nginx diff --git a/packages/apps/redis/tests/service-tls_test.yaml b/packages/apps/redis/tests/service-tls_test.yaml new file mode 100644 index 00000000..ebd1c1f6 --- /dev/null +++ b/packages/apps/redis/tests/service-tls_test.yaml @@ -0,0 +1,48 @@ +suite: External service TLS port switching +templates: + - templates/service.yaml +tests: + - it: no service when external is false + set: + external: false + tls: + enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: external true and TLS false renders port 6379 plaintext + set: + external: true + tls: + enabled: false + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.ports[0].port + value: 6379 + - equal: + path: spec.ports[0].name + value: redis + - equal: + path: spec.ports[0].targetPort + value: redis + + - it: external true and TLS true renders port 6380 TLS + set: + external: true + tls: + enabled: true + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.ports[0].port + value: 6380 + - equal: + path: spec.ports[0].name + value: redis-tls + - equal: + path: spec.ports[0].targetPort + value: redis-tls diff --git a/packages/apps/redis/tests/tls-certificate_test.yaml b/packages/apps/redis/tests/tls-certificate_test.yaml new file mode 100644 index 00000000..dbb689cb --- /dev/null +++ b/packages/apps/redis/tests/tls-certificate_test.yaml @@ -0,0 +1,56 @@ +suite: TLS Certificate +templates: + - templates/tls-certificate.yaml +tests: + - it: does not render when tls.enabled is false + set: + tls: + enabled: false + asserts: + - hasDocuments: { count: 0 } + + - it: does not render when tls.secretName is provided (user owns secret) + set: + tls: + enabled: true + secretName: my-existing-secret + asserts: + - hasDocuments: { count: 0 } + + - it: renders Certificate when tls.enabled and secretName empty + set: + tls: + enabled: true + secretName: "" + asserts: + - hasDocuments: { count: 1 } + - isKind: { of: Certificate } + - equal: { path: apiVersion, value: cert-manager.io/v1 } + - equal: { path: spec.secretName, value: RELEASE-NAME-redis-tls } + + - it: uses default issuerRef + set: + tls: + enabled: true + asserts: + - equal: { path: spec.issuerRef.name, value: selfsigned-cluster-issuer } + - equal: { path: spec.issuerRef.kind, value: ClusterIssuer } + + - it: respects custom issuerRef + set: + tls: + enabled: true + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + asserts: + - equal: { path: spec.issuerRef.name, value: letsencrypt-prod } + + - it: dnsNames include external-lb and rfs- service variants + set: + tls: + enabled: true + asserts: + - contains: { path: spec.dnsNames, content: RELEASE-NAME-external-lb } + - contains: { path: spec.dnsNames, content: rfs-RELEASE-NAME } + - contains: { path: spec.dnsNames, content: rfs-RELEASE-NAME.NAMESPACE.svc.cluster.local } diff --git a/packages/apps/redis/tests/tls-nginx-config_test.yaml b/packages/apps/redis/tests/tls-nginx-config_test.yaml new file mode 100644 index 00000000..c7d57b01 --- /dev/null +++ b/packages/apps/redis/tests/tls-nginx-config_test.yaml @@ -0,0 +1,44 @@ +suite: TLS nginx ConfigMap +templates: + - templates/tls-nginx-config.yaml +tests: + - it: does not render when tls.enabled is false + set: + tls: + enabled: false + asserts: + - hasDocuments: { count: 0 } + + - it: renders ConfigMap when tls.enabled is true + set: + tls: + enabled: true + asserts: + - hasDocuments: { count: 1 } + - isKind: { of: ConfigMap } + - equal: + path: metadata.name + value: RELEASE-NAME-redis-tls-nginx + - exists: + path: data["nginx.conf"] + + - it: nginx config contains required directives + set: + tls: + enabled: true + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: "listen 6380 ssl" + - matchRegex: + path: data["nginx.conf"] + pattern: "127\\.0\\.0\\.1:6379" + - matchRegex: + path: data["nginx.conf"] + pattern: "ssl_certificate /etc/nginx/tls/tls\\.crt" + - matchRegex: + path: data["nginx.conf"] + pattern: "ssl_certificate_key /etc/nginx/tls/tls\\.key" + - matchRegex: + path: data["nginx.conf"] + pattern: "ssl_protocols TLSv1\\.2 TLSv1\\.3" From 857ebd0420110b553eeb739bac2eb326fbbf55d3 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:35:14 +0300 Subject: [PATCH 09/22] feat(redis): add TLS values and schema Add tls section to values.yaml and values.schema.json so users can enable TLS termination on the client-facing port. Defaults preserve current behavior (disabled), and the schema documents the relationship with cert-manager. Signed-off-by: Arsolitt --- packages/apps/redis/values.schema.json | 36 ++++++++++++++++++++++++++ packages/apps/redis/values.yaml | 13 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/apps/redis/values.schema.json b/packages/apps/redis/values.schema.json index a93d92ac..6f0eccb4 100644 --- a/packages/apps/redis/values.schema.json +++ b/packages/apps/redis/values.schema.json @@ -91,6 +91,42 @@ "description": "Enable password generation.", "type": "boolean", "default": true + }, + "tls": { + "description": "TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext.", + "type": "object", + "properties": { + "enabled": { + "description": "Enable TLS termination via nginx stream sidecar.", + "type": "boolean", + "default": false + }, + "secretName": { + "description": "Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret.", + "type": "string", + "default": "" + }, + "issuerRef": { + "description": "cert-manager Issuer reference. Used only when secretName is empty.", + "type": "object", + "properties": { + "name": { + "description": "Issuer/ClusterIssuer resource name.", + "type": "string", + "default": "selfsigned-cluster-issuer" + }, + "kind": { + "description": "Either \"Issuer\" or \"ClusterIssuer\".", + "type": "string", + "default": "ClusterIssuer", + "enum": [ + "Issuer", + "ClusterIssuer" + ] + } + } + } + } } } } diff --git a/packages/apps/redis/values.yaml b/packages/apps/redis/values.yaml index efe80d07..ca05812c 100644 --- a/packages/apps/redis/values.yaml +++ b/packages/apps/redis/values.yaml @@ -46,3 +46,16 @@ version: v8 ## @param {bool} authEnabled - Enable password generation. authEnabled: true + +## @param {struct} tls - TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext. +## @field {bool} enabled - Enable TLS termination via nginx stream sidecar. +## @field {string} secretName - Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret. +## @field {struct} issuerRef - cert-manager Issuer reference. Used only when secretName is empty. +## @field {string} issuerRef.name - Issuer/ClusterIssuer resource name. +## @field {string} issuerRef.kind - Either "Issuer" or "ClusterIssuer". +tls: + enabled: false + secretName: "" + issuerRef: + name: "selfsigned-cluster-issuer" + kind: "ClusterIssuer" From a18bfc3f9113212f89f5c2a32e9a36a53cb59154 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:35:37 +0300 Subject: [PATCH 10/22] feat(redis): add TLS helper template Define local helpers redis.tls.secretName (wraps the cozy-lib helper with the redis-tls suffix) and redis.tls.dnsNames (covers the external LB service and the operator-managed master service rfs-). Signed-off-by: Arsolitt --- packages/apps/redis/templates/_tls.tpl | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/apps/redis/templates/_tls.tpl diff --git a/packages/apps/redis/templates/_tls.tpl b/packages/apps/redis/templates/_tls.tpl new file mode 100644 index 00000000..fc90a011 --- /dev/null +++ b/packages/apps/redis/templates/_tls.tpl @@ -0,0 +1,27 @@ +{{/* +redis.tls.secretName — wraps cozy-lib.tls.secretName with redis-specific suffix. +Returns either the user-provided tls.secretName or "-redis-tls". +*/}} +{{- define "redis.tls.secretName" -}} +{{- include "cozy-lib.tls.secretName" (dict + "Release" .Release + "suffix" "redis-tls" + "secretName" .Values.tls.secretName +) -}} +{{- end -}} + +{{/* +redis.tls.dnsNames — DNS SANs for the Redis TLS certificate. +Covers the external LoadBalancer service and the operator-managed master +service (rfs-) across short, namespaced, and FQDN forms. +*/}} +{{- define "redis.tls.dnsNames" -}} +- {{ .Release.Name }}-external-lb +- {{ .Release.Name }}-external-lb.{{ .Release.Namespace }} +- {{ .Release.Name }}-external-lb.{{ .Release.Namespace }}.svc +- {{ .Release.Name }}-external-lb.{{ .Release.Namespace }}.svc.cluster.local +- rfs-{{ .Release.Name }} +- rfs-{{ .Release.Name }}.{{ .Release.Namespace }} +- rfs-{{ .Release.Name }}.{{ .Release.Namespace }}.svc +- rfs-{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local +{{- end -}} From 9102d8094ad75c6b4b3ad79c44b43aece65a3a1a Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:35:59 +0300 Subject: [PATCH 11/22] feat(redis): add TLS Certificate and nginx ConfigMap templates Render a cert-manager Certificate via the cozy-lib helper (only when tls.enabled and no user-provided secret) and a ConfigMap with the nginx stream config that terminates TLS on 6380 and proxies to Redis on 6379. Signed-off-by: Arsolitt --- .../apps/redis/templates/tls-certificate.yaml | 18 ++++++++++ .../redis/templates/tls-nginx-config.yaml | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 packages/apps/redis/templates/tls-certificate.yaml create mode 100644 packages/apps/redis/templates/tls-nginx-config.yaml diff --git a/packages/apps/redis/templates/tls-certificate.yaml b/packages/apps/redis/templates/tls-certificate.yaml new file mode 100644 index 00000000..7a0e8229 --- /dev/null +++ b/packages/apps/redis/templates/tls-certificate.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.tls.enabled (not .Values.tls.secretName) }} +{{- $dnsNames := list -}} +{{- $dnsNames = append $dnsNames (printf "%s-external-lb" .Release.Name) -}} +{{- $dnsNames = append $dnsNames (printf "%s-external-lb.%s" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-external-lb.%s.svc" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-external-lb.%s.svc.cluster.local" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "rfs-%s" .Release.Name) -}} +{{- $dnsNames = append $dnsNames (printf "rfs-%s.%s" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "rfs-%s.%s.svc" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "rfs-%s.%s.svc.cluster.local" .Release.Name .Release.Namespace) -}} +{{- include "cozy-lib.tls.certificate" (dict + "Release" .Release + "name" (printf "%s-redis-tls" .Release.Name) + "secretName" (include "redis.tls.secretName" .) + "dnsNames" $dnsNames + "issuerRef" .Values.tls.issuerRef +) }} +{{- end }} diff --git a/packages/apps/redis/templates/tls-nginx-config.yaml b/packages/apps/redis/templates/tls-nginx-config.yaml new file mode 100644 index 00000000..cf08f1ea --- /dev/null +++ b/packages/apps/redis/templates/tls-nginx-config.yaml @@ -0,0 +1,34 @@ +{{- if .Values.tls.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-redis-tls-nginx + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/component: redis-tls-proxy +data: + nginx.conf: | + worker_processes 1; + error_log /dev/stderr notice; + pid /tmp/nginx.pid; + events { + worker_connections 1024; + } + stream { + upstream redis_backend { + server 127.0.0.1:6379; + } + server { + listen 6380 ssl; + ssl_certificate /etc/nginx/tls/tls.crt; + ssl_certificate_key /etc/nginx/tls/tls.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers off; + proxy_pass redis_backend; + proxy_timeout 1h; + proxy_connect_timeout 5s; + } + } +{{- end }} From 47936637b6156e510c1bb59afacb047c0e28a702 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:36:16 +0300 Subject: [PATCH 12/22] feat(redis): inject nginx TLS sidecar into RedisFailover Use the operator's spec.redis.extraContainers / extraVolumes hooks to attach an nginx-unprivileged stream sidecar that terminates TLS on 6380. The Redis container itself is untouched, so operator probes, sentinel, and replication continue talking plaintext on 6379. Signed-off-by: Arsolitt --- .../apps/redis/templates/redisfailover.yaml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/apps/redis/templates/redisfailover.yaml b/packages/apps/redis/templates/redisfailover.yaml index 160e030d..b05d76a7 100644 --- a/packages/apps/redis/templates/redisfailover.yaml +++ b/packages/apps/redis/templates/redisfailover.yaml @@ -64,6 +64,48 @@ spec: - appendonly no - save "" {{- end }} + {{- if .Values.tls.enabled }} + extraContainers: + - name: tls-proxy + image: nginxinc/nginx-unprivileged:1.29-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6380 + name: redis-tls + protocol: TCP + volumeMounts: + - name: tls-certs + mountPath: /etc/nginx/tls + readOnly: true + - name: nginx-tls-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi + livenessProbe: + tcpSocket: + port: 6380 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 6380 + initialDelaySeconds: 2 + periodSeconds: 5 + extraVolumes: + - name: tls-certs + secret: + secretName: {{ include "redis.tls.secretName" . }} + - name: nginx-tls-config + configMap: + name: {{ .Release.Name }}-redis-tls-nginx + {{- end }} {{- if .Values.authEnabled }} auth: secretPath: {{ .Release.Name }}-auth From 08d2b065e1e2042614f54e52c1622870f3d4d70e Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:36:30 +0300 Subject: [PATCH 13/22] feat(redis): route external service to TLS port when enabled When tls.enabled is true the LoadBalancer service exposes 6380 / redis-tls (targeting the sidecar) instead of 6379. Plaintext is intentionally not exposed externally in TLS mode; intra-cluster apps keep using the operator's rfs-* ClusterIP service on 6379. Signed-off-by: Arsolitt --- packages/apps/redis/templates/service.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/apps/redis/templates/service.yaml b/packages/apps/redis/templates/service.yaml index 81a73761..24324337 100644 --- a/packages/apps/redis/templates/service.yaml +++ b/packages/apps/redis/templates/service.yaml @@ -21,7 +21,15 @@ spec: app.kubernetes.io/part-of: redis-failover redisfailovers-role: master ports: + {{- if .Values.tls.enabled }} + - name: redis-tls + port: 6380 + targetPort: redis-tls + protocol: TCP + {{- else }} - name: redis port: 6379 targetPort: redis + protocol: TCP + {{- end }} {{- end }} From 94663e30947bc5b5281c55cf47cdf08e493615df Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:36:43 +0300 Subject: [PATCH 14/22] feat(redis): expose TLS secret to dashboard RBAC Add the TLS secret (resolved via redis.tls.secretName, which respects user-provided tls.secretName) to the Role's secrets resourceNames so tenants can fetch ca.crt from the dashboard when TLS is enabled. Signed-off-by: Arsolitt --- packages/apps/redis/templates/dashboard-resourcemap.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/apps/redis/templates/dashboard-resourcemap.yaml b/packages/apps/redis/templates/dashboard-resourcemap.yaml index 63f82a7d..d427cf37 100644 --- a/packages/apps/redis/templates/dashboard-resourcemap.yaml +++ b/packages/apps/redis/templates/dashboard-resourcemap.yaml @@ -19,6 +19,9 @@ rules: - secrets resourceNames: - "{{ .Release.Name }}-auth" + {{- if .Values.tls.enabled }} + - "{{ include "redis.tls.secretName" . }}" + {{- end }} verbs: ["get", "list", "watch"] - apiGroups: - cozystack.io From a8fc51564987fe8bf16ebcc80a45ce8a918edccf Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:39:11 +0300 Subject: [PATCH 15/22] chore(redis): regenerate after TLS support Regenerate Go types, README parameter table, normalized JSON schema, and the redis-rd openAPISchema/keysOrder so generator output stays in sync with the values.yaml additions for TLS. Switch the inline `@param {struct} tls` annotation to a pair of `@typedef`s (`TLS` and `TLSIssuerRef`) so cozyvalues-gen can resolve the nested issuerRef object. Signed-off-by: Arsolitt --- api/apps/v1alpha1/redis/types.go | 24 ++++++++++++++ packages/apps/redis/README.md | 12 +++++-- packages/apps/redis/values.schema.json | 35 ++++++++++++--------- packages/apps/redis/values.yaml | 12 ++++--- packages/system/redis-rd/cozyrds/redis.yaml | 4 +-- 5 files changed, 64 insertions(+), 23 deletions(-) diff --git a/api/apps/v1alpha1/redis/types.go b/api/apps/v1alpha1/redis/types.go index e9a02c72..3e12687a 100644 --- a/api/apps/v1alpha1/redis/types.go +++ b/api/apps/v1alpha1/redis/types.go @@ -41,6 +41,9 @@ type ConfigSpec struct { // Enable password generation. // +kubebuilder:default:=true AuthEnabled bool `json:"authEnabled"` + // TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext. + // +kubebuilder:default:={} + Tls TLS `json:"tls"` } type Resources struct { @@ -50,6 +53,27 @@ type Resources struct { Memory resource.Quantity `json:"memory,omitempty"` } +type TLS struct { + // Enable TLS termination via nginx stream sidecar. + // +kubebuilder:default:=false + Enabled bool `json:"enabled"` + // cert-manager Issuer reference. Used only when secretName is empty. + // +kubebuilder:default:={} + IssuerRef TLSIssuerRef `json:"issuerRef"` + // Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret. + // +kubebuilder:default:="" + SecretName string `json:"secretName"` +} + +type TLSIssuerRef struct { + // Either "Issuer" or "ClusterIssuer". + // +kubebuilder:default:="ClusterIssuer" + Kind string `json:"kind"` + // Issuer/ClusterIssuer resource name. + // +kubebuilder:default:="selfsigned-cluster-issuer" + Name string `json:"name"` +} + // +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge" type ResourcesPreset string diff --git a/packages/apps/redis/README.md b/packages/apps/redis/README.md index 7f82d229..454ff91a 100644 --- a/packages/apps/redis/README.md +++ b/packages/apps/redis/README.md @@ -28,9 +28,15 @@ Service utilizes the Spotahome Redis Operator for efficient management and orche ### Application-specific parameters -| Name | Description | Type | Value | -| ------------- | --------------------------- | ------ | ------ | -| `authEnabled` | Enable password generation. | `bool` | `true` | +| Name | Description | Type | Value | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------- | +| `authEnabled` | Enable password generation. | `bool` | `true` | +| `tls` | TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext. | `object` | `{}` | +| `tls.enabled` | Enable TLS termination via nginx stream sidecar. | `bool` | `false` | +| `tls.secretName` | Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret. | `string` | `""` | +| `tls.issuerRef` | cert-manager Issuer reference. Used only when secretName is empty. | `object` | `{}` | +| `tls.issuerRef.name` | Issuer/ClusterIssuer resource name. | `string` | `selfsigned-cluster-issuer` | +| `tls.issuerRef.kind` | Either "Issuer" or "ClusterIssuer". | `string` | `ClusterIssuer` | ## Parameter examples and reference diff --git a/packages/apps/redis/values.schema.json b/packages/apps/redis/values.schema.json index 6f0eccb4..ad678339 100644 --- a/packages/apps/redis/values.schema.json +++ b/packages/apps/redis/values.schema.json @@ -95,36 +95,43 @@ "tls": { "description": "TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext.", "type": "object", + "default": {}, + "required": [ + "enabled", + "issuerRef", + "secretName" + ], "properties": { "enabled": { "description": "Enable TLS termination via nginx stream sidecar.", "type": "boolean", "default": false }, - "secretName": { - "description": "Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret.", - "type": "string", - "default": "" - }, "issuerRef": { "description": "cert-manager Issuer reference. Used only when secretName is empty.", "type": "object", + "default": {}, + "required": [ + "kind", + "name" + ], "properties": { + "kind": { + "description": "Either \"Issuer\" or \"ClusterIssuer\".", + "type": "string", + "default": "ClusterIssuer" + }, "name": { "description": "Issuer/ClusterIssuer resource name.", "type": "string", "default": "selfsigned-cluster-issuer" - }, - "kind": { - "description": "Either \"Issuer\" or \"ClusterIssuer\".", - "type": "string", - "default": "ClusterIssuer", - "enum": [ - "Issuer", - "ClusterIssuer" - ] } } + }, + "secretName": { + "description": "Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret.", + "type": "string", + "default": "" } } } diff --git a/packages/apps/redis/values.yaml b/packages/apps/redis/values.yaml index ca05812c..0dd7c9ea 100644 --- a/packages/apps/redis/values.yaml +++ b/packages/apps/redis/values.yaml @@ -47,12 +47,16 @@ version: v8 ## @param {bool} authEnabled - Enable password generation. authEnabled: true -## @param {struct} tls - TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext. +## @typedef {struct} TLSIssuerRef - cert-manager Issuer reference. +## @field {string} name - Issuer/ClusterIssuer resource name. +## @field {string} kind - Either "Issuer" or "ClusterIssuer". + +## @typedef {struct} TLS - TLS configuration for client-facing connections. ## @field {bool} enabled - Enable TLS termination via nginx stream sidecar. ## @field {string} secretName - Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret. -## @field {struct} issuerRef - cert-manager Issuer reference. Used only when secretName is empty. -## @field {string} issuerRef.name - Issuer/ClusterIssuer resource name. -## @field {string} issuerRef.kind - Either "Issuer" or "ClusterIssuer". +## @field {TLSIssuerRef} issuerRef - cert-manager Issuer reference. Used only when secretName is empty. + +## @param {TLS} tls - TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext. tls: enabled: false secretName: "" diff --git a/packages/system/redis-rd/cozyrds/redis.yaml b/packages/system/redis-rd/cozyrds/redis.yaml index e5191aa4..e9c9ce15 100644 --- a/packages/system/redis-rd/cozyrds/redis.yaml +++ b/packages/system/redis-rd/cozyrds/redis.yaml @@ -8,7 +8,7 @@ spec: plural: redises singular: redis openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each Redis replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"Redis major version to deploy","type":"string","default":"v8","enum":["v8","v7"]},"authEnabled":{"description":"Enable password generation.","type":"boolean","default":true}}} + {"title":"Chart Values","type":"object","properties":{"replicas":{"description":"Number of Redis replicas.","type":"integer","default":2},"resources":{"description":"Explicit CPU and memory configuration for each Redis replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"1Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"version":{"description":"Redis major version to deploy","type":"string","default":"v8","enum":["v8","v7"]},"authEnabled":{"description":"Enable password generation.","type":"boolean","default":true},"tls":{"description":"TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext.","type":"object","default":{},"required":["enabled","issuerRef","secretName"],"properties":{"enabled":{"description":"Enable TLS termination via nginx stream sidecar.","type":"boolean","default":false},"issuerRef":{"description":"cert-manager Issuer reference. Used only when secretName is empty.","type":"object","default":{},"required":["kind","name"],"properties":{"kind":{"description":"Either \"Issuer\" or \"ClusterIssuer\".","type":"string","default":"ClusterIssuer"},"name":{"description":"Issuer/ClusterIssuer resource name.","type":"string","default":"selfsigned-cluster-issuer"}}},"secretName":{"description":"Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret.","type":"string","default":""}}}}} release: prefix: redis- labels: @@ -25,7 +25,7 @@ spec: tags: - cache icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODNfMzIxMykiLz4KPHBhdGggZD0iTTEyMC4xNDkgOTUuNTQ5MUMxMTQuNTg2IDk4LjQ0ODUgODUuNzcwOSAxMTAuMjk2IDc5LjYzNjMgMTEzLjQ5NUM3My41MDE2IDExNi42OTMgNzAuMDkzNyAxMTYuNjYyIDY1LjI0NzIgMTE0LjM0NkM2MC40MDEyIDExMi4wMjkgMjkuNzM2NCA5OS42NDIzIDI0LjIxMjUgOTcuMDAxOUMyMS40NTE5IDk1LjY4MjcgMjAgOTQuNTY4NyAyMCA5My41MTY2VjgyLjk4MDlDMjAgODIuOTgwOSA1OS45MjIgNzQuMjkwMSA2Ni4zNjY5IDcxLjk3NzhDNzIuODExNSA2OS42NjU2IDc1LjA0NzYgNjkuNTgyMSA4MC41MzIgNzEuNTkxQzg2LjAxNzMgNzMuNjAwOCAxMTguODEyIDc5LjUxNzYgMTI0LjIzMyA4MS41MDI5TDEyNC4yMyA5MS44ODk2QzEyNC4yMzEgOTIuOTMxMSAxMjIuOTggOTQuMDczNiAxMjAuMTQ5IDk1LjU0OTFaIiBmaWxsPSIjOTEyNjI2Ii8+CjxwYXRoIGQ9Ik0xMjAuMTQ3IDg1LjA3NTJDMTE0LjU4NSA4Ny45NzM0IDg1Ljc3IDk5LjgyMTggNzkuNjM1NCAxMDMuMDJDNzMuNTAxMSAxMDYuMjE5IDcwLjA5MzIgMTA2LjE4NyA2NS4yNDcyIDEwMy44NzFDNjAuNDAwNyAxMDEuNTU1IDI5LjczNzEgODkuMTY2OCAyNC4yMTM2IDg2LjUyOEMxOC42OTAxIDgzLjg4NzYgMTguNTc0NCA4Mi4wNzA0IDI0LjAwMDMgNzkuOTQ1OEMyOS40MjYxIDc3LjgyMDUgNTkuOTIxNSA2NS44NTYxIDY2LjM2NzIgNjMuNTQzOEM3Mi44MTE4IDYxLjIzMjQgNzUuMDQ3NSA2MS4xNDgxIDgwLjUzMTkgNjMuMTU3OEM4Ni4wMTY4IDY1LjE2NjggMTE0LjY2IDc2LjU2NzYgMTIwLjA3OSA3OC41NTI1QzEyNS41MDEgODAuNTM5OSAxMjUuNzA5IDgyLjE3NjMgMTIwLjE0NyA4NS4wNzUyWiIgZmlsbD0iI0M2MzAyQiIvPgo8cGF0aCBkPSJNMTIwLjE0OSA3OC41MDJDMTE0LjU4NiA4MS40MDE4IDg1Ljc3MDkgOTMuMjQ5MyA3OS42MzYzIDk2LjQ0ODhDNzMuNTAxNiA5OS42NDYyIDcwLjA5MzcgOTkuNjE1MiA2NS4yNDcyIDk3LjI5ODVDNjAuNDAwOCA5NC45ODMgMjkuNzM2NCA4Mi41OTUyIDI0LjIxMjUgNzkuOTU0N0MyMS40NTE5IDc4LjYzNTUgMjAgNzcuNTIzMiAyMCA3Ni40NzA3VjY1LjkzMzhDMjAgNjUuOTMzOCA1OS45MjIgNTcuMjQzNCA2Ni4zNjY5IDU0LjkzMTFDNzIuODExNSA1Mi42MTg5IDc1LjA0NzYgNTIuNTM1IDgwLjUzMiA1NC41NDQzQzg2LjAxNzcgNTYuNTUzNiAxMTguODEzIDYyLjQ2OTMgMTI0LjIzMyA2NC40NTVMMTI0LjIzIDc0Ljg0MjhDMTI0LjIzMSA3NS44ODQgMTIyLjk4IDc3LjAyNjQgMTIwLjE0OSA3OC41MDJaIiBmaWxsPSIjOTEyNjI2Ii8+CjxwYXRoIGQ9Ik0xMjAuMTQ3IDY4LjAyODJDMTE0LjU4NSA3MC45MjcxIDg1Ljc3IDgyLjc3NDcgNzkuNjM1NCA4NS45NzM3QzczLjUwMTEgODkuMTcxNiA3MC4wOTMyIDg5LjE0MDIgNjUuMjQ3MiA4Ni44MjM1QzYwLjQwMDcgODQuNTA4NCAyOS43MzcxIDcyLjEyMDEgMjQuMjEzNiA2OS40ODA5QzE4LjY5MDEgNjYuODQxMyAxOC41NzQ0IDY1LjAyMzcgMjQuMDAwMyA2Mi44OTg0QzI5LjQyNjEgNjAuNzc0MiA1OS45MjE5IDQ4LjgwOSA2Ni4zNjcyIDQ2LjQ5NzJDNzIuODExOCA0NC4xODUzIDc1LjA0NzUgNDQuMTAxNCA4MC41MzE5IDQ2LjExMDhDODYuMDE2OCA0OC4xMTk3IDExNC42NiA1OS41MTk3IDEyMC4wNzkgNjEuNTA1NUMxMjUuNTAxIDYzLjQ5MjQgMTI1LjcwOSA2NS4xMjkyIDEyMC4xNDcgNjguMDI4MloiIGZpbGw9IiNDNjMwMkIiLz4KPHBhdGggZD0iTTEyMC4xNDkgNjAuODIyNEMxMTQuNTg2IDYzLjcyMTQgODUuNzcwOSA3NS41Njk4IDc5LjYzNjMgNzguNzY5MkM3My41MDE2IDgxLjk2NzEgNzAuMDkzNyA4MS45MzU3IDY1LjI0NzIgNzkuNjE5QzYwLjQwMDggNzcuMzAzNSAyOS43MzY0IDY0LjkxNTIgMjQuMjEyNSA2Mi4yNzZDMjEuNDUxOSA2MC45NTU2IDIwIDU5Ljg0MjggMjAgNTguNzkxNVY0OC4yNTQyQzIwIDQ4LjI1NDIgNTkuOTIyIDM5LjU2NDIgNjYuMzY2OSAzNy4yNTI0QzcyLjgxMTUgMzQuOTM5NyA3NS4wNDc2IDM0Ljg1NjcgODAuNTMyIDM2Ljg2NTZDODYuMDE3NyAzOC44NzQ5IDExOC44MTMgNDQuNzkwNSAxMjQuMjMzIDQ2Ljc3NjNMMTI0LjIzIDU3LjE2MzdDMTI0LjIzMSA1OC4yMDQgMTIyLjk4IDU5LjM0NjUgMTIwLjE0OSA2MC44MjI0WiIgZmlsbD0iIzkxMjYyNiIvPgo8cGF0aCBkPSJNMTIwLjE0NyA1MC4zNDlDMTE0LjU4NSA1My4yNDc5IDg1Ljc2OTggNjUuMDk2MyA3OS42MzUyIDY4LjI5NDFDNzMuNTAwOSA3MS40OTIgNzAuMDkzIDcxLjQ2MDYgNjUuMjQ2OSA2OS4xNDUxQzYwLjQwMDkgNjYuODI4MyAyOS43MzY5IDU0LjQ0MDkgMjQuMjEzOCA1MS44MDEzQzE4LjY4OTkgNDkuMTYyMSAxOC41NzQ2IDQ3LjM0NDEgMjQgNDUuMjE5MkMyOS40MjU5IDQzLjA5NDYgNTkuOTIxNyAzMS4xMzEgNjYuMzY3IDI4LjgxODRDNzIuODExNiAyNi41MDYxIDc1LjA0NzMgMjYuNDIzIDgwLjUzMTcgMjguNDMyNEM4Ni4wMTY2IDMwLjQ0MTcgMTE0LjY1OSA0MS44NDE4IDEyMC4wNzkgNDMuODI3NUMxMjUuNTAxIDQ1LjgxMjggMTI1LjcwOSA0Ny40NSAxMjAuMTQ3IDUwLjM0OVoiIGZpbGw9IiNDNjMwMkIiLz4KPHBhdGggZD0iTTg0Ljg1NDEgNDAuMDk5NEw3NS44OTI2IDQxLjAyOThMNzMuODg2NSA0NS44NTdMNzAuNjQ2MyA0MC40NzAzTDYwLjI5ODMgMzkuNTQwNEw2OC4wMTk3IDM2Ljc1NThMNjUuNzAzIDMyLjQ4MTRMNzIuOTMyMSAzNS4zMDg4TDc5Ljc0NzEgMzMuMDc3NUw3Ny45MDUyIDM3LjQ5NzJMODQuODU0MSA0MC4wOTk0Wk03My4zNTE1IDYzLjUxODRMNTYuNjI2NiA1Ni41ODE2TDgwLjU5MiA1Mi45MDI5TDczLjM1MTUgNjMuNTE4NFpNNTAuMTYzNyA0Mi43ODI2QzU3LjIzODEgNDIuNzgyNiA2Mi45NzMgNDUuMDA1NyA2Mi45NzMgNDcuNzQ3NUM2Mi45NzMgNTAuNDkwMSA1Ny4yMzgxIDUyLjcxMjggNTAuMTYzNyA1Mi43MTI4QzQzLjA4OTMgNTIuNzEyOCAzNy4zNTQ1IDUwLjQ4OTcgMzcuMzU0NSA0Ny43NDc1QzM3LjM1NDUgNDUuMDA1NyA0My4wODkzIDQyLjc4MjYgNTAuMTYzNyA0Mi43ODI2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTk1LjQ0MzQgNDEuNDE3NEwxMDkuNjI3IDQ3LjAyMjRMOTUuNDU1NiA1Mi42MjJMOTUuNDQzNCA0MS40MTc0WiIgZmlsbD0iIzYyMUIxQyIvPgo8cGF0aCBkPSJNNzkuNzUyOSA0Ny42MjYxTDk1LjQ0NDkgNDEuNDE4OUw5NS40NTcxIDUyLjYyMzZMOTMuOTE4NCA1My4yMjU0TDc5Ljc1MjkgNDcuNjI2MVoiIGZpbGw9IiM5QTI5MjgiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl82ODNfMzIxMyIgeDE9IjE4OSIgeTE9IjIxMC41IiB4Mj0iMCIgeTI9IjAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0E4MDAwMCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRkNGQ0YiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "version"], ["spec", "authEnabled"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "version"], ["spec", "authEnabled"], ["spec", "tls"], ["spec", "tls", "enabled"], ["spec", "tls", "secretName"], ["spec", "tls", "issuerRef"], ["spec", "tls", "issuerRef", "name"], ["spec", "tls", "issuerRef", "kind"]] secrets: exclude: [] include: From 125bd116ec8f7fcba3342258bb2d418dc8a71f3c Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:39:41 +0300 Subject: [PATCH 16/22] feat(redis-rd): include TLS secret in dashboard secrets list Allow the dashboard to read the auto-generated `redis--redis-tls` secret so tenants can download the CA certificate and connect with TLS clients. The pattern matches the secret produced by cozy-lib's TLS helper when no user-provided `tls.secretName` is set. Signed-off-by: Arsolitt --- packages/system/redis-rd/cozyrds/redis.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/system/redis-rd/cozyrds/redis.yaml b/packages/system/redis-rd/cozyrds/redis.yaml index e9c9ce15..73711517 100644 --- a/packages/system/redis-rd/cozyrds/redis.yaml +++ b/packages/system/redis-rd/cozyrds/redis.yaml @@ -31,6 +31,7 @@ spec: include: - resourceNames: - redis-{{ .name }}-auth + - redis-{{ .name }}-redis-tls services: exclude: [] include: From 61b84103213b343b22cc0cb22d935e375bbda62d Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 19:53:06 +0300 Subject: [PATCH 17/22] chore(redis): regenerate deepcopy code for TLS types Per-package make generate runs cozyvalues-gen and update-crd.sh, but deepcopy code is generated only by root-level make generate (which calls hack/update-codegen.sh). Add the missing DeepCopy methods for the new TLS and TLSIssuerRef structs. Signed-off-by: Arsolitt --- .../v1alpha1/redis/zz_generated.deepcopy.go | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/api/apps/v1alpha1/redis/zz_generated.deepcopy.go b/api/apps/v1alpha1/redis/zz_generated.deepcopy.go index 6390200c..4e5f844b 100644 --- a/api/apps/v1alpha1/redis/zz_generated.deepcopy.go +++ b/api/apps/v1alpha1/redis/zz_generated.deepcopy.go @@ -55,6 +55,7 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) { *out = *in in.Resources.DeepCopyInto(&out.Resources) out.Size = in.Size.DeepCopy() + out.Tls = in.Tls } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSpec. @@ -83,3 +84,34 @@ func (in *Resources) DeepCopy() *Resources { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLS) DeepCopyInto(out *TLS) { + *out = *in + out.IssuerRef = in.IssuerRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. +func (in *TLS) DeepCopy() *TLS { + if in == nil { + return nil + } + out := new(TLS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSIssuerRef) DeepCopyInto(out *TLSIssuerRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSIssuerRef. +func (in *TLSIssuerRef) DeepCopy() *TLSIssuerRef { + if in == nil { + return nil + } + out := new(TLSIssuerRef) + in.DeepCopyInto(out) + return out +} From e47e3103fb802b95343ba71ce7ae9d01387dd3c6 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 20:25:27 +0300 Subject: [PATCH 18/22] test(redis): update TLS tests for redesigned service architecture Drop rfs-* SAN expectations (operator's plaintext service must not be TLS-reachable) and assert master/replicas/external-lb variants instead. Add service tests for the two new TLS-only ClusterIP services that replace rfs-* for tenant-facing TLS connections. Signed-off-by: Arsolitt --- .../apps/redis/tests/service-tls_test.yaml | 151 ++++++++++++++++-- .../redis/tests/tls-certificate_test.yaml | 26 ++- 2 files changed, 164 insertions(+), 13 deletions(-) diff --git a/packages/apps/redis/tests/service-tls_test.yaml b/packages/apps/redis/tests/service-tls_test.yaml index ebd1c1f6..86d5ef9d 100644 --- a/packages/apps/redis/tests/service-tls_test.yaml +++ b/packages/apps/redis/tests/service-tls_test.yaml @@ -1,17 +1,21 @@ -suite: External service TLS port switching +suite: TLS service architecture templates: - templates/service.yaml tests: - - it: no service when external is false + ################################################################### + # External LoadBalancer service (existing behavior, port switches) # + ################################################################### + + - it: no service when external is false and TLS is false set: external: false tls: - enabled: true + enabled: false asserts: - hasDocuments: count: 0 - - it: external true and TLS false renders port 6379 plaintext + - it: external true and TLS false renders LB on port 6379 plaintext set: external: true tls: @@ -19,24 +23,39 @@ tests: asserts: - hasDocuments: count: 1 - - equal: + - documentIndex: 0 + equal: + path: metadata.name + value: RELEASE-NAME-external-lb + - documentIndex: 0 + equal: + path: spec.type + value: LoadBalancer + - documentIndex: 0 + equal: path: spec.ports[0].port value: 6379 - - equal: + - documentIndex: 0 + equal: path: spec.ports[0].name value: redis - - equal: + - documentIndex: 0 + equal: path: spec.ports[0].targetPort value: redis - - it: external true and TLS true renders port 6380 TLS + - it: external true and TLS true renders LB on port 6380 TLS set: external: true tls: enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-external-lb asserts: - - hasDocuments: - count: 1 + - equal: + path: spec.type + value: LoadBalancer - equal: path: spec.ports[0].port value: 6380 @@ -46,3 +65,115 @@ tests: - equal: path: spec.ports[0].targetPort value: redis-tls + + ################################################################### + # Internal ClusterIP services (only when TLS enabled) # + ################################################################### + + - it: no master or replicas service when TLS disabled (external true) + set: + external: true + tls: + enabled: false + asserts: + - hasDocuments: + count: 1 + - documentIndex: 0 + equal: + path: metadata.name + value: RELEASE-NAME-external-lb + + - it: no services when TLS disabled and external false + set: + external: false + tls: + enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: TLS enabled external false renders two ClusterIP services and no LB + set: + external: false + tls: + enabled: true + asserts: + - hasDocuments: + count: 2 + + - it: TLS enabled external true renders three services (master + replicas + LB) + set: + external: true + tls: + enabled: true + asserts: + - hasDocuments: + count: 3 + + ################### + # Master service # + ################### + + - it: master service is a TLS ClusterIP on port 6380 targeting redis-tls + set: + external: false + tls: + enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-master + asserts: + - isKind: + of: Service + - equal: + path: spec.type + value: ClusterIP + - equal: + path: spec.ports[0].port + value: 6380 + - equal: + path: spec.ports[0].name + value: redis-tls + - equal: + path: spec.ports[0].targetPort + value: redis-tls + - equal: + path: spec.selector["redisfailovers-role"] + value: master + - equal: + path: spec.selector["app.kubernetes.io/component"] + value: redis + + #################### + # Replicas service # + #################### + + - it: replicas service is a TLS ClusterIP on port 6380 selecting slave role + set: + external: false + tls: + enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-replicas + asserts: + - isKind: + of: Service + - equal: + path: spec.type + value: ClusterIP + - equal: + path: spec.ports[0].port + value: 6380 + - equal: + path: spec.ports[0].name + value: redis-tls + - equal: + path: spec.ports[0].targetPort + value: redis-tls + - equal: + path: spec.selector["redisfailovers-role"] + value: slave + - equal: + path: spec.selector["app.kubernetes.io/component"] + value: redis diff --git a/packages/apps/redis/tests/tls-certificate_test.yaml b/packages/apps/redis/tests/tls-certificate_test.yaml index dbb689cb..0fc7bd3f 100644 --- a/packages/apps/redis/tests/tls-certificate_test.yaml +++ b/packages/apps/redis/tests/tls-certificate_test.yaml @@ -46,11 +46,31 @@ tests: asserts: - equal: { path: spec.issuerRef.name, value: letsencrypt-prod } - - it: dnsNames include external-lb and rfs- service variants + - it: dnsNames include master, replicas and external-lb service variants set: tls: enabled: true asserts: + - contains: { path: spec.dnsNames, content: RELEASE-NAME-master } + - contains: { path: spec.dnsNames, content: RELEASE-NAME-master.NAMESPACE.svc.cluster.local } + - contains: { path: spec.dnsNames, content: RELEASE-NAME-replicas } + - contains: { path: spec.dnsNames, content: RELEASE-NAME-replicas.NAMESPACE.svc.cluster.local } - contains: { path: spec.dnsNames, content: RELEASE-NAME-external-lb } - - contains: { path: spec.dnsNames, content: rfs-RELEASE-NAME } - - contains: { path: spec.dnsNames, content: rfs-RELEASE-NAME.NAMESPACE.svc.cluster.local } + - contains: { path: spec.dnsNames, content: RELEASE-NAME-external-lb.NAMESPACE.svc.cluster.local } + + - it: dnsNames do not contain rfs- operator plaintext service + set: + tls: + enabled: true + asserts: + - notContains: { path: spec.dnsNames, content: rfs-RELEASE-NAME } + - notContains: { path: spec.dnsNames, content: rfs-RELEASE-NAME.NAMESPACE.svc.cluster.local } + + - it: dnsNames include external-lb variants even when external is false + set: + external: false + tls: + enabled: true + asserts: + - contains: { path: spec.dnsNames, content: RELEASE-NAME-external-lb } + - contains: { path: spec.dnsNames, content: RELEASE-NAME-external-lb.NAMESPACE.svc.cluster.local } From 7cc892163b9408ca17883c305027777ff6849647 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 20:25:51 +0300 Subject: [PATCH 19/22] refactor(redis): update TLS DNS SAN helper for redesigned services Switch redis.tls.dnsNames from rfs-* (plaintext) entries to the new master and replicas ClusterIP services that serve TLS on 6380. Keep external-lb variants unconditionally so flipping the external flag does not require certificate reissue. Signed-off-by: Arsolitt --- packages/apps/redis/templates/_tls.tpl | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/apps/redis/templates/_tls.tpl b/packages/apps/redis/templates/_tls.tpl index fc90a011..a5cbc5a7 100644 --- a/packages/apps/redis/templates/_tls.tpl +++ b/packages/apps/redis/templates/_tls.tpl @@ -12,16 +12,24 @@ Returns either the user-provided tls.secretName or "-redis-tls". {{/* redis.tls.dnsNames — DNS SANs for the Redis TLS certificate. -Covers the external LoadBalancer service and the operator-managed master -service (rfs-) across short, namespaced, and FQDN forms. +Covers the chart's TLS-only ClusterIP services (-master for writes, +-replicas for reads) and the external LoadBalancer service across +short, namespaced, and FQDN forms. The operator-managed rfs-* service is +intentionally excluded because it serves plaintext on 6379 only. +The external-lb variants are always included so toggling .Values.external +later does not require certificate reissue. */}} {{- define "redis.tls.dnsNames" -}} +- {{ .Release.Name }}-master +- {{ .Release.Name }}-master.{{ .Release.Namespace }} +- {{ .Release.Name }}-master.{{ .Release.Namespace }}.svc +- {{ .Release.Name }}-master.{{ .Release.Namespace }}.svc.cluster.local +- {{ .Release.Name }}-replicas +- {{ .Release.Name }}-replicas.{{ .Release.Namespace }} +- {{ .Release.Name }}-replicas.{{ .Release.Namespace }}.svc +- {{ .Release.Name }}-replicas.{{ .Release.Namespace }}.svc.cluster.local - {{ .Release.Name }}-external-lb - {{ .Release.Name }}-external-lb.{{ .Release.Namespace }} - {{ .Release.Name }}-external-lb.{{ .Release.Namespace }}.svc - {{ .Release.Name }}-external-lb.{{ .Release.Namespace }}.svc.cluster.local -- rfs-{{ .Release.Name }} -- rfs-{{ .Release.Name }}.{{ .Release.Namespace }} -- rfs-{{ .Release.Name }}.{{ .Release.Namespace }}.svc -- rfs-{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local {{- end -}} From 8048868c6ebbb7cc5201844991b454e800cb8239 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 20:26:14 +0300 Subject: [PATCH 20/22] feat(redis): align TLS Certificate SANs with new service architecture Replace rfs-* SAN entries with master/replicas variants and keep external-lb. The Certificate now matches the redesigned helper and covers every TLS endpoint that tenants are expected to connect to. Signed-off-by: Arsolitt --- packages/apps/redis/templates/tls-certificate.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/apps/redis/templates/tls-certificate.yaml b/packages/apps/redis/templates/tls-certificate.yaml index 7a0e8229..2c091acf 100644 --- a/packages/apps/redis/templates/tls-certificate.yaml +++ b/packages/apps/redis/templates/tls-certificate.yaml @@ -1,13 +1,17 @@ {{- if and .Values.tls.enabled (not .Values.tls.secretName) }} {{- $dnsNames := list -}} +{{- $dnsNames = append $dnsNames (printf "%s-master" .Release.Name) -}} +{{- $dnsNames = append $dnsNames (printf "%s-master.%s" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-master.%s.svc" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-master.%s.svc.cluster.local" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-replicas" .Release.Name) -}} +{{- $dnsNames = append $dnsNames (printf "%s-replicas.%s" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-replicas.%s.svc" .Release.Name .Release.Namespace) -}} +{{- $dnsNames = append $dnsNames (printf "%s-replicas.%s.svc.cluster.local" .Release.Name .Release.Namespace) -}} {{- $dnsNames = append $dnsNames (printf "%s-external-lb" .Release.Name) -}} {{- $dnsNames = append $dnsNames (printf "%s-external-lb.%s" .Release.Name .Release.Namespace) -}} {{- $dnsNames = append $dnsNames (printf "%s-external-lb.%s.svc" .Release.Name .Release.Namespace) -}} {{- $dnsNames = append $dnsNames (printf "%s-external-lb.%s.svc.cluster.local" .Release.Name .Release.Namespace) -}} -{{- $dnsNames = append $dnsNames (printf "rfs-%s" .Release.Name) -}} -{{- $dnsNames = append $dnsNames (printf "rfs-%s.%s" .Release.Name .Release.Namespace) -}} -{{- $dnsNames = append $dnsNames (printf "rfs-%s.%s.svc" .Release.Name .Release.Namespace) -}} -{{- $dnsNames = append $dnsNames (printf "rfs-%s.%s.svc.cluster.local" .Release.Name .Release.Namespace) -}} {{- include "cozy-lib.tls.certificate" (dict "Release" .Release "name" (printf "%s-redis-tls" .Release.Name) From e9e74a112ca9bef77e128a074349cb9064097b22 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 20:26:40 +0300 Subject: [PATCH 21/22] feat(redis): add TLS-only ClusterIP services for master and replicas Render -master and -replicas ClusterIP services on port 6380 whenever tls.enabled. Master selects redisfailovers-role=master (writes), replicas selects redisfailovers-role=slave (reads). Tenants use these as the in-cluster TLS endpoints; the operator-managed rfs-* service stays plaintext on 6379 for sentinel and replication. Signed-off-by: Arsolitt --- packages/apps/redis/templates/service.yaml | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/apps/redis/templates/service.yaml b/packages/apps/redis/templates/service.yaml index 24324337..321bf204 100644 --- a/packages/apps/redis/templates/service.yaml +++ b/packages/apps/redis/templates/service.yaml @@ -1,3 +1,47 @@ +{{- if .Values.tls.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-master + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/component: redis +spec: + type: ClusterIP + selector: + app.kubernetes.io/component: redis + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: redis-failover + redisfailovers-role: master + ports: + - name: redis-tls + port: 6380 + targetPort: redis-tls + protocol: TCP +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-replicas + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/component: redis +spec: + type: ClusterIP + selector: + app.kubernetes.io/component: redis + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/part-of: redis-failover + redisfailovers-role: slave + ports: + - name: redis-tls + port: 6380 + targetPort: redis-tls + protocol: TCP +{{- end }} {{- if .Values.external }} --- apiVersion: v1 From b2225140666f0d647aa1e9a49d09757d36ad5a37 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Wed, 29 Apr 2026 20:27:02 +0300 Subject: [PATCH 22/22] docs(redis): document TLS limitations and connection patterns Spell out which endpoints serve TLS (master/replicas ClusterIP on 6380), why Sentinel-aware clients cannot use TLS today, and how applications should connect to keep automatic master failover under TLS. Signed-off-by: Arsolitt --- packages/apps/redis/values.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/apps/redis/values.yaml b/packages/apps/redis/values.yaml index 0dd7c9ea..736c3c75 100644 --- a/packages/apps/redis/values.yaml +++ b/packages/apps/redis/values.yaml @@ -56,6 +56,11 @@ authEnabled: true ## @field {string} secretName - Name of an existing kubernetes.io/tls secret. When non-empty, cert-manager Certificate is NOT rendered and the user manages the secret. ## @field {TLSIssuerRef} issuerRef - cert-manager Issuer reference. Used only when secretName is empty. +## TLS limitations: +## - Internal connections must use -master:6380 (writes) or -replicas:6380 (reads). Direct connection only — no Sentinel-aware service discovery over TLS. +## - Sentinel HA clients (rfs-*:26379) operate on plaintext and resolve master IP as :6379 (the operator hardcodes the plaintext port). To use Sentinel-aware clients with TLS, the operator would need patching — out of scope. +## - For applications that need automatic master failover with TLS: use -master:6380 (the K8s Service's selector follows the master role label, so it auto-routes to the new master after failover). + ## @param {TLS} tls - TLS configuration for client-facing connections. When enabled, an nginx stream sidecar terminates TLS on port 6380. Intra-cluster traffic on 6379 (operator probes, sentinel, replication) remains plaintext. tls: enabled: false