diff --git a/charts/cloudflared/templates/NOTES.txt b/charts/cloudflared/templates/NOTES.txt index ea0feceb1..08e37db4b 100644 --- a/charts/cloudflared/templates/NOTES.txt +++ b/charts/cloudflared/templates/NOTES.txt @@ -1,9 +1,8 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} -cloudflared has been installed -============================== +cloudflared has been installed. -Installation summary --------------------- +1. Installation summary +----------------------- Release: {{ .Release.Name }} Namespace: {{ .Release.Namespace }} Chart version: {{ .Chart.Version }} @@ -14,8 +13,8 @@ Quick tunnel: {{ ternary "enabled" "disabled" .Values.tunnel.quickTunnel.enabled Metrics service: {{ ternary "enabled" "disabled" .Values.metrics.enabled }} ServiceMonitor: {{ ternary "enabled" "disabled" .Values.serviceMonitor.enabled }} -Tunnel status -------------- +2. Tunnel status +---------------- cloudflared creates outbound-only connections from this cluster to Cloudflare. No inbound Kubernetes Service or LoadBalancer is required for application traffic. Public hostnames and origins are configured in the Cloudflare Zero @@ -29,14 +28,9 @@ Follow connection logs: kubectl logs -l app.kubernetes.io/name=cloudflared -n {{ .Release.Namespace }} --all-containers --tail=50 -f -Look for established tunnel connections: - - kubectl logs -l app.kubernetes.io/name=cloudflared -n {{ .Release.Namespace }} --all-containers | grep -i "connection\|tunnel\|registered" - +3. Metrics +---------- {{- if .Values.metrics.enabled }} - -Metrics -------- Internal metrics endpoint: http://{{ include "cloudflared.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port | default 2000 }}/metrics @@ -44,10 +38,12 @@ Internal metrics endpoint: Port-forward metrics locally: kubectl port-forward svc/{{ include "cloudflared.fullname" . }} -n {{ .Release.Namespace }} 2000:{{ .Values.service.port | default 2000 }} +{{- else }} +Metrics Service rendering is disabled. {{- end }} -Credentials ------------ +4. Credentials +-------------- {{- if .Values.tunnel.quickTunnel.enabled }} Quick tunnel mode does not use a managed tunnel token. Use it only for demos or smoke tests. @@ -68,8 +64,16 @@ Tunnel token was provided through chart-managed Secret data. Prefer an existing Secret or External Secrets Operator for production. {{- end }} -Troubleshooting ---------------- +5. High availability +-------------------- +Replicas: {{ .Values.replicaCount }} +PodDisruptionBudget: {{ ternary "enabled" "disabled" .Values.pdb.enabled }} +{{- if .Values.pdb.enabled }} +PDB minAvailable: {{ .Values.pdb.minAvailable }} +{{- end }} + +6. Troubleshooting +------------------ Describe pods: kubectl describe pod -l app.kubernetes.io/name=cloudflared -n {{ .Release.Namespace }} @@ -82,15 +86,15 @@ Inspect recent events: kubectl get events -n {{ .Release.Namespace }} --sort-by=.lastTimestamp -Cloudflare routing ------------------- +7. Cloudflare routing +--------------------- Configure Public Hostnames in the Cloudflare Zero Trust dashboard and point origins to Kubernetes DNS names such as: http://service.namespace.svc.cluster.local:80 -Documentation -------------- +8. Resources +------------ Chart docs: https://helmforge.dev/docs/charts/cloudflared Chart source: https://github.com/helmforgedev/charts/tree/main/charts/cloudflared Cloudflare Tunnel docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/ diff --git a/charts/cloudflared/templates/_helpers.tpl b/charts/cloudflared/templates/_helpers.tpl index ce0ba3d4b..cd56c0819 100644 --- a/charts/cloudflared/templates/_helpers.tpl +++ b/charts/cloudflared/templates/_helpers.tpl @@ -43,6 +43,37 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- printf "%s:%s" .Values.image.repository .Values.image.tag -}} {{- end -}} +{{- define "cloudflared.validate" -}} +{{- if and (not .Values.tunnel.quickTunnel.enabled) (not .Values.tunnel.token) (not .Values.tunnel.existingSecret) -}} +{{- fail "tunnel.token or tunnel.existingSecret is required when tunnel.quickTunnel.enabled is false" -}} +{{- end -}} +{{- if and .Values.tunnel.quickTunnel.enabled (not .Values.tunnel.quickTunnel.helloWorld) (not .Values.tunnel.quickTunnel.url) -}} +{{- fail "tunnel.quickTunnel.url is required when tunnel.quickTunnel.enabled=true and helloWorld=false" -}} +{{- end -}} +{{- if and .Values.externalSecrets.enabled (not .Values.tunnel.existingSecret) -}} +{{- fail "externalSecrets.enabled requires tunnel.existingSecret to be set to prevent credential drift between the chart-managed Secret and the ExternalSecret." -}} +{{- end -}} +{{- if and .Values.externalSecrets.enabled (not .Values.externalSecrets.secretStoreRef.name) -}} +{{- fail "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" -}} +{{- end -}} +{{- if and .Values.externalSecrets.enabled (not .Values.externalSecrets.data) -}} +{{- fail "externalSecrets.data must not be empty when externalSecrets.enabled=true" -}} +{{- end -}} +{{- if and .Values.serviceMonitor.enabled (not .Values.metrics.enabled) -}} +{{- fail "metrics.enabled must be true when serviceMonitor.enabled is true" -}} +{{- end -}} +{{- if and .Values.pdb.enabled (lt (int .Values.replicaCount) (int .Values.pdb.minAvailable)) -}} +{{- fail "replicaCount must be greater than or equal to pdb.minAvailable when pdb.enabled is true" -}} +{{- end -}} +{{- $podLabels := .Values.podLabels | default dict -}} +{{- $selectorLabels := include "cloudflared.selectorLabels" . | fromYaml -}} +{{- range $key, $_ := $selectorLabels -}} +{{- if hasKey $podLabels $key -}} +{{- fail (printf "podLabels must not override selector label %q" $key) -}} +{{- end -}} +{{- end -}} +{{- end -}} + {{/* Tunnel token secret name */}} {{- define "cloudflared.tunnelSecretName" -}} {{- if .Values.tunnel.existingSecret -}} diff --git a/charts/cloudflared/templates/validate.yaml b/charts/cloudflared/templates/validate.yaml new file mode 100644 index 000000000..d72bfa1d2 --- /dev/null +++ b/charts/cloudflared/templates/validate.yaml @@ -0,0 +1,2 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- include "cloudflared.validate" . -}} diff --git a/charts/cloudflared/tests/validation_test.yaml b/charts/cloudflared/tests/validation_test.yaml new file mode 100644 index 000000000..101f96f09 --- /dev/null +++ b/charts/cloudflared/tests/validation_test.yaml @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: Validation +templates: + - templates/validate.yaml +tests: + - it: should render no validation manifest by default + asserts: + - hasDocuments: + count: 0 + + - it: should fail when managed tunnel has no credentials + set: + tunnel.quickTunnel.enabled: false + asserts: + - failedTemplate: + errorMessage: "tunnel.token or tunnel.existingSecret is required when tunnel.quickTunnel.enabled is false" + + - it: should fail when quick tunnel url mode has no url + set: + tunnel.quickTunnel.enabled: true + tunnel.quickTunnel.helloWorld: false + tunnel.quickTunnel.url: "" + asserts: + - failedTemplate: + errorMessage: "tunnel.quickTunnel.url is required when tunnel.quickTunnel.enabled=true and helloWorld=false" + + - it: should fail when external secrets has no target secret + set: + externalSecrets.enabled: true + externalSecrets.secretStoreRef.name: helmforge-fake-store + externalSecrets.data: + - secretKey: token + remoteRef: + key: test/password + asserts: + - failedTemplate: + errorMessage: "externalSecrets.enabled requires tunnel.existingSecret to be set to prevent credential drift between the chart-managed Secret and the ExternalSecret." + + - it: should fail when external secrets has no store reference + set: + tunnel.existingSecret: cloudflared-tunnel-token + externalSecrets.enabled: true + externalSecrets.data: + - secretKey: token + remoteRef: + key: test/password + asserts: + - failedTemplate: + errorMessage: "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" + + - it: should fail when external secrets has no data mappings + set: + tunnel.existingSecret: cloudflared-tunnel-token + externalSecrets.enabled: true + externalSecrets.secretStoreRef.name: helmforge-fake-store + asserts: + - failedTemplate: + errorMessage: "externalSecrets.data must not be empty when externalSecrets.enabled=true" + + - it: should fail when service monitor has no metrics service + set: + metrics.enabled: false + serviceMonitor.enabled: true + asserts: + - failedTemplate: + errorMessage: "metrics.enabled must be true when serviceMonitor.enabled is true" + + - it: should fail when pdb minAvailable exceeds replicas + set: + replicaCount: 1 + pdb.enabled: true + pdb.minAvailable: 2 + asserts: + - failedTemplate: + errorMessage: "replicaCount must be greater than or equal to pdb.minAvailable when pdb.enabled is true" + + - it: should fail when podLabels override selector labels + set: + podLabels: + app.kubernetes.io/name: custom + asserts: + - failedTemplate: + errorMessage: 'podLabels must not override selector label "app.kubernetes.io/name"' + + - it: should fail when podLabels override release selector labels + set: + podLabels: + app.kubernetes.io/instance: custom + asserts: + - failedTemplate: + errorMessage: 'podLabels must not override selector label "app.kubernetes.io/instance"'