Compare commits

...
Sign in to create a new pull request.

23 commits

Author SHA1 Message Date
Arsolitt
b222514066
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 <arsolitt@gmail.com>
2026-04-29 20:27:02 +03:00
Arsolitt
e9e74a112c
feat(redis): add TLS-only ClusterIP services for master and replicas
Render <release>-master and <release>-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 <arsolitt@gmail.com>
2026-04-29 20:26:40 +03:00
Arsolitt
8048868c6e
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 <arsolitt@gmail.com>
2026-04-29 20:26:14 +03:00
Arsolitt
7cc892163b
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 <arsolitt@gmail.com>
2026-04-29 20:25:51 +03:00
Arsolitt
e47e3103fb
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 <arsolitt@gmail.com>
2026-04-29 20:25:27 +03:00
Arsolitt
61b8410321
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 <arsolitt@gmail.com>
2026-04-29 19:53:06 +03:00
Arsolitt
125bd116ec
feat(redis-rd): include TLS secret in dashboard secrets list
Allow the dashboard to read the auto-generated `redis-<name>-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 <arsolitt@gmail.com>
2026-04-29 19:39:41 +03:00
Arsolitt
a8fc515649
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 <arsolitt@gmail.com>
2026-04-29 19:39:11 +03:00
Arsolitt
94663e3094
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 <arsolitt@gmail.com>
2026-04-29 19:36:43 +03:00
Arsolitt
08d2b065e1
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 <arsolitt@gmail.com>
2026-04-29 19:36:30 +03:00
Arsolitt
47936637b6
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 <arsolitt@gmail.com>
2026-04-29 19:36:16 +03:00
Arsolitt
9102d8094a
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 <arsolitt@gmail.com>
2026-04-29 19:35:59 +03:00
Arsolitt
a18bfc3f91
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-<release>).

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 19:35:37 +03:00
Arsolitt
857ebd0420
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 <arsolitt@gmail.com>
2026-04-29 19:35:14 +03:00
Arsolitt
d482d1e56a
test(redis): add helm unit tests for TLS support
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 19:32:55 +03:00
Arsolitt
3dfde08242
Merge branch 'feat/cozy-lib-tls' into feat/redis-tls
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 19:28:50 +03:00
Arsolitt
cdc0e21831
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 <arsolitt@gmail.com>
2026-04-24 15:43:23 +03:00
Arsolitt
6dca3f52b6
fix(cozy-lib-tls): add issuerRef.kind enum validation, Release guard, and improve docs
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:12:15 +03:00
Arsolitt
ab6d7ad6d5
fix(cozy-lib-tls): add secretName validation and fix misleading test name
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:06:57 +03:00
Arsolitt
2aa4863cdd
fix(cozy-lib-tls): fix ternary argument order and strengthen input validation
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 13:56:35 +03:00
Arsolitt
2e1fc38405
fix(cozy-lib-tls): fix empty string defeating duration/renewBefore defaults and add missing tests
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 13:37:14 +03:00
Arsolitt
cb932d17f4
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 <vladimir.safronov@aenix.io>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 12:34:29 +03:00
Arsolitt
325132d48e
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 <vladimir.safronov@aenix.io>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 12:34:29 +03:00
31 changed files with 1381 additions and 5 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -0,0 +1,35 @@
{{/*
redis.tls.secretName — wraps cozy-lib.tls.secretName with redis-specific suffix.
Returns either the user-provided tls.secretName or "<release>-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 chart's TLS-only ClusterIP services (<release>-master for writes,
<release>-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
{{- end -}}

View file

@ -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

View file

@ -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

View file

@ -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
@ -21,7 +65,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 }}

View file

@ -0,0 +1,22 @@
{{- 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) -}}
{{- 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 }}

View file

@ -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 }}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,179 @@
suite: TLS service architecture
templates:
- templates/service.yaml
tests:
###################################################################
# External LoadBalancer service (existing behavior, port switches) #
###################################################################
- it: no service when external is false and TLS is false
set:
external: false
tls:
enabled: false
asserts:
- hasDocuments:
count: 0
- it: external true and TLS false renders LB on port 6379 plaintext
set:
external: true
tls:
enabled: false
asserts:
- hasDocuments:
count: 1
- 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
- documentIndex: 0
equal:
path: spec.ports[0].name
value: redis
- documentIndex: 0
equal:
path: spec.ports[0].targetPort
value: redis
- 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:
- equal:
path: spec.type
value: LoadBalancer
- 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
###################################################################
# 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

View file

@ -0,0 +1,76 @@
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 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: 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 }

View file

@ -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"

View file

@ -91,6 +91,49 @@
"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": ""
}
}
}
}
}

View file

@ -46,3 +46,25 @@ version: v8
## @param {bool} authEnabled - Enable password generation.
authEnabled: true
## @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 {TLSIssuerRef} issuerRef - cert-manager Issuer reference. Used only when secretName is empty.
## TLS limitations:
## - Internal connections must use <release>-master:6380 (writes) or <release>-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 <ip>: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 <release>-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
secretName: ""
issuerRef:
name: "selfsigned-cluster-issuer"
kind: "ClusterIssuer"

View file

@ -0,0 +1,110 @@
{{/*
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). 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"] (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 -}}
{{- 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 -}}
{{- 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 -}}
{{- 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:
name: {{ .name }}
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
secretName: {{ .secretName }}
duration: {{ ternary "8760h" .duration (empty .duration) }}
renewBefore: {{ ternary "720h" .renewBefore (empty .renewBefore) }}
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 }}
{{/*
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.
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 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 -}}
{{- printf "%s-%s" .Release.Name .suffix -}}
{{- end -}}
{{- end }}

View file

@ -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,12 +25,13 @@ 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:
- resourceNames:
- redis-{{ .name }}-auth
- redis-{{ .name }}-redis-tls
services:
exclude: []
include:

View file

@ -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 -}}

View file

@ -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 -}}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -0,0 +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 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 }}

View file

@ -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.'

View file

@ -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.'

View file

@ -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.'

View file

@ -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").'

View file

@ -0,0 +1,46 @@
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
- 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

View file

@ -0,0 +1,350 @@
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 is passed through to Certificate CR
- it: passes issuerRef kind to Certificate CR
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
# 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 triggers a validation error
- it: errors when dnsNames is empty
release:
name: test-release
namespace: tenant-test
set:
_cluster: {}
_namespace: {}
tls:
name: empty-dns-tls
dnsNames: []
issuerRef:
name: selfsigned-cluster-issuer
kind: ClusterIssuer
asserts:
- failedTemplate:
errorMessage: 'ERROR: "dnsNames" is required for cozy-lib.tls.certificate. Provide at least one DNS name as a list.'
# Missing issuerRef name triggers a validation error
- it: errors when issuerRef name is not provided
release:
name: test-release
namespace: tenant-test
set:
_cluster: {}
_namespace: {}
tls:
name: missing-issuer-tls
dnsNames:
- svc.example
issuerRef:
kind: ClusterIssuer
asserts:
- failedTemplate:
errorMessage: 'ERROR: "issuerRef.name" is required for cozy-lib.tls.certificate. Specify the ClusterIssuer or Issuer name.'
# Missing issuerRef kind triggers a validation error
- it: errors when issuerRef kind is 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:
- 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:
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.duration
value: 8760h
- equal:
path: spec.renewBefore
value: 720h

View file

@ -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: {}