Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions .ci/test-certificate-san-modes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
#!/usr/bin/env bash
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#

set -euo pipefail

chart_dir=${1:-charts/pulsar}
helm_bin=${HELM_BIN:-helm}
release_name=myrelease
namespace=mynamespace
components=(proxy broker function-worker bookie recovery toolset zookeeper)

render_certificates() {
local san_mode=$1

"$helm_bin" template "$release_name" "$chart_dir" \
--namespace "$namespace" \
--show-only templates/tls-certs-internal.yaml \
--set certs.internal_issuer.enabled=true \
--set tls.enabled=true \
--set tls.common.sanMode="$san_mode" \
--set tls.proxy.enabled=true \
--set tls.broker.enabled=true \
--set tls.bookie.enabled=true \
--set tls.zookeeper.enabled=true \
--set tls.function_worker.enabled=true \
--set components.function_worker=true
}

render_standalone_certificate() {
local san_mode=$1

"$helm_bin" template "$release_name" "$chart_dir" \
--namespace "$namespace" \
--show-only templates/tls-certs-internal.yaml \
--set certs.internal_issuer.enabled=true \
--set standalone.enabled=true \
--set tls.enabled=true \
--set tls.common.sanMode="$san_mode"
}

render_renamed_certificates() {
"$helm_bin" template "$release_name" "$chart_dir" \
--namespace "$namespace" \
--show-only templates/tls-certs-internal.yaml \
--set certs.internal_issuer.enabled=true \
--set tls.enabled=true \
--set tls.proxy.enabled=true \
--set tls.broker.enabled=true \
--set tls.bookie.enabled=true \
--set tls.zookeeper.enabled=true \
--set tls.function_worker.enabled=true \
--set components.function_worker=true \
--set tls.common.sanMode=fqdn \
--set proxy.component=renamed-proxy \
--set broker.component=renamed-broker \
--set function_worker.component=renamed-function-worker \
--set bookkeeper.component=renamed-bookie \
--set autorecovery.component=renamed-recovery \
--set toolset.component=renamed-toolset \
--set zookeeper.component=renamed-zookeeper
}

render_renamed_standalone_certificate() {
"$helm_bin" template "$release_name" "$chart_dir" \
--namespace "$namespace" \
--show-only templates/tls-certs-internal.yaml \
--set certs.internal_issuer.enabled=true \
--set standalone.enabled=true \
--set tls.enabled=true \
--set tls.common.sanMode=fqdn \
--set standalone.component=renamed-standalone
}

render_autoscaled_broker_certificate() {
"$helm_bin" template "$release_name" "$chart_dir" \
--namespace "$namespace" \
--show-only templates/tls-certs-internal.yaml \
--set certs.internal_issuer.enabled=true \
--set tls.enabled=true \
--set tls.broker.enabled=true \
--set tls.common.sanMode=fqdn \
--set broker.autoscaling.enabled=true \
--set broker.autoscaling.maxReplicas=5
}

certificate() {
local certificate_name=$1
awk -v certificate_name="$certificate_name" '
{ sub(/\r$/, "") }
/^---$/ { in_certificate = 0 }
$0 == " name: \"" certificate_name "\"" { in_certificate = 1 }
in_certificate { print }
'
}

assert_contains() {
local content=$1
local expected=$2
local description=$3

if ! grep -Fq -- "$expected" <<<"$content"; then
echo "Expected $description to contain: $expected" >&2
exit 1
fi
}

assert_not_contains() {
local content=$1
local unexpected=$2
local description=$3

if grep -Fq -- "$unexpected" <<<"$content"; then
echo "Expected $description not to contain: $unexpected" >&2
exit 1
fi
}

service_name() {
local component_key=$1
local component=${2:-$component_key}

case "$component_key" in
broker|zookeeper|function-worker)
printf '%s-pulsar-%s-headless' "$release_name" "$component"
;;
*)
printf '%s-pulsar-%s' "$release_name" "$component"
;;
esac
}

for san_mode in wildcard fqdn none; do
rendered=$(render_certificates "$san_mode")
rendered+=$'\n'
rendered+=$(render_standalone_certificate "$san_mode")

for component in "${components[@]}" standalone; do
certificate_name="$release_name-pulsar-tls-$component"
certificate_manifest=$(certificate "$certificate_name" <<<"$rendered")
service="$release_name-pulsar-$component"
service_fqdn="$service.$namespace.svc.cluster.local"

