Compare commits

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

7 commits

Author SHA1 Message Date
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
14 changed files with 648 additions and 0 deletions

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

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