diff --git a/packages/library/cozy-lib/templates/_tls.tpl b/packages/library/cozy-lib/templates/_tls.tpl new file mode 100644 index 00000000..310ccfbb --- /dev/null +++ b/packages/library/cozy-lib/templates/_tls.tpl @@ -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 }} 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/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/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/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 new file mode 100644 index 00000000..3d38d7dc --- /dev/null +++ b/packages/tests/cozy-lib-tests/templates/tests/tls.yaml @@ -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 }} 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_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_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").' 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..94386838 --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_secretname_test.yaml @@ -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 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..a1b33587 --- /dev/null +++ b/packages/tests/cozy-lib-tests/tests/tls_test.yaml @@ -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 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: {}