assert_contains "$certificate_manifest" "name: \"$certificate_name\"" "$component certificate"
assert_contains "$certificate_manifest" "\"$service_fqdn\"" "$component $san_mode SANs"
assert_contains "$certificate_manifest" "\"$service\"" "$component $san_mode SANs"

case "$san_mode:$component" in
wildcard:proxy|wildcard:standalone|wildcard:function-worker)
assert_contains "$certificate_manifest" "\"*.$service.$namespace.svc.cluster.local\"" "$component wildcard SANs"
;;
wildcard:*)
assert_contains "$certificate_manifest" "\"*.$(service_name "$component").$namespace.svc.cluster.local\"" "$component wildcard SANs"
;;
fqdn:proxy)
assert_not_contains "$certificate_manifest" "-$component-0." "$component FQDN SANs"
;;
fqdn:standalone)
assert_not_contains "$certificate_manifest" "-$component-0." "$component FQDN SANs"
assert_contains "$certificate_manifest" "\"$component.$release_name-pulsar-$component-headless.$namespace.svc.cluster.local\"" "$component FQDN SANs"
;;
fqdn:*)
assert_contains "$certificate_manifest" "\"$service-0.$(service_name "$component").$namespace.svc.cluster.local\"" "$component FQDN SANs"
;;
none:*)
assert_not_contains "$certificate_manifest" "\"*." "$component none SANs"
assert_not_contains "$certificate_manifest" "-$component-0." "$component none SANs"
;;
esac
done
done

renamed_certificates=$(render_renamed_certificates)
renamed_certificates+=$'\n'
renamed_certificates+=$(render_renamed_standalone_certificate)

for component in "${components[@]}" standalone; do
certificate_name="$release_name-pulsar-tls-$component"
certificate_manifest=$(certificate "$certificate_name" <<<"$renamed_certificates")
renamed_component="renamed-$component"
renamed_service="$release_name-pulsar-$renamed_component"

assert_contains "$certificate_manifest" \
"\"$renamed_service.$namespace.svc.cluster.local\"" \
"renamed $component service SANs"

case "$component" in
proxy|standalone)
assert_not_contains "$certificate_manifest" "-$renamed_component-0." "renamed $component FQDN SANs"
;;
*)
assert_contains "$certificate_manifest" \
"\"$renamed_service-0.$(service_name "$component" "$renamed_component").$namespace.svc.cluster.local\"" \
"renamed $component FQDN SANs"
;;
esac
done

autoscaled_broker_certificate=$(render_autoscaled_broker_certificate | certificate "$release_name-pulsar-tls-broker")
assert_contains "$autoscaled_broker_certificate" \
"\"$release_name-pulsar-broker-4.$release_name-pulsar-broker-headless.$namespace.svc.cluster.local\"" \
"autoscaled broker FQDN SANs"
assert_not_contains "$autoscaled_broker_certificate" \
"\"$release_name-pulsar-broker-5.$release_name-pulsar-broker-headless.$namespace.svc.cluster.local\"" \
"autoscaled broker FQDN SANs"

echo "Certificate SAN mode rendering checks passed"
4 changes: 4 additions & 0 deletions .github/workflows/pulsar-helm-chart-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ jobs:
-strict -kubernetes-version $kube_version -summary "$render_dir_patch1"
echo "::endgroup::"

- name: Validate certificate SAN modes
if: ${{ steps.check_changes.outputs.docs_only != 'true' }}
run: bash .ci/test-certificate-san-modes.sh

- name: Save kubeconform schema cache
# save the cache even when validation failed so that already downloaded
# schemas don't get re-downloaded on the next attempt
Expand Down
94 changes: 83 additions & 11 deletions charts/pulsar/templates/_certs.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,73 @@ Define the pulsar certs ca issuer secret name
{{- end -}}
{{- end -}}

{{/*
Return the service name for a component.
Usage: {{ include "pulsar.certs.component.service" (dict "root" . "componentKey" "broker") }}
*/}}
{{- define "pulsar.certs.component.service" -}}
{{- $templateName := printf "pulsar.%s.service" .componentKey -}}
{{- include $templateName .root -}}
{{- end -}}

{{/*
Return the headless service name for a component.
Usage: {{ include "pulsar.certs.component.service.headless" (dict "root" . "componentKey" "broker") }}
*/}}
{{- define "pulsar.certs.component.service.headless" -}}
{{- $templateName := printf "pulsar.%s.service.headless" .componentKey -}}
{{- include $templateName .root -}}
{{- end -}}

{{/*
Return the component replica count.
When autoscaling is enabled, uses maxReplicas; otherwise uses replicaCount.
Usage: {{ include "pulsar.certs.component.replicaCount" (dict "componentKey" "broker" "componentConfig" .Values.broker) }}
*/}}
{{- define "pulsar.certs.component.replicaCount" -}}
{{- $componentKey := .componentKey -}}
{{- $componentConfig := .componentConfig -}}
{{- if and $componentConfig.autoscaling $componentConfig.autoscaling.enabled -}}
{{- if not $componentConfig.autoscaling.maxReplicas -}}
{{- fail (printf "%s.autoscaling.maxReplicas must be defined when %s.autoscaling.enabled is true" $componentKey $componentKey) -}}
{{- end -}}
{{- $componentConfig.autoscaling.maxReplicas -}}
{{- else -}}
{{- $componentConfig.replicaCount -}}
{{- end -}}
{{- end -}}

{{/*
Common certificate template
Usage: {{- include "pulsar.cert.template" (dict "root" . "componentConfig" .Values.proxy "tlsConfig" .Values.tls.proxy) -}}
Usage: {{- include "pulsar.cert.template" (dict "root" . "componentKey" "proxy" "componentConfig" .Values.proxy "tlsConfig" .Values.tls.proxy) -}}
*/}}
{{- define "pulsar.cert.template" -}}
{{- if eq .root.Values.certs.internal_issuer.apiVersion "cert-manager.io/v1beta1" -}}
{{- fail "cert-manager.io/v1beta1 is no longer supported. Please set certs.internal_issuer.apiVersion to cert-manager.io/v1" -}}
{{- end -}}
{{- $root := .root -}}
{{- $fullname := include "pulsar.fullname" .root -}}
{{- $component := .componentConfig.component -}}
{{- $namespace := include "pulsar.namespace" .root -}}
{{- $clusterDomain := .root.Values.clusterDomain -}}
{{- $service := include "pulsar.certs.component.service" (dict "root" .root "componentKey" .componentKey "component" $component) -}}
{{- $serviceHeadless := "" -}}
{{- $serviceDns := $service -}}
{{- if or (eq .componentKey "broker") (eq .componentKey "zookeeper") }}
{{- $serviceHeadless = include "pulsar.certs.component.service.headless" (dict "root" .root "componentKey" .componentKey "component" $component) -}}
{{- $serviceDns = $serviceHeadless -}}
{{- end -}}
Comment thread
lhotari marked this conversation as resolved.
{{- /* Per-pod FQDNs are under the service the pod's DNS records are published in: the StatefulSet's
serviceName, or the standalone Deployment's subdomain. These are headless services for broker, zookeeper,
function_worker and standalone. */ -}}
{{- $podServiceDns := $serviceDns -}}
{{- if or (eq .componentKey "function_worker") (eq .componentKey "standalone") }}
{{- $podServiceDns = include "pulsar.certs.component.service.headless" (dict "root" .root "componentKey" .componentKey "component" $component) -}}
{{- end -}}
{{- $sanMode := .root.Values.tls.common.sanMode -}}
{{- if not (has $sanMode (list "wildcard" "fqdn" "none")) -}}
{{- fail (printf "tls.common.sanMode must be one of: wildcard, fqdn, none (got %q)" $sanMode) -}}
{{- end -}}
apiVersion: "{{ .root.Values.certs.internal_issuer.apiVersion }}"
kind: Certificate
metadata:
Expand All @@ -92,7 +151,7 @@ spec:
{{ toYaml .root.Values.tls.common.organization | indent 4 }}
# The use of the common name field has been deprecated since 2000 and is
# discouraged from being used.
commonName: "{{ template "pulsar.fullname" .root }}-{{ .componentConfig.component }}"
commonName: "{{ template "pulsar.fullname" .root }}-{{ $component }}"
isCA: false
privateKey:
size: {{ .root.Values.tls.common.keySize }}
Expand All @@ -103,17 +162,30 @@ spec:
- client auth
# At least one of a DNS Name, USI SAN, or IP address is required.
dnsNames:
{{- if .tlsConfig.dnsNames }}
{{ if .tlsConfig.dnsNames }}
{{ toYaml .tlsConfig.dnsNames | indent 4 }}
{{ end }}
{{ if eq $sanMode "wildcard" }}
- {{ printf "*.%s.%s.svc.%s" $serviceDns $namespace $clusterDomain | quote }}
{{ end }}
{{/* The proxy uses a regular ClusterIP service, so it does not need per-pod FQDN SANs. */}}
{{ if and (eq $sanMode "fqdn") (not (eq .componentKey "proxy")) }}
{{- $replicaCount := (include "pulsar.certs.component.replicaCount" (dict "componentKey" .componentKey "componentConfig" .componentConfig) | int) -}}
{{- if gt $replicaCount 0 }}
{{- range $i := until $replicaCount }}
- {{ printf "%s-%s-%d.%s.%s.svc.%s" $fullname $component $i $podServiceDns $namespace $clusterDomain | quote }}
{{- end }}
{{- end }}
{{ end }}
{{- /* The standalone pod uses its component name as hostname in the headless service subdomain. */}}
{{- if and (eq $sanMode "fqdn") (eq .componentKey "standalone") }}
- {{ printf "%s.%s.%s.svc.%s" $component $podServiceDns $namespace $clusterDomain | quote }}
{{- end }}
{{- if or (eq .componentConfig.component "broker") (eq .componentConfig.component "zookeeper") }}
- {{ printf "*.%s-%s-headless.%s.svc.%s" (include "pulsar.fullname" .root) .componentConfig.component (include "pulsar.namespace" .root) .root.Values.clusterDomain | quote }}
- {{ printf "%s-%s-headless.%s.svc.%s" (include "pulsar.fullname" .root) .componentConfig.component (include "pulsar.namespace" .root) .root.Values.clusterDomain | quote }}
{{- else }}
- {{ printf "*.%s-%s.%s.svc.%s" (include "pulsar.fullname" .root) .componentConfig.component (include "pulsar.namespace" .root) .root.Values.clusterDomain | quote }}
{{- end }}
- {{ printf "%s-%s.%s.svc.%s" (include "pulsar.fullname" .root) .componentConfig.component (include "pulsar.namespace" .root) .root.Values.clusterDomain | quote }}
- {{ printf "%s-%s" (include "pulsar.fullname" .root) .componentConfig.component | quote }}
{{ if or (eq .componentKey "broker") (eq .componentKey "zookeeper") }}
- {{ printf "%s.%s.svc.%s" $serviceHeadless $namespace $clusterDomain | quote }}
{{ end }}
- {{ printf "%s.%s.svc.%s" $service $namespace $clusterDomain | quote }}
- {{ printf "%s" $service | quote }}
{{- if .tlsConfig.ipAddresses }}
ipAddresses:
{{ toYaml .tlsConfig.ipAddresses | indent 4 }}
Expand Down
7 changes: 7 additions & 0 deletions charts/pulsar/templates/_proxy.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ specific language governing permissions and limitations
under the License.
*/}}

{{/*
Define the proxy service (ordinary ClusterIP, targeted by clients)
*/}}
{{- define "pulsar.proxy.service" -}}
{{ template "pulsar.fullname" . }}-{{ .Values.proxy.component }}
{{- end }}

{{/*
Define proxy tls certs mounts
*/}}
Expand Down
2 changes: 1 addition & 1 deletion charts/pulsar/templates/proxy-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
apiVersion: v1
kind: Service
metadata:
name: "{{ template "pulsar.fullname" . }}-{{ .Values.proxy.component }}"
name: "{{ template "pulsar.proxy.service" . }}"
namespace: {{ template "pulsar.namespace" . }}
labels:
{{- include "pulsar.standardLabels" . | nindent 4 }}
Expand Down
2 changes: 1 addition & 1 deletion charts/pulsar/templates/proxy-statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ metadata:
{{- include "pulsar.standardLabels" . | nindent 4 }}
component: {{ .Values.proxy.component }}
spec:
serviceName: "{{ template "pulsar.fullname" . }}-{{ .Values.proxy.component }}"
serviceName: "{{ template "pulsar.proxy.service" . }}"
{{- if not .Values.proxy.autoscaling.enabled }}
replicas: {{ .Values.proxy.replicaCount }}
{{- end }}
Expand Down
Loading